diff --git a/CHANGELOG.md b/CHANGELOG.md index 933dbdc6..0ca621af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ [English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md) · [简体中文](CHANGELOG.zh-CN.md) +## Unreleased + +- Added a rich file viewer to the **Files** card: Markdown files (`.md`, `.markdown`, `.mdown`, `.mkd`) render as formatted GitHub-Flavored Markdown with headings, lists, task lists, tables, blockquotes, horizontal rules, inline code, and fenced code blocks with syntax highlighting, recognized code files are syntax-highlighted by the language detected from their extension while unknown languages stay plain monospaced text, all rendered content is sanitized so raw HTML, scripts, event-handler attributes, and dangerous link schemes never render, `http(s)` links open in the app's built-in browser with other schemes inert, only embedded `data:` images show while remote or relative sources fall back to alt text or a placeholder without a network request, and text and code files above the rich-rendering size threshold fall back to plain text with a notice. +- Added a read-only **Files** canvas card and reader: open it from the empty-canvas context menu or the `Cmd/Ctrl+K` palette, root it at a live terminal session working directory or a folder picked in a native dialog, browse a directory tree that loads on demand, and read text/code as selectable monospace (bounded at 2 MB with a truncation marker and total size) or supported images (bounded at 25 MB) with explicit binary, unsupported, and too-large states. Quick open searches file names inside the active root, each card's root, bounds, open file, and expanded folders persist and restore with an unavailable state when the root is gone, and a new main-process `FileAccessService` enforces `realpath` containment so every listing, read, and search uses relative paths strictly inside registered roots with no write, rename, or delete capability. + ## 1.7.1 - Added built-in stable-release updates with explicit download and installation, active-session shutdown confirmation, and signed Sparkle updates on macOS. Install this first updater-enabled release manually; later compatible releases can be installed from the app. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 10ac6349..a3fe11c1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,6 +38,7 @@ Electron main process ├── PluginAgentTools / PluginSessions / PluginCards → plugin tools in canvastty_agents, session events and owned-card control, card badges and actions ├── PluginSecretsService → OS-backed encrypted plugin credentials with fail-closed availability ├── PluginMediaService → user-granted music folders, ranged audio streams, playlist files + ├── FileAccessService → read-only, root-scoped file listing, reading, and search ├── HermesHudService → permission-gated Hermes Desktop HUD lifecycle through a fixed control contract ├── BrowserService → tabs, shared persistent profile, downloads, presence, WebContentsView lifecycle │ ├── BrowserStore / BrowserPolicyService / BrowserAuditStore @@ -51,7 +52,7 @@ Electron main process - `src/shared/contracts.ts` is the single public contract between processes. Add or change cross-process data here first. - `src/preload/index.ts` exposes only the typed capabilities the renderer needs. Node integration stays disabled; context isolation and sandbox stay enabled. - Terminal file drops resolve native `File` objects through preload's `webUtils.getPathForFile`, format paths for the host's default shell, and paste through xterm without submitting. File contents are not read and no new main-process IPC is exposed. -- `src/main/ipc/registerIpc.ts` owns native side effects and validates access to persisted media. +- `src/main/ipc/registerIpc.ts` owns native side effects, validates access to persisted media, and guards the allow-listed `files:*` channels with the trusted-main-renderer check. - `src/main/services/TerminalManager.ts` is the source of truth for live session state and PTY buffers. It keeps scrollback in a bounded chunk buffer and coalesces PTY data into 16ms IPC batches so clear/redraw sequences reach xterm together. A plain terminal starts `idle`; an agent stays `unavailable` until its provider emits a machine-readable lifecycle signal. Codex, Claude Code, Qwen Code, Kimi Code, OpenCode, Hermes, and Grok Build then transition through `idle`, `working`, and `needs_approval` from provider hooks; exact Claude/Qwen OSC 0/2 markers remain a compatibility fallback. Human-readable terminal text and PTY existence are never treated as activity. Process exit provides only `done` or `failed`. An exited PTY may be restarted under the same session ID while preserving its card, bounds, title, and scrollback. Optional restart persistence (Settings → General, "Agent sessions after restart": Don't save / Reopen windows / Continue conversations) writes session records v2 through `TerminalSessionStore`: the card descriptor, the state at quit or exit, the provider conversation id a lifecycle hook reported (`threadId`: Codex thread or Claude session UUID, OpenCode `ses_` id), the per-card restore flag, and two opaque plugin slots (launch options and an environment reference, at most 4 KB each). It never writes PTY scrollback, prompts, child environment, secrets, or capabilities. Plain terminals reopen as fresh shells in their saved folder. - `src/renderer/src/features/terminal/webglContextPool.ts` decides which terminal cards draw with xterm's WebGL renderer. Chromium keeps at most 16 WebGL contexts per renderer process, so the pool hands out 10: the focused card first, then by on-screen area (a busy holder keeps its context against a card less than 1.25× larger), then by most recent output. Camera and layout changes settle for 200 ms before contexts move; a card that leaves the screen, zooms above 1× or enters summary mode releases its context (and explicitly loses it) at once. Every other card uses the DOM renderer. A lost context puts the card back on DOM with its buffer intact and keeps it there for 30 s, doubling on repeats; returning to DOM refits the grid, because WebGL cells are snapped down to whole device pixels. - `src/main/services/LimitsService.ts` reads Codex through the installed CLI's app-server protocol and Claude, Kimi, OpenCode Go, and Grok Build through their provider usage or billing endpoints. Qwen Code is multi-provider and exposes no provider-neutral read-only quota protocol, so its adapter reports `cli-not-found` or `unsupported-protocol` and never invents percentages. Provider credentials are read only inside the trusted main process, sent only to the matching provider over HTTPS, and never logged or exposed over IPC. The service owns timeout, structural normalization, caching, stale fallback, and subprocess cleanup; raw provider responses never cross IPC. @@ -64,6 +65,7 @@ Electron main process - `src/main/services/PluginAgentTools.ts` lists trusted services' `tools` per session role (the orchestration handler answers the helper's `list_tools`; `TerminalManager` gives a non-orchestrator card the `canvastty_agents` bridge only when a plugin tool lists its role) and routes `__` calls to `canvastty.tools.call`, masked and bounded. `PluginSessions.ts` turns terminal-manager events into `canvastty.sessions.event` notifications (metadata; masked screen text only with `sessions:read-screen`) and runs `sessions.create/send/stop` with per-plugin ownership saved in the card's session record (`ownerPluginId`). `PluginCards.ts` keeps plain-text badges and declared card actions, pushes them to the renderer (`plugins:card-decorations-changed`) and calls `canvastty.cards.invoke`. - `src/main/services/PluginSecretsService.ts` serializes per-plugin secret writes, encrypts the complete bounded payload through Electron `safeStorage`, rejects plaintext-only backends, and removes each encrypted file on uninstall. `ProviderSecretsService.ts` applies the same architecture to provider API keys for BYOK-capable CLIs: values stay in the main process, and the renderer contract exposes only per-key `configured` flags plus set/clear actions. `ApiProfile` settings entries name model backends (protocol, HTTPS base URL, secret reference) for the same BYOK runtimes; they are not agent providers, and the settings normalizer drops invalid profiles instead of repairing them. - `src/main/services/PluginMediaService.ts` persists per-plugin grants only after a native folder choice, hides absolute paths, skips symlinks, and serves contained audio with HTTP Range semantics. Playlist reads stay inside granted libraries; writes are bounded and atomic under the library's `Playlists/` directory. +- `src/main/services/FileAccessService.ts` is the single owner of read-only, root-scoped file access. A root is registered either from a terminal session's working directory, which main resolves through `TerminalManager`, or from a folder the user picked in the native dialog. The renderer receives only an opaque `rootId` and a display label, and addresses content with relative paths; the `files:*` channels (`list-roots`, `register-root`, `open-folder`, `list`, `read`, `search`, `close-root`) never carry an absolute path for list/read/search. Every operation canonicalizes root and target with `realpath`, rejects absolute paths, `..` traversal, symlinks that resolve outside the root, and non-regular files, and re-checks containment around the open. Text returns bounded UTF-8 with a `truncated` flag and total size, images return a preview under a MIME allow-list and byte cap, and binary content returns an explicit unsupported reason with no bytes. Directory listings are capped and ordered directories-before-files; search is bounded by depth and result count and skips VCS and dependency directories. Roots are released when their card closes and re-validated on restore, and there is no write, rename, or delete path. - `src/main/services/HermesHudService.ts` is the only plugin-facing native application controller. It resolves the installed Hermes CLI through the immutable provider registry, sends only the fixed `--hud`/`--quit` control commands, and derives visible state from Hermes Desktop's validated live runtime record. It never accepts executable paths, arguments, PIDs, or arbitrary commands from plugin code. - `src/main/services/BrowserService.ts` is the only owner of the built-in browser's `WebContentsView` tabs and shared persistent partition. Remote pages have no preload or Node access, keep context isolation and sandbox enabled, and cannot request hardware, location, notification, clipboard-read, certificate-bypass, or external-protocol capabilities. HTTP(S) popups are adopted as internal tabs; other schemes are rejected. - `src/main/services/browser/` contains the browser kernel. `BrowserStore` atomically persists only tab order, active tab, and safe restore URLs. `BrowserPolicyService` centralizes URL, permission, download, and upload rules; validated uploads are copied through an already-open no-follow file descriptor into private staging before Chromium sees them. `BrowserAutomationService` attaches Electron's internal debugger to the existing live tab without a remote-debugging port. `BrowserCommandDispatcher` adds revisions, revision-bound refs, mutation request deduplication, per-tab FIFO mutation lanes, bounded concurrency, typed errors, and redacted fail-closed audit for agent mutations. @@ -102,6 +104,7 @@ App │ ├── CanvasCommandPalette searchable sessions and the same global creation/launch actions │ ├── PluginCanvasCard sandboxed plugin app with canvas bounds and semantic summary │ ├── BrowserCard trusted browser chrome and canvas geometry for the native WebContentsView +│ ├── FileBrowserCard read-only tree and viewer for files inside a registered root │ └── CanvasMinimap viewport/entity overview, camera recentering, and canvas-direction drag panning ├── AgentLaunchDialog fixed provider + folder + profile + launch └── SettingsPanel two-pane icon-sidebar modal for General, Appearance, Agents, Controls, Browser, Plugins, and About diff --git a/docs/README.md b/docs/README.md index cabf3fdb..39aca2bc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ CanvasTTY is a spatial Electron desktop for real local terminals and AI-agent CL |:--|:--| | [Getting started](getting-started.md) | Requirements, local launch, first session, and verification commands | | [Built-in browser and audit log](browser.md) | Canvas controls, settings, agent access, website/file boundaries, activity, and persistent redacted audit files | +| [File browser and reader](files.md) | Opening a Files card, session and folder roots, supported content, read limits, quick open, persistence, and read-only security boundaries | | [Web companion](mobile-companion.md) | Tailscale Serve HTTPS setup, browser pairing, session grants, revocation, and security limits | | [Installing, releases, and local data](installing-and-security.md) | Installer formats, unsigned-preview caveats, credential boundaries, and release checks | | [Widget authoring](widget-authoring.md) | Source-level extension paths, visual grammar, process boundaries, and an AI-agent brief | diff --git a/docs/UI_CONTRACT.md b/docs/UI_CONTRACT.md index 581c6ca8..37b2c4b3 100644 --- a/docs/UI_CONTRACT.md +++ b/docs/UI_CONTRACT.md @@ -60,6 +60,8 @@ This contract preserves the approved MVP concept and prevents feature ownership - Controls includes an off-by-default **Copy on selection** setting, persisted and applied immediately to every agent and terminal, including fullscreen cards. Completing a non-empty mouse selection copies its text to the system clipboard without clearing the highlight; search results and cleared selections do not copy. With the setting enabled, Shift forces text selection in mouse-reporting TUIs on all desktop platforms; on macOS the terminal mouse adapter translates it to xterm's Option selection without taking the canvas's Option navigation binding. - Canvas plugin apps use the same movable card grammar, `54px` header, resize/snap behavior, and semantic summary below `0.5×`. A `window` contribution opens a separate CanvasTTY-owned sandboxed window; arbitrary native window embedding is not part of the contract. - The built-in Browser is one movable, resizable core canvas card, not a plugin contribution. It uses the same `54px` outer window header as other canvas cards, with its identity and hide-card action isolated from the internal tab strip below. Its trusted DOM chrome owns compact tabs/favicons, address/search, back/forward/reload, downloads, per-tab provider badges, site dialogs, and explicit Close tab/Close all. Hiding the card keeps its tabs and shared authenticated Chromium profile alive. Below `0.5×`, during Edit HOME, behind trusted dialogs/popovers, and while overlapped by a higher renderer-owned canvas window, the native page is hidden and replaced with a stable semantic surface. During card or camera motion it remains live and follows frame-coalesced viewport geometry. +- The Files card is a movable, resizable trusted canvas card using the same `54px` header, edge/corner resize, snapping, focus, stacking, minimap marker, and close grammar as other cards. Its header shows the root label and the active file; its body pairs a tree that expands directories on demand with a read-only viewer, and quick open searches file names inside the active root. Text and code render as selectable monospace text with a truncation banner, supported images render as a preview, and binary, oversized, unavailable, or out-of-root content shows an explicit non-content state with the reason. Below `0.5×` it collapses to the shared semantic summary. It never exposes a write, rename, or delete action. +- The Files viewer renders Markdown (`.md`, `.markdown`, `.mdown`, `.mkd`) as formatted GitHub-Flavored Markdown with fenced code blocks syntax-highlighted, and syntax-highlights recognized code files by the language detected from their extension while unknown languages stay plain monospaced text. All rendered output is sanitized: raw HTML and scripts never render and event-handler attributes and dangerous link schemes such as `javascript:` are neutralized. Activating an `http(s)` link opens it in the built-in browser while other schemes are inert, only embedded `data:` images render while remote or relative sources fall back to alt text or a placeholder without a network request, and text or code above the rich-rendering size threshold falls back to plain text with a notice. - An authenticated connection or heartbeat alone never shows agent presence. A branded badge appears only after that agent actually issues a browser command, and its cursor appears only after a real pointer position exists. Claude uses `#D97757`, Codex `#10A37F`, Qwen `#6D44E8`, Kimi `#7C5CFC`, OpenCode `#5A5858`, Hermes `#D6A700`, and an unknown provider `#7A8291`. Browser Settings separately controls indicator visibility without revoking browser access; the agent-access kill switch revokes the connection itself. - With window snapping enabled, drag and resize use a hidden `10px` grid and a `10px` magnetic threshold for neighboring edges, centers, and a consistent `20px` gap. - One trusted context dispatcher owns right-clicks on the canvas. Truly empty canvas offers region creation, a sticky note, the Settings-configured launcher submenu, Browser, and Settings. Holding RMB on empty canvas also opens the configurable radial quick launcher: moving toward an action and releasing activates it, while a stationary right-click continues to open the standard empty-canvas menu. Region and note right-clicks remain context-specific. A region offers rename, color, a note at the pressed point, and region-only deletion. A note frame offers edit, bring-to-front, and deletion. Terminal, Browser, plugin, textarea/input, and contenteditable surfaces keep their native context menus; middle-button panning is unchanged. `Cmd/Ctrl+K` opens a searchable palette over live sessions and the same global creation/launch actions. Regions persist independently of terminal restore, render behind HOME and windows, resize from every edge/corner, and move every terminal, plugin, Browser, or note whose complete bounds were inside the region at drag start. Attached contents follow the region continuously during the gesture and persist once at pointer release; deleting a region never deletes its contents. Sticky notes persist bounded text and geometry, snap like other windows, and resize from every edge/corner. General Settings exposes independent **Save colored regions after exit** and **Save notes after exit** controls. Both default on to preserve existing profiles. Turning either off removes only that collection from disk persistence without deleting its live canvas objects; turning it back on saves the current collection, while leaving it off starts the next launch with that collection empty. diff --git a/docs/files.md b/docs/files.md new file mode 100644 index 00000000..824938c3 --- /dev/null +++ b/docs/files.md @@ -0,0 +1,65 @@ +# File browser and reader + +[English](files.md) · [Docs home](README.md) + +CanvasTTY's **Files** card reads project files strictly inside a root you choose, without leaving the canvas. It is a trusted, read-only feature: it never writes, renames, or deletes, and it exposes no arbitrary filesystem access. + +## Open a Files card + +1. Choose **Files** from the empty-canvas context menu or the `Cmd/Ctrl+K` canvas command palette. A Files card appears on the canvas with a root selection. +2. Move, resize, focus, stack, and close it like any other canvas card. Below `0.5×` zoom it collapses to the shared semantic summary with the root label. +3. In the card, either open a session's working directory or pick a folder on disk. + +The card lives entirely on the canvas. Closing it releases its root; opening a new one starts from root selection. + +## Roots + +A root is the only directory the card can read. Every listing, read, and search is addressed by a path relative to that root. + +| Root | How it is chosen | What happens | +|:--|:--|:--| +| **Session working directory** | Select one of the live sessions offered in the card | Main resolves the session ID to its current working directory through the terminal service; the renderer never supplies a path | +| **Picked folder** | Choose the folder button, then pick a folder in the native dialog | Main canonicalizes the folder and registers it for that card | + +The renderer receives only an opaque root ID and a display label. Listings, reads, and searches never carry the absolute root path: those calls always pair the root ID with a relative path, and their payloads stay relative. The one exception is a **folder** root's descriptor, which may also carry its chosen canonical path so the card can persist and re-register that folder after relaunch; session-root descriptors omit it, so a session's working directory never crosses the bridge. + +## Supported content + +- **Markdown** — `.md`, `.markdown`, `.mdown`, and `.mkd` files are rendered as formatted GitHub-Flavored Markdown: headings, lists, task lists, tables, blockquotes, horizontal rules, inline code, and fenced code blocks with syntax highlighting. +- **Code** — recognized code files are syntax-highlighted by the language detected from the file extension, using a palette that matches the active night or day theme. A text file whose language cannot be determined is shown as readable monospaced text without highlighting and without error. +- **Images** — `png`, `jpg`/`jpeg`, `gif`, `webp`, `bmp`, and `svg` are shown as a preview with their media type. +- **Everything else** — binary content and unsupported or oversized files show an explicit non-content state with the reason. CanvasTTY never fabricates or partially decodes placeholder file data. + +Rendered Markdown and code are sanitized: raw HTML and scripts are never rendered as live markup, event-handler attributes and dangerous link schemes such as `javascript:` are neutralized, and untrusted content is never injected as raw HTML. Activating an `http(s)` link opens it in the app's built-in browser instead of navigating the viewer, while any other scheme is inert. Only embedded `data:` images render; remote or relative image sources show their alternative text or a placeholder and never trigger a network request. + +## Limits + +| Limit | Value | +|:--|:--| +| Text read | Leading `2 MB` returned, with a **truncated** marker and the total file size | +| Image read | `25 MB` maximum; larger images report **too large** and return no bytes | +| Directory listing | Up to `2,000` immediate entries | +| Search results | Up to `500` matching files | +| Search depth | Up to `12` directory levels | + +Long text reads are bounded rather than rejected: the card shows the leading portion, marks it truncated, and reports the file's total size. Binary detection never returns bytes as if they were text. Rich rendering is skipped above a roughly `250 KB` threshold: larger text and code files fall back to plain selectable monospace text with a notice that rich rendering is disabled, while the `2 MB` read cap and truncation marker still apply. + +## Quick open + +Type in the card's **Quick open** field to search file names inside the active root. Results are relative paths; select one to open it. Directories skipped during search are `node_modules`, `.git`, `dist`, `out`, `build`, and `.cache`. A query with no matches reports that plainly instead of guessing. + +## Persistence and restore + +Each Files card persists its root reference, bounds, open file, and expanded folders in app settings, consistent with other canvas entities. On relaunch the root is re-validated: a card whose root still exists restores its bounds, expanded folders, and previously open file, while a root that has moved or disappeared shows an **unavailable** state explaining that it cannot be read instead of reading an unrelated location. + +## Security boundaries + +- **Reads stay inside registered roots.** A root is either an existing terminal session working directory or a folder the user explicitly picked; there is no path into the filesystem outside one. +- **Relative paths only.** Requests address the root by opaque ID plus a relative path. Absolute paths are rejected. +- **Containment is enforced in main.** Root and target are canonicalized with `realpath` and the target must be the root itself or a descendant. Parent-directory traversal, symlinks that resolve outside the root, and non-regular files (directories, sockets, devices) are rejected, and containment is re-checked around the open. +- **Read-only by design.** There is no write, rename, delete, or arbitrary filesystem capability, and nothing is uploaded; file content stays on the machine and is never logged. +- **No content leaves the boundary.** List, read, and search payloads expose relative paths only. A folder root's descriptor alone may carry its chosen absolute path, solely so restore can re-register it; session roots never expose their working directory. + +A file that is sensitive but lives inside a root you chose (for example a project's `.env`) is readable by explicit user action. CanvasTTY never auto-expands a root beyond the directory you opened and never scans outside it. + +For service ownership and the `files:*` IPC channels, read [Architecture](ARCHITECTURE.md). For canvas and interaction invariants, read the [UI contract](UI_CONTRACT.md). diff --git a/package-lock.json b/package-lock.json index 301d8211..992853d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,13 @@ "dependencies": { "@xterm/headless": "6.0.0", "electron-updater": "6.8.9", + "hast-util-to-jsx-runtime": "2.3.6", + "lowlight": "3.3.0", "node-pty": "1.1.0", + "react-markdown": "10.1.0", + "rehype-highlight": "7.0.2", + "rehype-sanitize": "6.0.0", + "remark-gfm": "4.0.1", "secure-remote-password": "0.3.1", "unzipper": "^0.12.3", "yaml": "2.9.0" @@ -54,1339 +60,1184 @@ "vite": "8.2.2" } }, - "integrations/even-g2/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], + "integrations/even-g2/node_modules/@evenrealities/evenhub-cli": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@evenrealities/evenhub-cli/-/evenhub-cli-0.1.14.tgz", + "integrity": "sha512-aSaCuj01LDaRUinXu1uAAw6743oGGB6Um8mEiXB5M0mj3dbqzdJ9dijSM1lGcuDX/+wFTa/qUdbx7QippBlDuQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "chalk": "^5.6.2", + "commander": "^14.0.2", + "inquirer": "^13.1.0", + "js-yaml": "^4.1.1", + "open": "^11.0.0", + "qr-image": "^3.2.0", + "qrcode-terminal": "^0.12.0", + "zod": "^4.3.2" + }, + "bin": { + "eh": "main.js", + "evenhub": "main.js" + }, + "peerDependencies": { + "typescript": "^5" } }, - "integrations/even-g2/node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], + "integrations/even-g2/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, "engines": { - "node": ">=18" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "integrations/even-g2/node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], + "integrations/even-g2/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=20" } }, - "integrations/even-g2/node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], + "integrations/even-g2/node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": "^10 || ^12 || >=14" } }, - "integrations/even-g2/node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], + "integrations/even-g2/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "integrations/even-g2/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], + "integrations/even-g2/node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "integrations/even-g2/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } }, - "integrations/even-g2/node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "integrations/even-g2/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "integrations/even-g2/node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "integrations/even-g2/node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "integrations/even-g2/node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/@evenrealities/evenhub-cli": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@evenrealities/evenhub-cli/-/evenhub-cli-0.1.14.tgz", - "integrity": "sha512-aSaCuj01LDaRUinXu1uAAw6743oGGB6Um8mEiXB5M0mj3dbqzdJ9dijSM1lGcuDX/+wFTa/qUdbx7QippBlDuQ==", + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^5.6.2", - "commander": "^14.0.2", - "inquirer": "^13.1.0", - "js-yaml": "^4.1.1", - "open": "^11.0.0", - "qr-image": "^3.2.0", - "qrcode-terminal": "^0.12.0", - "zod": "^4.3.2" - }, - "bin": { - "eh": "main.js", - "evenhub": "main.js" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, - "peerDependencies": { - "typescript": "^5" + "engines": { + "node": ">=6.9.0" } }, - "integrations/even-g2/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" } }, - "integrations/even-g2/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=20" + "node": "*" } }, - "integrations/even-g2/node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "peer": true, + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, "bin": { - "esbuild": "bin/esbuild" + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" + "node": ">=10" } }, - "integrations/even-g2/node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" } }, - "integrations/even-g2/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/@electron/get/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "license": "Apache-2.0", - "peer": true, + "license": "ISC", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "semver": "bin/semver.js" }, "engines": { - "node": ">=14.17" + "node": ">=10" } }, - "integrations/even-g2/node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">= 10.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, "engines": { - "node": ">=6.9.0" + "node": ">=12.0.0" } }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, "engines": { - "node": ">=6.9.0" + "node": ">= 8.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "url": "https://github.com/sponsors/gjtorikian/" } }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "node_modules/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" }, "engines": { - "node": ">=6.9.0" + "node": ">=22.12.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.4" } }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.14" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16 || 14 >=14.17" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.14" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.14" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", - "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@electron-internal/extract-zip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", - "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=22.12.0" + "node": ">=18" } }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@electron/get": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", - "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "undici": "^7.24.4" + "node": ">=18" } }, - "node_modules/@electron/get/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 10.0.0" + "node": ">=18" } }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "node": ">=18" } }, - "node_modules/@electron/rebuild": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", - "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", - "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", - "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/openharmony-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "openharmony" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { + "node_modules/@esbuild/sunos-x64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "sunos" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm64": { + "node_modules/@esbuild/win32-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -1394,465 +1245,273 @@ "license": "MIT", "optional": true, "os": [ - "android" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { + "node_modules/@esbuild/win32-ia32": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@esbuild/win32-x64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@evenrealities/even_hub_sdk": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@evenrealities/even_hub_sdk/-/even_hub_sdk-0.0.14.tgz", + "integrity": "sha512-lNtyIldZfe0dhAVxCfNuJ7waH2lMPli8DyklA8jF4DlvByQtbK1janodPNo7M8+7Plp30mlQT8d+vYU3gO8vKQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": "^20.0.0 || >=22.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "node_modules/@fontsource/ibm-plex-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz", + "integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/checkbox": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", + "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], + "node_modules/@inquirer/checkbox/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], + "node_modules/@inquirer/checkbox/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], + "node_modules/@inquirer/confirm/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], + "node_modules/@inquirer/confirm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/editor": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", + "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/external-editor": "^3.0.5", + "@inquirer/type": "4.1.1" + }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@evenrealities/even_hub_sdk": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@evenrealities/even_hub_sdk/-/even_hub_sdk-0.0.14.tgz", - "integrity": "sha512-lNtyIldZfe0dhAVxCfNuJ7waH2lMPli8DyklA8jF4DlvByQtbK1janodPNo7M8+7Plp30mlQT8d+vYU3gO8vKQ==", - "license": "MIT", - "engines": { - "node": "^20.0.0 || >=22.0.0" - } - }, - "node_modules/@fontsource/ibm-plex-mono": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", - "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/ibm-plex-sans": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz", - "integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@inquirer/ansi": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", - "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", - "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.8", - "@inquirer/core": "^12.0.3", - "@inquirer/figures": "^2.0.9", - "@inquirer/type": "4.1.1" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/checkbox/node_modules/@inquirer/core": { + "node_modules/@inquirer/editor/node_modules/@inquirer/core": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", @@ -1879,7 +1538,7 @@ } } }, - "node_modules/@inquirer/checkbox/node_modules/signal-exit": { + "node_modules/@inquirer/editor/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", @@ -1892,10 +1551,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@inquirer/confirm": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", - "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "node_modules/@inquirer/expand": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", + "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", "dev": true, "license": "MIT", "dependencies": { @@ -1914,7 +1573,7 @@ } } }, - "node_modules/@inquirer/confirm/node_modules/@inquirer/core": { + "node_modules/@inquirer/expand/node_modules/@inquirer/core": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", @@ -1941,7 +1600,7 @@ } } }, - "node_modules/@inquirer/confirm/node_modules/signal-exit": { + "node_modules/@inquirer/expand/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", @@ -1954,20 +1613,15 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@inquirer/core": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", - "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "node_modules/@inquirer/external-editor": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", + "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1981,28 +1635,24 @@ } } }, - "node_modules/@inquirer/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, - "node_modules/@inquirer/editor": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", - "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", + "node_modules/@inquirer/input": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", + "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^12.0.3", - "@inquirer/external-editor": "^3.0.5", "@inquirer/type": "4.1.1" }, "engines": { @@ -2017,7 +1667,7 @@ } } }, - "node_modules/@inquirer/editor/node_modules/@inquirer/core": { + "node_modules/@inquirer/input/node_modules/@inquirer/core": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", @@ -2044,163 +1694,7 @@ } } }, - "node_modules/@inquirer/editor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@inquirer/expand": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", - "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^12.0.3", - "@inquirer/type": "4.1.1" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand/node_modules/@inquirer/core": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", - "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.8", - "@inquirer/figures": "^2.0.9", - "@inquirer/type": "4.1.1", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@inquirer/external-editor": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", - "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", - "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - } - }, - "node_modules/@inquirer/input": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", - "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^12.0.3", - "@inquirer/type": "4.1.1" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/input/node_modules/@inquirer/core": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", - "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.8", - "@inquirer/figures": "^2.0.9", - "@inquirer/type": "4.1.1", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/input/node_modules/signal-exit": { + "node_modules/@inquirer/input/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", @@ -3485,7 +2979,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -3495,9 +2988,17 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3508,6 +3009,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -3525,11 +3035,19 @@ "@types/node": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -3546,7 +3064,6 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3572,6 +3089,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -3912,6 +3435,12 @@ "node": ">=16.20.0" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -4310,6 +3839,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -4566,6 +4105,16 @@ "resolved": "integrations/even-g2", "link": true }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4583,6 +4132,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", @@ -4694,6 +4283,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -4777,7 +4376,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -4797,6 +4395,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4927,6 +4538,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4945,6 +4565,19 @@ "license": "MIT", "optional": true }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -5453,6 +5086,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -5460,6 +5103,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5922,12 +5571,105 @@ "node": ">= 0.4" } }, - "node_modules/hex-to-array-buffer": { + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hex-to-array-buffer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-to-array-buffer/-/hex-to-array-buffer-1.1.0.tgz", "integrity": "sha512-vvl3IM8FfT1uOnHtEqyjkDK9Luqz6MQrH82qIvVnjyXxRhkeaEZyRRPiBgf2yym3nweRVEfayxt/1SoTXZYd4Q==", "license": "MIT" }, + "node_modules/highlight.js": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.2.tgz", + "integrity": "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -5961,6 +5703,16 @@ "dev": true, "license": "ISC" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -6045,6 +5797,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/inquirer": { "version": "13.4.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.4.3.tgz", @@ -6072,6 +5830,40 @@ } } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -6098,6 +5890,16 @@ "node": ">=8" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-in-ssh": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", @@ -6130,6 +5932,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -6592,6 +6406,16 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -6602,50 +6426,943 @@ "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", + "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "micromark-util-types": "^2.0.0" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "optional": true, "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -7058,6 +7775,31 @@ "integrity": "sha512-EBN8Ez1SVRcZT1XsIE4WkdnZ5coLoaChkIgAET6gIlaLhXqCz9upVk0DQWFtOYkrpTVvbEppRUnqhTiJrBdkfw==", "license": "MIT" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7283,6 +8025,16 @@ "signal-exit": "^3.0.2" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -7347,7 +8099,6 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7366,6 +8117,33 @@ "react": "^19.2.8" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -7404,6 +8182,103 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7802,6 +8677,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -7844,6 +8729,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7857,6 +8756,24 @@ "node": ">=8" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -7999,6 +8916,26 @@ "tmp": "^0.2.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -8083,6 +9020,107 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -8163,6 +9201,34 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -8900,6 +9966,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 29a2321b..6eeb64f5 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,13 @@ "dependencies": { "@xterm/headless": "6.0.0", "electron-updater": "6.8.9", + "hast-util-to-jsx-runtime": "2.3.6", + "lowlight": "3.3.0", "node-pty": "1.1.0", + "react-markdown": "10.1.0", + "rehype-highlight": "7.0.2", + "rehype-sanitize": "6.0.0", + "remark-gfm": "4.0.1", "secure-remote-password": "0.3.1", "unzipper": "^0.12.3", "yaml": "2.9.0" diff --git a/src/main/index.ts b/src/main/index.ts index e2fb63e2..12243aec 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -24,6 +24,7 @@ import { ManualReleaseAdapter } from "./services/updates/ManualReleaseAdapter"; import { registerMaterialIpc } from "./ipc/registerMaterialIpc"; import { textResponse } from "./services/fileResponse"; import { SettingsStore } from "./services/SettingsStore"; +import { FileAccessService } from "./services/FileAccessService"; import { SkinRegistry } from "./services/SkinRegistry"; import { PixelSkinPackRegistry } from "./services/PixelSkinPackRegistry"; import { TerminalManager, reachesObservers, reachesRenderer } from "./services/TerminalManager"; @@ -774,6 +775,14 @@ async function initializeServices(ipc: IpcRegistrar): Promise { await Promise.all([browserReady, storesLoaded]); pluginManager.registerTokenProvider(() => githubAuth!.getToken()); protocol.handle("canvastty-media", (request) => pluginMediaService!.protocolResponse(request)); + // Session roots resolve a sessionId to the terminal's current working + // directory through TerminalManager's metadata snapshots; the renderer never + // supplies a path. Folder roots come from the native dialog or a persisted + // folderPath on restore. + const fileAccessService = new FileAccessService({ + resolveSessionCwd: (sessionId) => + terminalManager?.listMetadata().find((session) => session.id === sessionId)?.cwd + }); materialService = new MaterialService({ userDataPath: app.getPath("userData"), persist: () => settings.get().persistMaterials, @@ -785,6 +794,7 @@ async function initializeServices(ipc: IpcRegistrar): Promise { }); await materialService.load(); registerMaterialIpc(ipc, { materials: materialService, getMainWindow: () => mainWindow }); + registerIpc(ipc, { settings, recheckProviderClis: async () => { @@ -806,6 +816,7 @@ async function initializeServices(ipc: IpcRegistrar): Promise { pluginSecrets: pluginSecretsService, providerSecrets: providerSecretsService!, browser: browserService, + files: fileAccessService, githubAuth: githubAuth!, hermesHud: hermesHudService, launchFieldOptions: (pluginId, provider) => launchPipeline.fieldOptions(pluginId, provider), diff --git a/src/main/ipc/registerIpc.ts b/src/main/ipc/registerIpc.ts index 72a6c87e..bae07a6c 100644 --- a/src/main/ipc/registerIpc.ts +++ b/src/main/ipc/registerIpc.ts @@ -9,6 +9,7 @@ import type { CanvasNavigationPointerBindingInput, CustomTerminalBorderSkinId, CreateSessionRequest, + FileRootReference, PluginBrowserOpenResponse, PluginCanvasRequest, PluginLaunchFieldOptions, @@ -38,6 +39,7 @@ import type { PluginMediaService } from "../services/PluginMediaService"; import type { PluginSecretsService } from "../services/PluginSecretsService"; import type { ProviderSecretsService } from "../services/ProviderSecretsService"; import type { BrowserService } from "../services/BrowserService"; +import type { FileAccessService } from "../services/FileAccessService"; import type { IpcRegistrar } from "./IpcReadinessGate"; import { normalizePluginBrowserUrl } from "../services/browser/PluginBrowserOpenPolicy"; import { PluginBrowserOpenBroker } from "./PluginBrowserOpenBroker"; @@ -69,6 +71,7 @@ interface Dependencies { pluginSecrets: PluginSecretsService; providerSecrets: ProviderSecretsService; browser: BrowserService; + files: FileAccessService; githubAuth: GithubAuthService; hermesHud: HermesHudService; launchFieldOptions(pluginId: string, provider: ProviderId): Promise; @@ -205,6 +208,7 @@ export function registerIpc(ipcMain: IpcRegistrar, { pluginSecrets, providerSecrets, browser, + files, githubAuth, hermesHud, launchFieldOptions, @@ -321,6 +325,58 @@ export function registerIpc(ipcMain: IpcRegistrar, { } }); + ipcMain.handle(IPC.filesListRoots, (event) => { + assertMainRenderer(event, getMainWindow); + return files.listRoots(); + }); + ipcMain.handle(IPC.filesRegisterRoot, async (event, reference: unknown) => { + assertMainRenderer(event, getMainWindow); + const parsed = fileRootReference(reference); + if (!parsed) throw new Error("File root reference is invalid."); + try { + return await files.registerRoot(parsed); + } catch (error) { + console.warn("CanvasTTY file root could not be registered.", error); + return null; + } + }); + ipcMain.handle(IPC.filesOpenFolder, async (event) => { + assertMainRenderer(event, getMainWindow); + const owner = getMainWindow(); + const options: OpenDialogOptions = { + title: "Choose a folder", + properties: ["openDirectory"] + }; + const result = owner + ? await dialog.showOpenDialog(owner, options) + : await dialog.showOpenDialog(options); + const selected = result.filePaths[0]; + if (result.canceled || !selected) return null; + try { + return files.registerFolderRoot(selected); + } catch (error) { + console.warn("CanvasTTY selected folder could not be opened.", error); + return null; + } + }); + ipcMain.handle(IPC.filesList, async (event, rootId: unknown, relativePath: unknown) => { + assertMainRenderer(event, getMainWindow); + return files.list(stringValue(rootId, "rootId"), fileRelativePath(relativePath)); + }); + ipcMain.handle(IPC.filesRead, async (event, rootId: unknown, relativePath: unknown) => { + assertMainRenderer(event, getMainWindow); + return files.read(stringValue(rootId, "rootId"), fileRelativePath(relativePath)); + }); + ipcMain.handle(IPC.filesSearch, async (event, rootId: unknown, query: unknown) => { + assertMainRenderer(event, getMainWindow); + return files.search(stringValue(rootId, "rootId"), fileSearchQuery(query)); + }); + ipcMain.handle(IPC.filesCloseRoot, (event, rootId: unknown) => { + assertMainRenderer(event, getMainWindow); + files.releaseRoot(stringValue(rootId, "rootId")); + return true; + }); + ipcMain.handle(IPC.limitsGet, (event) => { assertMainRenderer(event, getMainWindow); return limits.get(); @@ -984,6 +1040,43 @@ function stringValue(value: unknown, label: string): string { return value; } +function fileRootReference(value: unknown): FileRootReference | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const reference = value as Record; + if (reference.rootType === "session") { + if (typeof reference.sessionId !== "string" || reference.sessionId.length === 0 || reference.sessionId.length > 2_048) { + return null; + } + return { rootType: "session", sessionId: reference.sessionId }; + } + if (reference.rootType === "folder") { + if (reference.folderPath === undefined) return { rootType: "folder" }; + if ( + typeof reference.folderPath !== "string" + || reference.folderPath.length === 0 + || reference.folderPath.length > 4_096 + ) return null; + return { rootType: "folder", folderPath: reference.folderPath }; + } + return null; +} + +function fileRelativePath(value: unknown): string { + // An empty relative path addresses the root itself, so unlike stringValue it + // is allowed; FileAccessService re-enforces containment on every operation. + if (typeof value !== "string" || value.length > 2_048 || value.includes("\0")) { + throw new Error("File relative path is invalid."); + } + return value; +} + +function fileSearchQuery(value: unknown): string { + if (typeof value !== "string" || value.length > 512) { + throw new Error("File search query is invalid."); + } + return value; +} + function playlistContent(value: unknown): string { if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 4 * 1024 * 1024) { throw new Error("Plugin playlist content is invalid or exceeds 4 MB."); diff --git a/src/main/services/FileAccessService.ts b/src/main/services/FileAccessService.ts new file mode 100644 index 00000000..d1bb30cb --- /dev/null +++ b/src/main/services/FileAccessService.ts @@ -0,0 +1,454 @@ +import { constants, realpathSync, statSync } from "node:fs"; +import { open, readdir, stat } from "node:fs/promises"; +import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { TextDecoder } from "node:util"; +import { randomUUID } from "node:crypto"; +import type { + FileEntry, + FileReadResult, + FileRootDescriptor, + FileRootKind, + FileRootReference, + FileSearchResult +} from "../../shared/contracts"; + +export interface FileAccessLimits { + maxTextBytes: number; + maxImageBytes: number; + maxDirectoryEntries: number; + maxSearchResults: number; + maxSearchDepth: number; + skipDirectories: string[]; +} + +const DEFAULT_LIMITS: FileAccessLimits = { + maxTextBytes: 2_000_000, + maxImageBytes: 25 * 1024 * 1024, + maxDirectoryEntries: 2_000, + maxSearchResults: 500, + maxSearchDepth: 12, + skipDirectories: ["node_modules", ".git", "dist", "out", "build", ".cache"] +}; + +const BINARY_SAMPLE_BYTES = 64 * 1024; +const MAX_CONTROL_RATIO = 0.3; + +const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true }); + +const IMAGE_MIME: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".bmp": "image/bmp", + ".svg": "image/svg+xml" +}; + +interface RegisteredRoot { + rootId: string; + path: string; + label: string; + rootType: FileRootKind; + available: boolean; +} + +interface FileAccessOptions { + resolveSessionCwd: (sessionId: string) => string | undefined; + limits?: Partial; +} + +type ResolvedTarget = + | { ok: true; real: string; root: RegisteredRoot } + | { ok: false; reason: "not-permitted" | "unavailable" }; + +export class FileAccessService { + private readonly roots = new Map(); + private readonly resolveSessionCwd: (sessionId: string) => string | undefined; + private readonly limits: FileAccessLimits; + + constructor(options: FileAccessOptions) { + this.resolveSessionCwd = options.resolveSessionCwd; + this.limits = { + maxTextBytes: options.limits?.maxTextBytes ?? DEFAULT_LIMITS.maxTextBytes, + maxImageBytes: options.limits?.maxImageBytes ?? DEFAULT_LIMITS.maxImageBytes, + maxDirectoryEntries: options.limits?.maxDirectoryEntries ?? DEFAULT_LIMITS.maxDirectoryEntries, + maxSearchResults: options.limits?.maxSearchResults ?? DEFAULT_LIMITS.maxSearchResults, + maxSearchDepth: options.limits?.maxSearchDepth ?? DEFAULT_LIMITS.maxSearchDepth, + skipDirectories: options.limits?.skipDirectories + ? [...options.limits.skipDirectories] + : [...DEFAULT_LIMITS.skipDirectories] + }; + } + + registerSessionRoot(sessionId: string): FileRootDescriptor | null { + const cwd = this.resolveSessionCwd(sessionId); + if (typeof cwd !== "string" || cwd.length === 0) return null; + try { + const canonical = realpathSync(cwd); + if (!statSync(canonical).isDirectory()) return null; + return this.register(canonical, "session"); + } catch { + return null; + } + } + + registerFolderRoot(folderPath: string): FileRootDescriptor { + let canonical: string; + try { + canonical = realpathSync(folderPath); + } catch { + throw new Error("The selected folder is unavailable."); + } + if (!statSync(canonical).isDirectory()) throw new Error("The selected folder is not a directory."); + return this.register(canonical, "folder"); + } + + async registerRoot(reference: FileRootReference): Promise { + if (reference.rootType === "session") return this.registerSessionRoot(reference.sessionId); + // Folder roots are normally created by the native dialog; `folderPath` is + // only supplied when re-registering a persisted root after relaunch. A + // folder that has since disappeared resolves null (unavailable) rather than + // throwing, so restore can surface the missing-root state. + if (typeof reference.folderPath === "string" && reference.folderPath.length > 0) { + try { + return this.registerFolderRoot(reference.folderPath); + } catch { + return null; + } + } + return null; + } + + releaseRoot(rootId: string): void { + this.roots.delete(rootId); + } + + listRoots(): FileRootDescriptor[] { + return [...this.roots.values()].map((root) => this.describe(root)); + } + + async list(rootId: string, relativePath: string): Promise { + const root = this.requireRoot(rootId); + if (!root.available) throw new Error("The file root is unavailable."); + const real = await this.resolveForOperation(root, relativePath); + let metadata; + try { + metadata = await stat(real); + } catch { + throw new Error("The requested directory is unavailable."); + } + if (!metadata.isDirectory()) throw new Error("The requested path is not a directory."); + + let entries; + try { + entries = await readdir(real, { withFileTypes: true }); + } catch { + throw new Error("The requested directory could not be read."); + } + + const base = toPosix(relative(root.path, real)); + const result: FileEntry[] = []; + for (const entry of entries) { + if (entry.isSymbolicLink()) continue; + const kind: FileEntry["kind"] | null = entry.isDirectory() + ? "directory" + : entry.isFile() + ? "file" + : null; + if (kind === null) continue; + const relativeEntry = base.length === 0 ? entry.name : `${base}/${entry.name}`; + let size: number | null = null; + if (kind === "file") { + try { + size = (await stat(join(real, entry.name))).size; + } catch { + continue; + } + } + result.push({ name: entry.name, relativePath: relativeEntry, kind, size }); + } + result.sort(compareEntries); + return result.slice(0, this.limits.maxDirectoryEntries); + } + + async read(rootId: string, relativePath: string): Promise { + const root = this.roots.get(rootId); + if (!root || !root.available) return { kind: "unsupported", reason: "not-permitted" }; + const target = await this.resolveTarget(root, relativePath); + if (!target.ok) return { kind: "unsupported", reason: target.reason }; + + let handle; + try { + handle = await open(target.real, constants.O_RDONLY | constants.O_NOFOLLOW); + } catch (error) { + return { kind: "unsupported", reason: openFailureReason(error) }; + } + try { + const metadata = await handle.stat(); + if (!metadata.isFile()) return { kind: "unsupported", reason: "not-permitted" }; + + // Post-open containment re-checks. `O_NOFOLLOW` guards only the final + // component, so an intermediate directory swapped after `realpath` could + // redirect the open; re-resolve the requested path and compare the + // descriptor's real target (/proc/self/fd or /dev/fd) against the root. + // Residual race: the descriptor path is read after the open, so a swap + // between the open and these checks is caught where the platform exposes + // the fd path, but there is no atomic openat chain here by design. + let resolvedAfterOpen: string; + try { + resolvedAfterOpen = realpathSync(target.real); + } catch { + return { kind: "unsupported", reason: "unavailable" }; + } + if (!isContainedPath(root.path, resolvedAfterOpen)) { + return { kind: "unsupported", reason: "not-permitted" }; + } + const descriptorPath = await descriptorRealPath(handle); + if (descriptorPath !== null && !isContainedPath(root.path, descriptorPath)) { + return { kind: "unsupported", reason: "not-permitted" }; + } + + const size = metadata.size; + + const mediaType = IMAGE_MIME[extname(target.real).toLowerCase()]; + if (mediaType) { + if (size > this.limits.maxImageBytes) return { kind: "too-large", reason: "too-large", size }; + const data = await readExactly(handle, size); + return { + kind: "image", + mediaType, + dataUrl: `data:${mediaType};base64,${data.toString("base64")}`, + size + }; + } + + const limit = this.limits.maxTextBytes; + const inspected = await readExactly(handle, Math.min(size, limit)); + if (looksBinary(inspected)) return { kind: "unsupported", reason: "binary" }; + return { + kind: "text", + content: inspected.toString("utf8"), + truncated: size > limit, + size + }; + } finally { + await handle.close().catch(() => undefined); + } + } + + async search(rootId: string, query: string): Promise { + const root = this.requireRoot(rootId); + if (!root.available) throw new Error("The file root is unavailable."); + const needle = typeof query === "string" ? query.toLowerCase() : ""; + const skip = new Set(this.limits.skipDirectories); + const matches: string[] = []; + let truncated = false; + + const pending: Array<{ dir: string; depth: number }> = [{ dir: root.path, depth: 0 }]; + while (pending.length > 0) { + const { dir, depth } = pending.pop()!; + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) { + if (skip.has(entry.name)) continue; + if (depth < this.limits.maxSearchDepth) { + pending.push({ dir: join(dir, entry.name), depth: depth + 1 }); + } + continue; + } + if (!entry.isFile()) continue; + if (!entry.name.toLowerCase().includes(needle)) continue; + if (matches.length >= this.limits.maxSearchResults) { + truncated = true; + pending.length = 0; + break; + } + matches.push(toPosix(relative(root.path, join(dir, entry.name)))); + } + } + + matches.sort((left, right) => left.localeCompare(right)); + return { relativePaths: matches, truncated }; + } + + revalidateRoot(rootId: string): boolean { + const root = this.roots.get(rootId); + if (!root) return false; + try { + const canonical = realpathSync(root.path); + const valid = canonical === root.path && statSync(root.path).isDirectory(); + root.available = valid; + return valid; + } catch { + root.available = false; + return false; + } + } + + private register(canonicalPath: string, rootType: FileRootKind): FileRootDescriptor { + const root: RegisteredRoot = { + rootId: randomUUID(), + path: canonicalPath, + label: basename(canonicalPath) || "root", + rootType, + available: true + }; + this.roots.set(root.rootId, root); + return this.describe(root); + } + + private describe(root: RegisteredRoot): FileRootDescriptor { + return { + rootId: root.rootId, + label: root.label, + available: root.available, + rootType: root.rootType, + // Folder roots carry their canonical path so the renderer can persist it + // and re-register the root after relaunch. Session roots omit it so the + // session cwd is never exposed across the bridge. + ...(root.rootType === "folder" ? { folderPath: root.path } : {}) + }; + } + + private requireRoot(rootId: string): RegisteredRoot { + const root = this.roots.get(rootId); + if (!root) throw new Error("The file root is not registered."); + return root; + } + + private async resolveForOperation(root: RegisteredRoot, relativePath: string): Promise { + const target = await this.resolveTarget(root, relativePath); + if (!target.ok) { + throw new Error( + target.reason === "not-permitted" + ? "The requested path is outside the root." + : "The requested path is unavailable." + ); + } + return target.real; + } + + private async resolveTarget(root: RegisteredRoot, relativePath: string): Promise { + if (!root.available) return { ok: false, reason: "not-permitted" }; + if (typeof relativePath !== "string" || relativePath.includes("\0") || relativePath.includes("\\")) { + return { ok: false, reason: "not-permitted" }; + } + if (isAbsolute(relativePath)) return { ok: false, reason: "not-permitted" }; + + const candidate = resolve(root.path, relativePath); + if (!isContainedPath(root.path, candidate)) return { ok: false, reason: "not-permitted" }; + + let real; + try { + // Canonicalize with the same resolver used when the root was registered + // (`realpathSync`). The async `realpath` can return a differently-cased or + // 8.3-shortened path on Windows, which would fail the containment check. + real = realpathSync(candidate); + } catch { + return { ok: false, reason: "unavailable" }; + } + if (!isContainedPath(root.path, real)) return { ok: false, reason: "not-permitted" }; + return { ok: true, real, root }; + } +} + +function isContainedPath(root: string, candidate: string): boolean { + // Windows paths are case-insensitive and `realpath` may surface a long (`\\?\`) + // form, so compare normalized values there instead of raw strings. + const normalizedRoot = normalizePathForCompare(root); + const normalizedCandidate = normalizePathForCompare(candidate); + if (normalizedCandidate === normalizedRoot) return true; + const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return normalizedCandidate.startsWith(prefix); +} + +function normalizePathForCompare(value: string): string { + if (process.platform !== "win32") return value; + const withoutLongPrefix = value.startsWith("\\\\?\\") ? value.slice(4) : value; + return withoutLongPrefix.toLowerCase(); +} + +function toPosix(value: string): string { + return value.split(sep).join("/"); +} + +function compareEntries(left: FileEntry, right: FileEntry): number { + if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1; + return left.name.localeCompare(right.name); +} + +async function descriptorRealPath(handle: Awaited>): Promise { + for (const candidate of [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]) { + try { + return realpathSync(candidate); + } catch { + continue; + } + } + return null; +} + +function looksBinary(buffer: Buffer): boolean { + if (buffer.length === 0) return false; + if (buffer.includes(0)) return true; + const sample = buffer.length > BINARY_SAMPLE_BYTES ? buffer.subarray(0, BINARY_SAMPLE_BYTES) : buffer; + if (!isValidUtf8(sample)) return true; + return hasHighControlRatio(sample); +} + +function isValidUtf8(buffer: Buffer): boolean { + // A bounded read may split the final multi-byte code point, so a strict decode + // of the tail can look invalid even when the bytes are valid UTF-8. Decode + // strictly first; if that fails, retry with a streaming decoder that tolerates + // an incomplete trailing sequence while still rejecting genuinely invalid + // bytes. Trimming a fixed number of bytes is wrong: cutting three bytes off a + // complete two-byte character (or the middle of a four-byte one) corrupts it. + try { + STRICT_UTF8.decode(buffer); + return true; + } catch { + try { + new TextDecoder("utf-8", { fatal: true }).decode(buffer, { stream: true }); + return true; + } catch { + return false; + } + } +} + +function hasHighControlRatio(buffer: Buffer): boolean { + let control = 0; + for (const byte of buffer) { + if ((byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) || byte === 0x7f) { + control += 1; + } + } + return control / buffer.length > MAX_CONTROL_RATIO; +} + +function openFailureReason(error: unknown): "not-permitted" | "unavailable" { + const code = error && typeof error === "object" && "code" in error + ? (error as { code?: unknown }).code + : undefined; + if (code === "ELOOP" || code === "EISDIR") return "not-permitted"; + return "unavailable"; +} + +async function readExactly(handle: Awaited>, length: number): Promise { + if (length <= 0) return Buffer.alloc(0); + const buffer = Buffer.allocUnsafe(length); + let offset = 0; + while (offset < length) { + const { bytesRead } = await handle.read(buffer, offset, length - offset, offset); + if (bytesRead <= 0) break; + offset += bytesRead; + } + return offset === length ? buffer : buffer.subarray(0, offset); +} diff --git a/src/main/services/SettingsStore.ts b/src/main/services/SettingsStore.ts index d8963b07..d27856db 100644 --- a/src/main/services/SettingsStore.ts +++ b/src/main/services/SettingsStore.ts @@ -18,6 +18,8 @@ import type { CanvasWheelCaptureMode, CanvasPatternId, EdgePanSpeed, + FileCard, + FileRootReference, FocusActivation, HomeAccentColors, HomeAccentPresetId, @@ -198,6 +200,7 @@ export class SettingsStore { || !("persistMaterials" in source) || !("canvasRegions" in source) || !("stickyNotes" in source) + || !("fileCards" in source) || !("apiProfiles" in source) || source.canvasColor === "palette" || source.settingsVersion !== SETTINGS_VERSION; @@ -412,6 +415,7 @@ function createDefaults(systemLocale: string, platform: string): AppSettings { canvasRegions: [], stickyNotes: [], pluginCanvas: [], + fileCards: [], browserCanvas: null, browserAgentAccess: true, browserShowAgentPresence: true, @@ -532,6 +536,7 @@ export function normalizeSettings( homeGridSize ); const pluginCanvas = normalizePluginCanvas(source.pluginCanvas, fallback.pluginCanvas ?? []); + const fileCards = normalizeFileCards(source.fileCards, fallback.fileCards ?? []); const canvasRegions = normalizeCanvasRegions(source.canvasRegions, fallback.canvasRegions ?? []); const stickyNotes = normalizeStickyNotes(source.stickyNotes, fallback.stickyNotes ?? []); const browserCanvas = normalizeBrowserCanvas(source.browserCanvas, fallback.browserCanvas ?? null); @@ -693,6 +698,7 @@ export function normalizeSettings( canvasRegions, stickyNotes, pluginCanvas, + fileCards, browserCanvas, browserAgentAccess: typeof source.browserAgentAccess === "boolean" ? source.browserAgentAccess @@ -994,6 +1000,86 @@ function normalizePluginCanvas(candidate: unknown, fallback: readonly PluginCanv return instances; } +const MAX_FILE_CARDS = 64; +const MAX_FILE_CARD_EXPANDED_FOLDERS = 256; +const MAX_FILE_CARD_FOLDER_PATH = 4_096; +const MAX_FILE_CARD_RELATIVE_PATH = 1_024; + +function normalizeFileRootReference(candidate: unknown): FileRootReference | null { + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null; + const source = candidate as { rootType?: unknown; sessionId?: unknown; folderPath?: unknown }; + if (source.rootType === "folder") { + // A folder root may retain its chosen canonical path so restore can + // re-register it; it is validated and capped like the card-level path. + const folderPath = typeof source.folderPath === "string" + && source.folderPath.length > 0 + && source.folderPath.length <= MAX_FILE_CARD_FOLDER_PATH + ? source.folderPath + : undefined; + return folderPath ? { rootType: "folder", folderPath } : { rootType: "folder" }; + } + if (source.rootType !== "session") return null; + return typeof source.sessionId === "string" && source.sessionId.length > 0 && source.sessionId.length <= 80 + ? { rootType: "session", sessionId: source.sessionId } + : null; +} + +// Invalid cards are dropped rather than repaired: a card whose root or bounds +// no longer validate must not silently point at an unrelated location. +export function normalizeFileCards( + candidate: unknown, + fallback: readonly FileCard[] = [] +): FileCard[] { + if (!Array.isArray(candidate)) return fallback.map((card) => structuredClone(card)); + + const cards: FileCard[] = []; + const ids = new Set(); + for (const value of candidate.slice(0, MAX_FILE_CARDS)) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + const source = value as Partial; + if (!isInstanceId(source.id) || ids.has(source.id)) continue; + const root = normalizeFileRootReference(source.root); + if (!root) continue; + if (!isFinitePoint(source.position) || !isFiniteSize(source.size)) continue; + const label = typeof source.label === "string" && source.label.trim().length > 0 + ? source.label.trim().slice(0, 80) + : null; + const folderPath = root.rootType === "folder" + && typeof source.folderPath === "string" + && source.folderPath.length > 0 + && source.folderPath.length <= MAX_FILE_CARD_FOLDER_PATH + ? source.folderPath + : null; + const activeFile = typeof source.activeFile === "string" + && source.activeFile.length > 0 + && source.activeFile.length <= MAX_FILE_CARD_RELATIVE_PATH + ? source.activeFile + : null; + const expandedFolders = Array.isArray(source.expandedFolders) + ? source.expandedFolders + .filter((entry): entry is string => typeof entry === "string" + && entry.length > 0 + && entry.length <= MAX_FILE_CARD_RELATIVE_PATH) + .slice(0, MAX_FILE_CARD_EXPANDED_FOLDERS) + : []; + cards.push({ + id: source.id, + root, + label, + folderPath, + activeFile, + expandedFolders, + position: { x: source.position.x, y: source.position.y }, + size: { + width: clamp(source.size.width, 240, 1_600), + height: clamp(source.size.height, 140, 1_100) + } + }); + ids.add(source.id); + } + return cards; +} + function normalizeCanvasRegions( candidate: unknown, fallback: readonly CanvasRegion[] = [] diff --git a/src/main/services/browser/BrowserElectronSmoke.ts b/src/main/services/browser/BrowserElectronSmoke.ts index b114cbf4..48ed324f 100644 --- a/src/main/services/browser/BrowserElectronSmoke.ts +++ b/src/main/services/browser/BrowserElectronSmoke.ts @@ -9,6 +9,7 @@ import { waitForBrowserSmokeWheelReady, runBrowserSmokeCleanup, requireBrowserSmokeScrollBaseline, + freezeEndedBeforeIdle, type BrowserSmokePageState, type BrowserSmokeWheelReadiness } from "./BrowserSmokeReadiness.ts"; @@ -498,7 +499,8 @@ async function assertOwnerWheelFreezesCrossingBrowser(service: BrowserService): globalThis.__canvasttyFreezeEvents.push({ active: event.active, generation: event.generation, - dataUrlLength: event.dataUrl?.length ?? 0 + dataUrlLength: event.dataUrl?.length ?? 0, + observedAt: Date.now() }); }); void 0; @@ -529,26 +531,27 @@ async function assertOwnerWheelFreezesCrossingBrowser(service: BrowserService): await waitUntil(async () => owner.webContents.executeJavaScript( "globalThis.__canvasttyFreezeEvents?.some((event) => event.active && event.dataUrlLength > 0) === true" )); + // The preceding IPC/capture wait can consume the original 250 ms wheel + // interval. Refresh it before testing the surface transition. + const sequenceRefreshedAt = Date.now(); + service.beginRendererWheelSequence({ clientX: 400, clientY: 300 }); service.setViewport({ ...initialViewport, x: 403, surface: "placeholder" }); await new Promise((resolve) => setTimeout(resolve, 80)); service.setViewport({ ...initialViewport, x: 403, surface: "native" }); await new Promise((resolve) => setTimeout(resolve, 80)); - const endedDuringPlaceholder = await owner.webContents.executeJavaScript(` - (() => { - const events = globalThis.__canvasttyFreezeEvents ?? []; - const activeIndex = events.findIndex((event) => event.active && event.dataUrlLength > 0); - return activeIndex >= 0 && events.slice(activeIndex + 1).some((event) => !event.active); - })() - `); + const freezeEvents = await owner.webContents.executeJavaScript( + "globalThis.__canvasttyFreezeEvents ?? []" + ) as Array<{ active: boolean; observedAt: number }>; + const endedDuringPlaceholder = freezeEndedBeforeIdle( + freezeEvents, sequenceRefreshedAt, BROWSER_CANVAS_WHEEL_IDLE_MS + ); if (endedDuringPlaceholder) { throw new Error("Native/placeholder transition ended the active canvas wheel sequence."); } + const idleDeadline = sequenceRefreshedAt + BROWSER_CANVAS_WHEEL_IDLE_MS; await waitUntil(async () => owner.webContents.executeJavaScript(` - (() => { - const events = globalThis.__canvasttyFreezeEvents ?? []; - const activeIndex = events.findIndex((event) => event.active && event.dataUrlLength > 0); - return activeIndex >= 0 && events.slice(activeIndex + 1).some((event) => !event.active); - })() + (globalThis.__canvasttyFreezeEvents ?? []).some((event) => + !event.active && event.observedAt >= ${idleDeadline}) `)); } finally { service.setViewport({ x: 0, y: 0, width: 820, height: 620, surface: "native", canvasScale: 1 }); diff --git a/src/main/services/browser/BrowserSmokeReadiness.ts b/src/main/services/browser/BrowserSmokeReadiness.ts index d4e15242..319c3e14 100644 --- a/src/main/services/browser/BrowserSmokeReadiness.ts +++ b/src/main/services/browser/BrowserSmokeReadiness.ts @@ -22,6 +22,17 @@ export function isBrowserSmokeWheelReady(state: BrowserSmokeWheelReadiness): boo && state.page.focused; } +/** A freeze release after the wheel idle deadline is expected, even if the renderer reports it late. */ +export function freezeEndedBeforeIdle( + events: readonly { active: boolean; observedAt: number }[], + sequenceRefreshedAt: number, + idleMs: number +): boolean { + return events.some((event) => !event.active + && event.observedAt >= sequenceRefreshedAt + && event.observedAt < sequenceRefreshedAt + idleMs); +} + type BrowserSmokeSchedule = (callback: () => void, delayMs: number) => () => void; const scheduleBrowserSmokeCheck: BrowserSmokeSchedule = (callback, delayMs) => { diff --git a/src/preload/index.ts b/src/preload/index.ts index 78821131..8b5da667 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -14,7 +14,9 @@ import type { CanvasTTYApi, CustomTerminalBorderSkinId, CreateSessionRequest, + FileRootReference, MaterialsSnapshot, + PluginBrowserOpenRequest, PluginBrowserOpenResponse, PluginCanvasRequest, @@ -120,6 +122,15 @@ const api: CanvasTTYApi = { media: { read: (path: string) => ipcRenderer.invoke(IPC.mediaRead, path) }, + files: { + listRoots: () => ipcRenderer.invoke(IPC.filesListRoots), + registerRoot: (reference: FileRootReference) => ipcRenderer.invoke(IPC.filesRegisterRoot, reference), + openFolder: () => ipcRenderer.invoke(IPC.filesOpenFolder), + list: (rootId: string, relativePath: string) => ipcRenderer.invoke(IPC.filesList, rootId, relativePath), + read: (rootId: string, relativePath: string) => ipcRenderer.invoke(IPC.filesRead, rootId, relativePath), + search: (rootId: string, query: string) => ipcRenderer.invoke(IPC.filesSearch, rootId, query), + closeRoot: (rootId: string) => ipcRenderer.invoke(IPC.filesCloseRoot, rootId) + }, materials: { snapshot: () => ipcRenderer.invoke(IPC.materialsSnapshot), addFiles: (files: File[], point: Point) => ipcRenderer.invoke( @@ -140,6 +151,7 @@ const api: CanvasTTYApi = { updateRemark: (id: string, patch: unknown) => ipcRenderer.invoke(IPC.materialsUpdateRemark, id, patch), deleteRemark: (id: string) => ipcRenderer.invoke(IPC.materialsDeleteRemark, id), onChanged: (listener: (snapshot: MaterialsSnapshot) => void) => subscribe(IPC.materialsChanged, listener) + }, limits: { get: () => ipcRenderer.invoke(IPC.limitsGet) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index a494320a..6c4922b7 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -9,6 +9,8 @@ import type { BrowserSnapshot, CameraState, CanvasRegion, + FileCard, + FileRootDescriptor, GithubPluginSearchResult, HomeAccentColors, HomeGridSize, @@ -58,6 +60,13 @@ import { UpdateNotice } from "./components/UpdateNotice"; import { resolveAppearanceSettings } from "./features/settings/appearanceSettings"; import { persistSettingsUpdate } from "./features/settings/persistSettings"; import { PluginBrowserOpenQueue } from "./features/plugins/PluginBrowserOpenQueue"; +import { + EMPTY_FILE_CARD_RUNTIME, + type FileCardRuntimeState +} from "./features/files/fileCardRuntime"; +import { fileCardToRootReference, isFileRootUsable } from "./features/files/fileCardRestore"; +import { matchQuickOpen, toggleExpandedFolder } from "./features/files/fileTree"; +import type { FileSessionOption } from "./features/files/FileBrowserCard"; import { GitRiskNotice } from "./features/terminal/GitRiskNotice"; import { WorkspaceCanvas } from "./features/workspace/WorkspaceCanvas"; import { createCameraStore } from "./features/workspace/cameraStore"; @@ -168,6 +177,7 @@ const FALLBACK_SETTINGS: AppSettings = { canvasRegions: [], stickyNotes: [], pluginCanvas: [], + fileCards: [], browserCanvas: null, browserAgentAccess: true, browserShowAgentPresence: true, @@ -200,6 +210,7 @@ const EMPTY_BROWSER_SNAPSHOT: BrowserSnapshot = { const DEFAULT_FOCUS_ZOOM = 0.92; const PLUGIN_CANVAS_FOCUS_ZOOM = 1; +const FILE_CARD_FOCUS_ZOOM = 1; function startHomeMediaRead( path: string, @@ -319,6 +330,14 @@ export function App(): React.JSX.Element { const settingsRef = useRef(settings); settingsRef.current = settings; const pluginBrowserOpenQueueRef = useRef(new PluginBrowserOpenQueue()); + /** Non-persisted Files-card runtime (root handle + loaded listings), keyed by card id. */ + const [fileCardRuntime, setFileCardRuntime] = useState>({}); + const fileCardRuntimeRef = useRef(fileCardRuntime); + fileCardRuntimeRef.current = fileCardRuntime; + /** Monotonic per-card quick-open sequence, so a stale search cannot overwrite a newer one. */ + const fileSearchSequence = useRef>({}); + /** Monotonic per-card read sequence, so a slow earlier read cannot overwrite a newer one. */ + const fileReadSequence = useRef>({}); const [launchProvider, setLaunchProvider] = useState(null); const [launchPosition, setLaunchPosition] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); @@ -350,10 +369,15 @@ export function App(): React.JSX.Element { }); const showToast = useCallback((message: string): void => setToast(message), []); + const fileSessionOptions = useMemo( + () => sessions.map((session) => ({ sessionId: session.id, label: session.title })), + [sessions] + ); const materials = useMaterials(); const materialsRef = useRef(materials.materials); materialsRef.current = materials.materials; + const openUpdates = useCallback((): void => { setSettingsOpen(true); setOpenUpdatesRequest(request => request + 1); @@ -869,13 +893,18 @@ export function App(): React.JSX.Element { const stickyNotes = settingsRef.current.stickyNotes.map((note) => boundsInsideRegion(note, previous) ? { ...note, ...translateBounds(note, delta) } : note); + const fileCards = settingsRef.current.fileCards.map((card) => boundsInsideRegion(card, previous) + ? { ...card, ...translateBounds(card, delta) } + : card); for (const material of materialsRef.current) { if (boundsInsideRegion(material, previous)) materials.setBounds(material.id, translateBounds(material, delta)); } + setSessions(movedSessions); patch.pluginCanvas = pluginCanvas; patch.browserCanvas = browserCanvas; patch.stickyNotes = stickyNotes; + patch.fileCards = fileCards; browserCanvasRef.current = browserCanvas; } } @@ -993,6 +1022,333 @@ export function App(): React.JSX.Element { void saveSettings({ canvasRegions }); }, [saveSettings]); + const updateFileCardRuntime = useCallback(( + id: string, + update: (state: FileCardRuntimeState) => Partial + ): void => { + setFileCardRuntime((current) => { + const base = current[id] ?? EMPTY_FILE_CARD_RUNTIME; + return { ...current, [id]: { ...base, ...update(base) } }; + }); + }, []); + + const writeFileCards = useCallback((fileCards: FileCard[]): void => { + settingsRef.current = { ...settingsRef.current, fileCards }; + setSettings((current) => ({ ...current, fileCards })); + void saveSettings({ fileCards }); + }, [saveSettings]); + + const updateFileCard = useCallback((id: string, patch: Partial): void => { + writeFileCards(settingsRef.current.fileCards.map((card) => ( + card.id === id ? { ...card, ...patch } : card + ))); + }, [writeFileCards]); + + /** Releases a card's currently registered root, if any. Safe to call when none. */ + const releaseFileCardRoot = useCallback((id: string): void => { + const rootId = fileCardRuntimeRef.current[id]?.root?.rootId ?? null; + if (rootId) void window.canvasTTY.files.closeRoot(rootId).catch(() => undefined); + }, []); + + const loadFileRoot = useCallback(async (id: string, descriptor: FileRootDescriptor): Promise => { + updateFileCardRuntime(id, () => ({ + root: descriptor, + rootUnavailable: !descriptor.available, + loading: descriptor.available, + error: null, + entries: [], + childrenByDirectory: {}, + readResult: null, + quickOpenQuery: "", + quickOpenResults: [] + })); + if (!descriptor.available) return; + try { + const entries = await window.canvasTTY.files.list(descriptor.rootId, ""); + updateFileCardRuntime(id, () => ({ entries, loading: false })); + } catch (error) { + updateFileCardRuntime(id, () => ({ loading: false, error: fileErrorMessage(error) })); + } + }, [updateFileCardRuntime]); + + const chooseFileFolder = useCallback((id: string): void => { + void (async () => { + try { + const descriptor = await window.canvasTTY.files.openFolder(); + if (!descriptor) return; + // The card may have closed while the native dialog was open; release the + // freshly registered root instead of leaking it. + if (!settingsRef.current.fileCards.some((card) => card.id === id)) { + void window.canvasTTY.files.closeRoot(descriptor.rootId).catch(() => undefined); + return; + } + releaseFileCardRoot(id); + fileReadSequence.current[id] = (fileReadSequence.current[id] ?? 0) + 1; + updateFileCard(id, { + root: { rootType: "folder" }, + label: descriptor.label, + folderPath: descriptor.folderPath ?? null, + activeFile: null, + expandedFolders: [] + }); + await loadFileRoot(id, descriptor); + } catch (error) { + updateFileCardRuntime(id, () => ({ loading: false, error: fileErrorMessage(error) })); + } + })(); + }, [loadFileRoot, releaseFileCardRoot, updateFileCard, updateFileCardRuntime]); + + const registerFileSession = useCallback((id: string, sessionId: string): void => { + void (async () => { + try { + const descriptor = await window.canvasTTY.files.registerRoot({ rootType: "session", sessionId }); + if (!descriptor) { + updateFileCardRuntime(id, () => ({ rootUnavailable: true, loading: false, error: null })); + return; + } + if (!settingsRef.current.fileCards.some((card) => card.id === id)) { + void window.canvasTTY.files.closeRoot(descriptor.rootId).catch(() => undefined); + return; + } + releaseFileCardRoot(id); + fileReadSequence.current[id] = (fileReadSequence.current[id] ?? 0) + 1; + updateFileCard(id, { + root: { rootType: "session", sessionId }, + label: descriptor.label, + folderPath: null, + activeFile: null, + expandedFolders: [] + }); + await loadFileRoot(id, descriptor); + } catch (error) { + updateFileCardRuntime(id, () => ({ loading: false, error: fileErrorMessage(error) })); + } + })(); + }, [loadFileRoot, releaseFileCardRoot, updateFileCard, updateFileCardRuntime]); + + const openFileDirectory = useCallback((id: string, relativePath: string): void => { + const card = settingsRef.current.fileCards.find((candidate) => candidate.id === id); + const runtime = fileCardRuntimeRef.current[id]; + const root = runtime?.root ?? null; + if (!card || !root || runtime?.rootUnavailable) return; + const wasExpanded = card.expandedFolders.includes(relativePath); + updateFileCard(id, { expandedFolders: toggleExpandedFolder(card.expandedFolders, relativePath) }); + if (wasExpanded || runtime.childrenByDirectory[relativePath] !== undefined) return; + updateFileCardRuntime(id, () => ({ loading: true, error: null })); + void window.canvasTTY.files.list(root.rootId, relativePath).then( + (entries) => updateFileCardRuntime(id, (state) => ({ + loading: false, + childrenByDirectory: { ...state.childrenByDirectory, [relativePath]: entries } + })), + (error: unknown) => updateFileCardRuntime(id, () => ({ + loading: false, + error: fileErrorMessage(error) + })) + ); + }, [updateFileCard, updateFileCardRuntime]); + + const openFile = useCallback((id: string, relativePath: string): void => { + const runtime = fileCardRuntimeRef.current[id]; + const root = runtime?.root ?? null; + if (!root || runtime?.rootUnavailable) return; + updateFileCard(id, { activeFile: relativePath }); + // Clear the binding so a still-in-flight read cannot render the previous file. + updateFileCardRuntime(id, () => ({ readResult: null, loading: true, error: null })); + const sequence = (fileReadSequence.current[id] ?? 0) + 1; + fileReadSequence.current[id] = sequence; + void window.canvasTTY.files.read(root.rootId, relativePath).then( + (result) => { + if (fileReadSequence.current[id] !== sequence) return; + updateFileCardRuntime(id, () => ({ + loading: false, + readResult: { relativePath, result } + })); + }, + (error: unknown) => { + if (fileReadSequence.current[id] !== sequence) return; + updateFileCardRuntime(id, () => ({ + loading: false, + readResult: null, + error: fileErrorMessage(error) + })); + } + ); + }, [updateFileCard, updateFileCardRuntime]); + + const quickOpenFiles = useCallback((id: string, query: string): void => { + updateFileCardRuntime(id, () => ({ quickOpenQuery: query })); + const sequence = (fileSearchSequence.current[id] ?? 0) + 1; + fileSearchSequence.current[id] = sequence; + const runtime = fileCardRuntimeRef.current[id]; + const root = runtime?.root ?? null; + if (query.trim() === "" || !root || runtime?.rootUnavailable) { + updateFileCardRuntime(id, () => ({ quickOpenResults: [], loading: false })); + return; + } + updateFileCardRuntime(id, () => ({ loading: true, quickOpenResults: [], error: null })); + void window.canvasTTY.files.search(root.rootId, query).then( + (result) => { + if (fileSearchSequence.current[id] !== sequence) return; + updateFileCardRuntime(id, () => ({ + loading: false, + quickOpenResults: matchQuickOpen(result.relativePaths, query) + })); + }, + (error: unknown) => { + if (fileSearchSequence.current[id] !== sequence) return; + updateFileCardRuntime(id, () => ({ loading: false, error: fileErrorMessage(error) })); + } + ); + }, [updateFileCardRuntime]); + + /** + * Re-registers one persisted Files card's root on launch. A session root is + * resolved through its session id; a folder root is reopened by its persisted + * canonical path. A null/unavailable registration shows the explicit + * unavailable state and reads nothing; a card that never chose a root stays in + * the root-selection state instead. + */ + const restoreFileCard = useCallback(async (card: FileCard): Promise => { + const reference = fileCardToRootReference(card); + if (!reference) { + updateFileCardRuntime(card.id, () => ({ ...EMPTY_FILE_CARD_RUNTIME })); + return; + } + const stillOpen = (): boolean => settingsRef.current.fileCards.some((candidate) => candidate.id === card.id); + const releaseRoot = (rootId: string): void => { + void window.canvasTTY.files.closeRoot(rootId).catch(() => undefined); + }; + let descriptor: FileRootDescriptor | null = null; + try { + descriptor = await window.canvasTTY.files.registerRoot(reference); + } catch (error) { + if (!stillOpen()) return; + updateFileCardRuntime(card.id, () => ({ + ...EMPTY_FILE_CARD_RUNTIME, + rootUnavailable: true, + error: fileErrorMessage(error) + })); + return; + } + if (descriptor !== null && !stillOpen()) { + releaseRoot(descriptor.rootId); + return; + } + if (!isFileRootUsable(descriptor)) { + updateFileCardRuntime(card.id, () => ({ ...EMPTY_FILE_CARD_RUNTIME, rootUnavailable: true })); + return; + } + const root = descriptor; + updateFileCardRuntime(card.id, () => ({ ...EMPTY_FILE_CARD_RUNTIME, root, loading: true })); + try { + const entries = await window.canvasTTY.files.list(root.rootId, ""); + if (!stillOpen()) { + releaseRoot(root.rootId); + return; + } + updateFileCardRuntime(card.id, () => ({ entries, loading: false })); + } catch (error) { + if (!stillOpen()) return; + updateFileCardRuntime(card.id, () => ({ loading: false, error: fileErrorMessage(error) })); + return; + } + for (const folder of card.expandedFolders) { + try { + const children = await window.canvasTTY.files.list(root.rootId, folder); + if (!stillOpen()) { + releaseRoot(root.rootId); + return; + } + updateFileCardRuntime(card.id, (state) => ({ + childrenByDirectory: { ...state.childrenByDirectory, [folder]: children } + })); + } catch { + // A folder that disappeared stays unloaded; the rest of the tree restores. + } + } + if (card.activeFile) { + const relativePath = card.activeFile; + updateFileCardRuntime(card.id, () => ({ loading: true })); + try { + const result = await window.canvasTTY.files.read(root.rootId, relativePath); + if (!stillOpen()) { + releaseRoot(root.rootId); + return; + } + updateFileCardRuntime(card.id, () => ({ + loading: false, + readResult: { relativePath, result } + })); + } catch (error) { + if (!stillOpen()) return; + updateFileCardRuntime(card.id, () => ({ + loading: false, + readResult: null, + error: fileErrorMessage(error) + })); + } + } + }, [updateFileCardRuntime]); + + const fileCardsRestored = useRef(false); + useEffect(() => { + if (!ready || fileCardsRestored.current) return; + fileCardsRestored.current = true; + for (const card of settingsRef.current.fileCards) void restoreFileCard(card); + }, [ready, restoreFileCard]); + + const changeFileCardBounds = useCallback((id: string, bounds: SessionBounds): void => { + updateFileCard(id, { position: bounds.position, size: bounds.size }); + }, [updateFileCard]); + + const closeFileCard = useCallback((id: string): void => { + const rootId = fileCardRuntimeRef.current[id]?.root?.rootId ?? null; + delete fileSearchSequence.current[id]; + delete fileReadSequence.current[id]; + setFileCardRuntime((current) => { + if (!(id in current)) return current; + const next = { ...current }; + delete next[id]; + return next; + }); + writeFileCards(settingsRef.current.fileCards.filter((card) => card.id !== id)); + if (rootId) void window.canvasTTY.files.closeRoot(rootId).catch(() => undefined); + }, [writeFileCards]); + + const focusFileCard = useCallback((id: string): void => { + const card = settingsRef.current.fileCards.find((candidate) => candidate.id === id); + if (!card) return; + setActiveSessionId(null); + setBrowserSelected(false); + isHomeCamera.current = false; + setCamera(focusCamera(card.position, card.size, FILE_CARD_FOCUS_ZOOM)); + }, []); + + const openFilesCard = useCallback((position?: Point): void => { + const id = crypto.randomUUID(); + const size = { width: 780, height: 520 }; + const index = settingsRef.current.fileCards.length; + const homeSize = homeGridPixelSize(settings.homeGridSize); + const card: FileCard = { + id, + root: { rootType: "folder" }, + label: null, + folderPath: null, + activeFile: null, + expandedFolders: [], + position: position + ? centeredWindowPosition(position, size) + : { x: homeSize.width + 160 + (index % 2) * 820, y: Math.floor(index / 2) * 560 + 20 }, + size + }; + writeFileCards([...settingsRef.current.fileCards, card]); + setFileCardRuntime((current) => ({ ...current, [id]: EMPTY_FILE_CARD_RUNTIME })); + setActiveSessionId(null); + setBrowserSelected(false); + isHomeCamera.current = false; + setCamera(focusCamera(card.position, card.size, FILE_CARD_FOCUS_ZOOM)); + }, [settings.homeGridSize, writeFileCards]); + const disposePluginCanvas = useCallback((id: string): void => { void saveSettings({ pluginCanvas: settingsRef.current.pluginCanvas.filter((instance) => instance.id !== id) }); }, [saveSettings]); @@ -1592,6 +1948,22 @@ export function App(): React.JSX.Element { onStickyNoteBoundsChange={changeStickyNoteBounds} onStickyNoteTextChange={changeStickyNoteText} onDeleteStickyNote={deleteStickyNote} + fileCardState={fileCardRuntime} + fileSessionOptions={fileSessionOptions} + onOpenFiles={openFilesCard} + onFocusFileCard={focusFileCard} + onFileCardBoundsChange={changeFileCardBounds} + onCloseFileCard={closeFileCard} + onChooseFileFolder={chooseFileFolder} + onRegisterFileSession={registerFileSession} + onOpenFileDirectory={openFileDirectory} + onOpenFile={openFile} + onFileQuickOpen={quickOpenFiles} + onOpenLink={(href) => { + void openBrowser(href).catch((error: unknown) => { + showToast(error instanceof Error ? error.message : t(settings.locale, "browserActionFailed")); + }); + }} materials={materials.materials} onAddMaterialFiles={addMaterialFiles} onPickMaterials={pickMaterials} @@ -1697,6 +2069,10 @@ export function App(): React.JSX.Element { ); } +function fileErrorMessage(error: unknown): string { + return error instanceof Error && error.message.length > 0 ? error.message : String(error); +} + function centeredWindowPosition(point: Point, size: { width: number; height: number }): Point { return { x: point.x - size.width / 2, diff --git a/src/renderer/src/features/files/CodeView.tsx b/src/renderer/src/features/files/CodeView.tsx new file mode 100644 index 00000000..c258cb59 --- /dev/null +++ b/src/renderer/src/features/files/CodeView.tsx @@ -0,0 +1,19 @@ +import { highlightCode } from "./codeHighlight.ts"; + +export interface CodeViewProps { + content: string; + language: string | null; +} + +/** + * Renders code with lowlight highlighting for registered languages and falls + * back to plain monospace text for unknown languages. + */ +export function CodeView({ content, language }: CodeViewProps): React.JSX.Element { + const highlighted = highlightCode(content, language); + return ( +
+      {highlighted ?? content}
+    
+ ); +} diff --git a/src/renderer/src/features/files/FileBrowserCard.tsx b/src/renderer/src/features/files/FileBrowserCard.tsx new file mode 100644 index 00000000..4f3cf54f --- /dev/null +++ b/src/renderer/src/features/files/FileBrowserCard.tsx @@ -0,0 +1,604 @@ +import { Suspense, lazy, useEffect, useMemo, useRef, useState } from "react"; +import type { + FileEntry, + FileRootDescriptor, + LocaleId, + Point, + SessionBounds +} from "../../../../shared/contracts"; +import { UiIcon } from "../../components/UiIcon"; +import { t } from "../../lib/i18n"; +import { filesLayerId } from "../workspace/canvasSelectionGesture"; +import { filesCanvasWidgetId } from "../workspace/canvasWidgetFocus"; +import { snapMove, snapResize, type ResizeDirection } from "../workspace/snap"; +import { buildFileTree, flattenFileTree, type FileTreeNode } from "./fileTree"; +import { resolveActiveFileRead, type FileReadBinding } from "./fileViewer"; + +// The rich renderers pull in react-markdown / rehype / lowlight. Lazy-load them +// so their weight stays out of the app's static import graph (see +// tests/renderer-bundle-splitting.test.mjs) and is only fetched when a rich file +// is actually opened. +const MarkdownView = lazy(() => import("./MarkdownView").then((module) => ({ default: module.MarkdownView }))); +const CodeView = lazy(() => import("./CodeView").then((module) => ({ default: module.CodeView }))); + +/** A selectable terminal session working directory offered as a Files root. */ +export interface FileSessionOption { + sessionId: string; + label: string; +} + +/** + * The Files card is strictly presentational: every piece of data and every + * action arrives through props. IPC, tree loading, root registration, search, + * and persistence are owned by the parent (WorkspaceCanvas / App). + */ +export interface FileBrowserCardProps { + /** Stable identity; the card renders `data-canvas-layer-id="files:"`. */ + cardId: string; + bounds: SessionBounds; + /** Opaque root handle; null means the card still needs a root selected. */ + root: FileRootDescriptor | null; + /** Sessions offered as session roots while no root is selected. */ + sessionOptions?: readonly FileSessionOption[]; + /** Root directory listing. */ + entries: readonly FileEntry[]; + /** Loaded child listings keyed by directory relative path. */ + childrenByDirectory?: Readonly>; + /** Relative paths of expanded tree folders. */ + expandedFolders: readonly string[]; + /** Relative path of the file shown in the viewer, or null. */ + activeFile: string | null; + /** + * Read result together with the relative path it was read for. Content is + * shown only when `readResult.relativePath === activeFile`; any mismatch (a + * read still in flight for a newly selected file) renders loading instead. + */ + readResult: FileReadBinding | null; + /** True while a listing, read, or search is in flight. */ + loading: boolean; + /** Error text for the last failed operation, or null. */ + error: string | null; + /** True when a restored root can no longer be read. */ + rootUnavailable: boolean; + quickOpenQuery: string; + quickOpenResults: readonly string[]; + /** Increments when the parent requests focus for this card's quick-open input. */ + quickOpenFocusRequest?: number; + locale: LocaleId; + zoom: number; + stackIndex: number; + snapEnabled: boolean; + getSnapTargets(): readonly SessionBounds[]; + onChooseFolder(): void; + onRegisterSession(sessionId: string): void; + onOpenDirectory(relativePath: string): void; + onOpenFile(relativePath: string): void; + onQuickOpen(query: string): void; + onChangeBounds(bounds: SessionBounds): void; + onClose(): void; + /** Opens a link from a rendered markdown file. */ + onOpenLink?: (href: string) => void; + /** True while this card is part of the marquee selection. */ + groupSelected?: boolean; +} + +interface DragState { + pointerId: number; + startClient: Point; + startBounds: SessionBounds; + snapTargets: readonly SessionBounds[]; +} + +interface ResizeState extends DragState { + direction: ResizeDirection; +} + +const RESIZE_DIRECTIONS: ResizeDirection[] = ["n", "ne", "e", "se", "s", "sw", "w", "nw"]; +const EMPTY_CHILDREN: Readonly> = {}; + +export function FileBrowserCard({ + cardId, + bounds, + root, + sessionOptions = [], + entries, + childrenByDirectory = EMPTY_CHILDREN, + expandedFolders, + activeFile, + readResult, + loading, + error, + rootUnavailable, + quickOpenQuery, + quickOpenResults, + quickOpenFocusRequest = 0, + locale, + zoom, + stackIndex, + snapEnabled, + getSnapTargets, + onChooseFolder, + onRegisterSession, + onOpenDirectory, + onOpenFile, + onQuickOpen, + onChangeBounds, + onClose, + onOpenLink = () => {}, + groupSelected = false +}: FileBrowserCardProps): React.JSX.Element { + const dragState = useRef(null); + const resizeState = useRef(null); + const quickOpenInput = useRef(null); + const initialBounds = constrainFileBrowserResize({ position: bounds.position, size: bounds.size }, "se"); + const [position, setPosition] = useState(initialBounds.position); + const [size, setSize] = useState(initialBounds.size); + const liveBounds = useRef(initialBounds); + const summaryMode = zoom < 0.5; + const summaryScale = summaryMode ? Math.min(2.5, Math.max(1, 0.5 / zoom)) : 1; + + const tree: FileTreeNode[] = useMemo( + () => buildFileTree(entries, childrenByDirectory), + [childrenByDirectory, entries] + ); + const rows = useMemo( + () => flattenFileTree(tree, expandedFolders), + [expandedFolders, tree] + ); + const viewer = resolveActiveFileRead(activeFile, readResult); + const activeName = activeFile ? basename(activeFile) : null; + const treeTitle = root?.label ?? t(locale, "files"); + const headerDetail = activeName ?? (root ? t(locale, "filesNoFile") : t(locale, "filesSelectRoot")); + const trimmedQuery = quickOpenQuery.trim(); + const unsupportedText = viewer?.kind === "unsupported" + ? unsupportedReasonText(locale, viewer.reason) + : ""; + const textContent = viewer?.kind === "text" ? viewer.content : null; + const textRenderKind = viewer?.kind === "text" ? viewer.renderKind : null; + const textLanguage = viewer?.kind === "text" ? viewer.language : null; + const textRichDisabled = viewer?.kind === "text" ? viewer.richDisabled : false; + const richView = useMemo(() => { + if (textContent === null || textRichDisabled) return null; + if (textRenderKind === "markdown") return ; + if (textRenderKind === "code") return ; + return null; + }, [textContent, textRenderKind, textLanguage, textRichDisabled, onOpenLink]); + + useEffect(() => { + const next = constrainFileBrowserResize({ position: bounds.position, size: bounds.size }, "se"); + liveBounds.current = next; + setPosition(next.position); + setSize(next.size); + }, [bounds]); + + useEffect(() => { + if (quickOpenFocusRequest > 0) quickOpenInput.current?.focus(); + }, [quickOpenFocusRequest]); + + const applyBounds = (next: SessionBounds): void => { + liveBounds.current = next; + setPosition(next.position); + setSize(next.size); + }; + + const startDrag = (event: React.PointerEvent): void => { + if ((event.target as HTMLElement).closest("button, input, [data-file-action]")) return; + event.currentTarget.setPointerCapture(event.pointerId); + dragState.current = { + pointerId: event.pointerId, + startClient: { x: event.clientX, y: event.clientY }, + startBounds: liveBounds.current, + snapTargets: snapEnabled ? getSnapTargets() : [] + }; + }; + + const drag = (event: React.PointerEvent): void => { + const state = dragState.current; + if (!state || state.pointerId !== event.pointerId) return; + // A buttonless move is a hover, not a drag. + if (event.buttons === 0) return; + const rawPosition = { + x: state.startBounds.position.x + (event.clientX - state.startClient.x) / zoom, + y: state.startBounds.position.y + (event.clientY - state.startClient.y) / zoom + }; + applyBounds({ + position: snapEnabled ? snapMove(rawPosition, state.startBounds.size, state.snapTargets) : rawPosition, + size: state.startBounds.size + }); + }; + + const endDrag = (event: React.PointerEvent): void => { + if (dragState.current?.pointerId !== event.pointerId) return; + dragState.current = null; + onChangeBounds(liveBounds.current); + }; + + // A group drag takes pointer capture without a pointerup; drop local state so + // a later hover cannot act on it. + const cancelDrag = (): void => { + dragState.current = null; + }; + + const cancelResize = (): void => { + resizeState.current = null; + }; + + const startResize = (event: React.PointerEvent, direction: ResizeDirection): void => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.setPointerCapture(event.pointerId); + resizeState.current = { + pointerId: event.pointerId, + direction, + startClient: { x: event.clientX, y: event.clientY }, + startBounds: liveBounds.current, + snapTargets: snapEnabled ? getSnapTargets() : [] + }; + }; + + const resize = (event: React.PointerEvent): void => { + const state = resizeState.current; + if (!state || state.pointerId !== event.pointerId) return; + // A buttonless move is a hover, not a resize. + if (event.buttons === 0) return; + event.preventDefault(); + event.stopPropagation(); + const deltaX = (event.clientX - state.startClient.x) / zoom; + const deltaY = (event.clientY - state.startClient.y) / zoom; + const raw: SessionBounds = { + position: { + x: state.startBounds.position.x + (state.direction.includes("w") ? deltaX : 0), + y: state.startBounds.position.y + (state.direction.includes("n") ? deltaY : 0) + }, + size: { + width: state.startBounds.size.width + + (state.direction.includes("e") ? deltaX : 0) + - (state.direction.includes("w") ? deltaX : 0), + height: state.startBounds.size.height + + (state.direction.includes("s") ? deltaY : 0) + - (state.direction.includes("n") ? deltaY : 0) + } + }; + const constrained = constrainFileBrowserResize(raw, state.direction); + applyBounds(snapEnabled ? snapResize(constrained, state.direction, state.snapTargets) : constrained); + }; + + const endResize = (event: React.PointerEvent): void => { + if (resizeState.current?.pointerId !== event.pointerId) return; + event.preventDefault(); + event.stopPropagation(); + resizeState.current = null; + onChangeBounds(liveBounds.current); + }; + + return ( +
+
+ + + + {treeTitle} + {headerDetail} + + +
+ + +
+
+ +
+ + +
+ {renderViewer()} +
+
+ + + + {RESIZE_DIRECTIONS.map((direction) => ( +
+ ); + + function renderViewer(): React.JSX.Element { + if (rootUnavailable) { + return ( + + ); + } + if (root === null) { + return ( +
+ + {t(locale, "filesSelectRoot")} +

{t(locale, "filesSelectRootHint")}

+ + {sessionOptions.length > 0 && ( + <> + {t(locale, "filesSessionRoot")} +
+ {sessionOptions.map((option) => ( + + ))} +
+ + )} +
+ ); + } + if (error !== null) { + return ; + } + if (activeFile === null) { + return ; + } + if (viewer === null) { + // The active file has no read bound to it yet: show loading instead of the + // previous file's content. + return ; + } + switch (viewer.kind) { + case "text": + return ( + <> + {viewer.truncated && ( +
+ + {t(locale, "filesTruncated")} + {viewer.sizeLabel} +
+ )} + {viewer.richDisabled ? ( + <> +
+ + {t(locale, "filesRichDisabled")} + {viewer.sizeLabel} +
+
{viewer.content}
+ + ) : richView !== null ? ( +
+ }> + {richView} + +
+ ) : ( +
{viewer.content}
+ )} + + ); + case "image": + return ( +
+ {activeName + {viewer.mediaType} · {viewer.sizeLabel} +
+ ); + case "unsupported": + return ( + + ); + case "too-large": + return ( + + ); + } + } +} + +function ViewerState({ icon, title, detail }: { + icon: React.ComponentProps["name"]; + title: string; + detail?: string; +}): React.JSX.Element { + return ( +
+ + {title} + {detail && {detail}} +
+ ); +} + +function TreeNotice({ icon, title, detail }: { + icon: React.ComponentProps["name"]; + title: string; + detail?: string; +}): React.JSX.Element { + return ( +
+ + {title} + {detail && {detail}} +
+ ); +} + +function unsupportedReasonText(locale: LocaleId, reason: "binary" | "not-permitted" | "unavailable"): string { + if (reason === "binary") return t(locale, "filesUnsupportedBinary"); + if (reason === "not-permitted") return t(locale, "filesUnsupportedNotPermitted"); + return t(locale, "filesUnsupportedUnavailable"); +} + +function basename(relativePath: string): string { + const index = relativePath.lastIndexOf("/"); + return index === -1 ? relativePath : relativePath.slice(index + 1); +} + +function constrainFileBrowserResize(bounds: SessionBounds, direction: ResizeDirection): SessionBounds { + const right = bounds.position.x + bounds.size.width; + const bottom = bounds.position.y + bounds.size.height; + const width = clamp(bounds.size.width, 460, 1_600); + const height = clamp(bounds.size.height, 340, 1_200); + return { + position: { + x: direction.includes("w") ? right - width : bounds.position.x, + y: direction.includes("n") ? bottom - height : bounds.position.y + }, + size: { width, height } + }; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} diff --git a/src/renderer/src/features/files/MarkdownView.tsx b/src/renderer/src/features/files/MarkdownView.tsx new file mode 100644 index 00000000..45b6416c --- /dev/null +++ b/src/renderer/src/features/files/MarkdownView.tsx @@ -0,0 +1,13 @@ +import type { JSX } from "react"; +import { createMarkdownElement } from "./markdownPipeline.ts"; + +export interface MarkdownViewProps { + content: string; + onOpenLink?: (href: string) => void; +} + +export function MarkdownView({ content, onOpenLink }: MarkdownViewProps): JSX.Element { + return ( +
{createMarkdownElement(content, onOpenLink)}
+ ); +} diff --git a/src/renderer/src/features/files/codeHighlight.ts b/src/renderer/src/features/files/codeHighlight.ts new file mode 100644 index 00000000..aa2ae298 --- /dev/null +++ b/src/renderer/src/features/files/codeHighlight.ts @@ -0,0 +1,41 @@ +import { toJsxRuntime } from "hast-util-to-jsx-runtime"; +import { common, createLowlight } from "lowlight"; +import type { ReactElement } from "react"; +import { Fragment, jsx, jsxs } from "react/jsx-runtime"; +import { COMMON_LANGUAGES } from "./fileLanguage.ts"; + +/** + * Deterministic code highlighting for the file viewer. This module carries no + * JSX so it can be imported directly by node:test; the matching React wrapper + * lives in `CodeView.tsx`. + * + * The lowlight instance registers the curated `common` set (not `all`) because + * `COMMON_LANGUAGES` in `fileLanguage.ts` is constrained to that set. + */ +const lowlight = createLowlight(common); + +// Invariant relied on by detection: every id `detectFileLanguage` can return is +// registered here, so a detected language is never silently unhighlighted. +const missingLanguages = COMMON_LANGUAGES.filter((language) => !lowlight.registered(language)); +if (missingLanguages.length > 0) { + throw new Error(`lowlight is missing registered languages: ${missingLanguages.join(", ")}`); +} + +/** True when `language` names a non-empty language registered by lowlight. */ +export function isRegisteredLanguage(language: string | null | undefined): boolean { + if (typeof language !== "string" || language.length === 0) return false; + return lowlight.registered(language); +} + +/** + * Highlights `content` as `language`, returning a React element, or null when + * the language is missing or unregistered (callers fall back to plain text). + */ +export function highlightCode( + content: string, + language: string | null | undefined +): ReactElement | null { + if (!isRegisteredLanguage(language)) return null; + const tree = lowlight.highlight(language as string, content); + return toJsxRuntime(tree, { Fragment, jsx, jsxs }); +} diff --git a/src/renderer/src/features/files/fileCardRestore.ts b/src/renderer/src/features/files/fileCardRestore.ts new file mode 100644 index 00000000..50a1bc3f --- /dev/null +++ b/src/renderer/src/features/files/fileCardRestore.ts @@ -0,0 +1,34 @@ +import type { FileCard, FileRootDescriptor, FileRootReference } from "../../../../shared/contracts"; + +/** + * Pure restore helpers for persisted Files cards. These map the settings shape to + * the root reference main needs to re-register and decide whether a restored + * root is usable, without touching IPC or the filesystem. + */ + +/** + * Rebuilds the root reference to register on launch. Session cards use their + * session id; folder cards use the persisted absolute `folderPath` (falling back + * to a path embedded in the root reference). Returns null when a card has no + * restorable root — e.g. a folder card whose folder was never chosen. + */ +export function fileCardToRootReference( + card: Pick +): FileRootReference | null { + if (card.root.rootType === "session") { + return card.root.sessionId ? { rootType: "session", sessionId: card.root.sessionId } : null; + } + const folderPath = card.folderPath ?? card.root.folderPath ?? null; + return folderPath ? { rootType: "folder", folderPath } : null; +} + +/** + * A restored root is usable only when registration returned a descriptor that is + * explicitly available. A null (missing/unreadable root) or `available: false` + * descriptor must render the card as unavailable instead of reading anything. + */ +export function isFileRootUsable( + descriptor: FileRootDescriptor | null +): descriptor is FileRootDescriptor { + return descriptor !== null && descriptor.available !== false; +} diff --git a/src/renderer/src/features/files/fileCardRuntime.ts b/src/renderer/src/features/files/fileCardRuntime.ts new file mode 100644 index 00000000..82e56cfa --- /dev/null +++ b/src/renderer/src/features/files/fileCardRuntime.ts @@ -0,0 +1,40 @@ +import type { FileEntry, FileRootDescriptor } from "../../../../shared/contracts"; +import type { FileReadBinding } from "./fileViewer"; + +/** + * Renderer-only, non-persisted state for one Files card: the opaque root handle + * and the directory/read/search data loaded for it. Persisted identity, bounds, + * root reference, active file, and expanded folders live in + * `AppSettings.fileCards`; this state is (re)derived at runtime. + */ +export interface FileCardRuntimeState { + root: FileRootDescriptor | null; + entries: readonly FileEntry[]; + childrenByDirectory: Readonly>; + readResult: FileReadBinding | null; + loading: boolean; + error: string | null; + rootUnavailable: boolean; + quickOpenQuery: string; + quickOpenResults: readonly string[]; +} + +export const EMPTY_FILE_CARD_RUNTIME: FileCardRuntimeState = { + root: null, + entries: [], + childrenByDirectory: {}, + readResult: null, + loading: false, + error: null, + rootUnavailable: false, + quickOpenQuery: "", + quickOpenResults: [] +}; + +/** Runtime state for a card, falling back to the empty state before it loads. */ +export function fileCardRuntimeFor( + state: Readonly>, + id: string +): FileCardRuntimeState { + return state[id] ?? EMPTY_FILE_CARD_RUNTIME; +} diff --git a/src/renderer/src/features/files/fileLanguage.ts b/src/renderer/src/features/files/fileLanguage.ts new file mode 100644 index 00000000..ae90ec73 --- /dev/null +++ b/src/renderer/src/features/files/fileLanguage.ts @@ -0,0 +1,84 @@ +/** + * Pure path -> language classification for the file viewer. This module carries + * no React and performs no I/O. It maps file extensions to highlight.js + * language ids and flags Markdown so the card can pick a rich renderer. + */ + +export const MARKDOWN_EXTENSIONS: readonly string[] = ["md", "markdown", "mdown", "mkd"]; + +const EXTENSION_LANGUAGE_MAP: Readonly> = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + mjs: "javascript", + cjs: "javascript", + py: "python", + json: "json", + jsonc: "json", + css: "css", + scss: "scss", + html: "xml", + htm: "xml", + xml: "xml", + svg: "xml", + yaml: "yaml", + yml: "yaml", + sh: "bash", + bash: "bash", + zsh: "bash", + go: "go", + rs: "rust", + java: "java", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + cxx: "cpp", + hpp: "cpp", + cs: "csharp", + rb: "ruby", + php: "php", + sql: "sql", + toml: "ini", + kt: "kotlin", + swift: "swift", + lua: "lua", + r: "r", + pl: "perl", + diff: "diff", + patch: "diff", + makefile: "makefile" +}; + +function extensionOf(relativePath: string): string | null { + const basename = relativePath.slice(relativePath.lastIndexOf("/") + 1); + const dotIndex = basename.lastIndexOf("."); + if (dotIndex < 0 || dotIndex === basename.length - 1) return null; + return basename.slice(dotIndex + 1).toLowerCase(); +} + +/** True when the lowercased extension is one of the Markdown extensions. */ +export function isMarkdownPath(relativePath: string): boolean { + const extension = extensionOf(relativePath); + return extension !== null && MARKDOWN_EXTENSIONS.includes(extension); +} + +/** + * Maps a path to a highlight.js language id. Returns null for unknown + * extensions and for Markdown extensions (which take the rich Markdown path). + */ +export function detectFileLanguage(relativePath: string): string | null { + const extension = extensionOf(relativePath); + if (extension === null) return null; + if (MARKDOWN_EXTENSIONS.includes(extension)) return null; + return EXTENSION_LANGUAGE_MAP[extension] ?? null; +} + +/** + * Sorted, de-duplicated registration list for lowlight: every language id + * detection can produce, never Markdown. + */ +export const COMMON_LANGUAGES: readonly string[] = Array.from( + new Set(Object.values(EXTENSION_LANGUAGE_MAP)) +).sort(); diff --git a/src/renderer/src/features/files/fileLinks.ts b/src/renderer/src/features/files/fileLinks.ts new file mode 100644 index 00000000..b6a2d513 --- /dev/null +++ b/src/renderer/src/features/files/fileLinks.ts @@ -0,0 +1,31 @@ +/** + * Pure link and image-source classifiers for Markdown rendering. This module + * carries no React and performs no I/O. It decides which links may be opened + * externally and which image sources are safe to render inline (embedded data + * URLs only, never remote or relative sources). + */ + +/** + * True only for absolute `http:`/`https:` URLs. Relative paths, fragment-only + * anchors, `mailto:`, `javascript:`, `data:`, `ftp:` and unparseable values are + * rejected. + */ +export function isOpenableLink(href: string | null | undefined): boolean { + if (typeof href !== "string" || href.length === 0) return false; + let parsed: URL; + try { + parsed = new URL(href); + } catch { + return false; + } + return parsed.protocol === "http:" || parsed.protocol === "https:"; +} + +/** + * True only for embedded `data:image/...` sources, so remote and relative + * images are never rendered with a network request. + */ +export function isEmbeddedImageSource(src: string | null | undefined): boolean { + if (typeof src !== "string" || src.length === 0) return false; + return /^data:image\//i.test(src); +} diff --git a/src/renderer/src/features/files/fileTree.ts b/src/renderer/src/features/files/fileTree.ts new file mode 100644 index 00000000..d37b08c9 --- /dev/null +++ b/src/renderer/src/features/files/fileTree.ts @@ -0,0 +1,146 @@ +import type { FileEntry } from "../../../../shared/contracts"; + +/** + * Pure, renderer-side tree helpers for the Files card. These functions never + * touch the filesystem or IPC; the main process returns ordered `FileEntry` + * lists and the card composes them into a display tree. + */ + +export interface FileTreeNode { + entry: FileEntry; + /** + * Ordered child nodes, or null when this directory's contents have not been + * loaded yet. A loaded but empty directory is an empty array. + */ + children: FileTreeNode[] | null; +} + +export interface FileTreeRow { + entry: FileEntry; + /** Zero-based nesting depth; root entries are depth 0. */ + depth: number; + isDirectory: boolean; + isExpanded: boolean; + /** True once a directory's children have been loaded into the tree. */ + hasLoadedChildren: boolean; +} + +/** Case-insensitive name compare with a stable case-sensitive tiebreak. */ +function compareNames(left: string, right: string): number { + const leftLower = left.toLowerCase(); + const rightLower = right.toLowerCase(); + if (leftLower < rightLower) return -1; + if (leftLower > rightLower) return 1; + if (left < right) return -1; + if (left > right) return 1; + return 0; +} + +/** Directories first, then files, each group sorted by name (case-insensitive). */ +export function orderFileEntries(entries: readonly FileEntry[]): FileEntry[] { + return [...entries].sort((left, right) => { + if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1; + return compareNames(left.name, right.name); + }); +} + +/** + * Builds the visible tree from the root listing plus a map of already-loaded + * directory listings keyed by directory relative path. Directories without a + * loaded listing keep `children: null` so the UI can request expansion. + */ +export function buildFileTree( + rootEntries: readonly FileEntry[], + childrenByDirectory: Readonly> = {} +): FileTreeNode[] { + const build = (entries: readonly FileEntry[]): FileTreeNode[] => + orderFileEntries(entries).map((entry) => { + if (entry.kind !== "directory") { + return { entry, children: null } satisfies FileTreeNode; + } + const loaded = childrenByDirectory[entry.relativePath]; + return { + entry, + children: loaded === undefined ? null : build(loaded) + } satisfies FileTreeNode; + }); + return build(rootEntries); +} + +/** Adds a folder path to the expanded set, or removes it when present. */ +export function toggleExpandedFolder( + expandedFolders: readonly string[], + relativePath: string +): string[] { + if (expandedFolders.includes(relativePath)) { + return expandedFolders.filter((path) => path !== relativePath); + } + return [...expandedFolders, relativePath]; +} + +/** + * Depth-first flattening of the tree into the rows a flat list can render. + * Children are only walked for directories present in `expandedFolders` whose + * listing has actually been loaded. + */ +export function flattenFileTree( + tree: readonly FileTreeNode[], + expandedFolders: readonly string[] +): FileTreeRow[] { + const expanded = new Set(expandedFolders); + const rows: FileTreeRow[] = []; + const walk = (nodes: readonly FileTreeNode[], depth: number): void => { + for (const node of nodes) { + const isDirectory = node.entry.kind === "directory"; + const isExpanded = isDirectory && expanded.has(node.entry.relativePath); + rows.push({ + entry: node.entry, + depth, + isDirectory, + isExpanded, + hasLoadedChildren: node.children !== null + }); + if (isDirectory && isExpanded && node.children !== null) { + walk(node.children, depth + 1); + } + } + }; + walk(tree, 0); + return rows; +} + +function basename(relativePath: string): string { + const index = relativePath.lastIndexOf("/"); + return index === -1 ? relativePath : relativePath.slice(index + 1); +} + +/** + * Ranks file paths for quick open by case-insensitive substring match: + * 4 = filename equals the query, 3 = filename starts with it, 2 = filename + * contains it, 1 = only the directory path contains it. Ties shorten first, + * then sort case-insensitively by full path. + */ +export function matchQuickOpen( + relativePaths: readonly string[], + query: string +): string[] { + const needle = query.trim().toLowerCase(); + if (needle === "") return []; + const matches: Array<{ relativePath: string; score: number }> = []; + for (const relativePath of relativePaths) { + if (!relativePath.toLowerCase().includes(needle)) continue; + const name = basename(relativePath).toLowerCase(); + let score = 1; + if (name === needle) score = 4; + else if (name.startsWith(needle)) score = 3; + else if (name.includes(needle)) score = 2; + matches.push({ relativePath, score }); + } + return matches + .sort((left, right) => + right.score - left.score + || left.relativePath.length - right.relativePath.length + || compareNames(left.relativePath, right.relativePath) + ) + .map((match) => match.relativePath); +} diff --git a/src/renderer/src/features/files/fileViewer.ts b/src/renderer/src/features/files/fileViewer.ts new file mode 100644 index 00000000..ead9669a --- /dev/null +++ b/src/renderer/src/features/files/fileViewer.ts @@ -0,0 +1,169 @@ +import type { FileReadResult, FileReadUnsupportedReason } from "../../../../shared/contracts"; +import { detectFileLanguage, isMarkdownPath } from "./fileLanguage.ts"; + +/** + * Pure classification of a `FileReadResult` into a render model. This module + * carries no React and performs no I/O; the card decides how to paint the + * returned model. + */ + +/** Content above this byte size falls back to plain text without rich rendering. */ +export const MAX_HIGHLIGHT_BYTES = 250 * 1024; + +export type FileRenderKind = + | "markdown" + | "code" + | "plain" + | "image" + | "unsupported" + | "too-large"; + +export interface FileTextViewerModel { + kind: "text"; + renderKind: FileRenderKind; + content: string; + truncated: boolean; + size: number; + sizeLabel: string; + language: string | null; + highlightable: boolean; + richDisabled: boolean; +} + +export interface FileImageViewerModel { + kind: "image"; + renderKind: FileRenderKind; + mediaType: string; + dataUrl: string; + size: number; + sizeLabel: string; +} + +export interface FileUnsupportedViewerModel { + kind: "unsupported"; + renderKind: FileRenderKind; + reason: FileReadUnsupportedReason; + reasonLabel: string; +} + +export interface FileTooLargeViewerModel { + kind: "too-large"; + renderKind: FileRenderKind; + reason: "too-large"; + size: number; + sizeLabel: string; +} + +export type FileViewerModel = + | FileTextViewerModel + | FileImageViewerModel + | FileUnsupportedViewerModel + | FileTooLargeViewerModel; + +/** A read result bound to the relative path it was read for. */ +export interface FileReadBinding { + relativePath: string; + result: FileReadResult; +} + +const UNSUPPORTED_REASON_LABELS: Record = { + binary: "Binary file", + "not-permitted": "File type not permitted", + unavailable: "File unavailable" +}; + +/** Human-readable byte size; powers of 1024 with one decimal above 1 KB. */ +export function formatFileSize(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; + if (bytes < 1024) return `${Math.round(bytes)} B`; + const units = ["KB", "MB", "GB", "TB"]; + let value = bytes / 1024; + let unitIndex = 0; + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + const rounded = value >= 100 ? Math.round(value) : Math.round(value * 10) / 10; + return `${rounded} ${units[unitIndex]}`; +} + +/** + * Resolves the render model for the active file. A read belongs only to the + * path it was made for, so a mismatched binding returns null and the card shows + * loading instead of the previous file's content. + */ +export function resolveActiveFileRead( + activeFile: string | null, + readResult: FileReadBinding | null +): FileViewerModel | null { + if (activeFile === null || readResult === null) return null; + if (readResult.relativePath !== activeFile) return null; + return classifyFileRead(readResult.result, readResult.relativePath); +} + +export function classifyFileRead(result: FileReadResult, relativePath = ""): FileViewerModel { + switch (result.kind) { + case "text": { + const sizeLabel = formatFileSize(result.size); + const base = { + kind: "text" as const, + content: result.content, + truncated: result.truncated, + size: result.size, + sizeLabel + }; + if (isMarkdownPath(relativePath)) { + const richDisabled = result.size > MAX_HIGHLIGHT_BYTES; + return { + ...base, + renderKind: richDisabled ? "plain" : "markdown", + language: null, + highlightable: result.size <= MAX_HIGHLIGHT_BYTES, + richDisabled + }; + } + const language = detectFileLanguage(relativePath); + if (language !== null) { + const highlightable = result.size <= MAX_HIGHLIGHT_BYTES; + return { + ...base, + renderKind: highlightable ? "code" : "plain", + language, + highlightable, + richDisabled: result.size > MAX_HIGHLIGHT_BYTES + }; + } + return { + ...base, + renderKind: "plain", + language: null, + highlightable: true, + richDisabled: false + }; + } + case "image": + return { + kind: "image", + renderKind: "image", + mediaType: result.mediaType, + dataUrl: result.dataUrl, + size: result.size, + sizeLabel: formatFileSize(result.size) + }; + case "unsupported": + return { + kind: "unsupported", + renderKind: "unsupported", + reason: result.reason, + reasonLabel: UNSUPPORTED_REASON_LABELS[result.reason] + }; + case "too-large": + return { + kind: "too-large", + renderKind: "too-large", + reason: "too-large", + size: result.size, + sizeLabel: formatFileSize(result.size) + }; + } +} diff --git a/src/renderer/src/features/files/markdownPipeline.ts b/src/renderer/src/features/files/markdownPipeline.ts new file mode 100644 index 00000000..4d790fc0 --- /dev/null +++ b/src/renderer/src/features/files/markdownPipeline.ts @@ -0,0 +1,93 @@ +import { createElement } from "react"; +import type { MouseEvent, ReactElement } from "react"; +import Markdown, { defaultUrlTransform } from "react-markdown"; +import type { Components, UrlTransform } from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import type { Options as SanitizeSchema } from "rehype-sanitize"; +import rehypeHighlight from "rehype-highlight"; +import { isEmbeddedImageSource, isOpenableLink } from "./fileLinks.ts"; + +/** + * Sanitization schema for rendered Markdown. Extends the GitHub-style default + * schema only to preserve syntax-highlight class names produced by + * `rehype-highlight` and to let embedded `data:image/...` sources reach the + * image component. Script stripping and dangerous protocols (`javascript:`, + * etc.) are left untouched. + */ +export const markdownSanitizeSchema: SanitizeSchema = { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + span: [...(defaultSchema.attributes?.span ?? []), ["className", "hljs", /^hljs-/, /^language-/]], + code: [...(defaultSchema.attributes?.code ?? []), ["className", "hljs", /^hljs-/, /^language-/]] + }, + protocols: { + ...defaultSchema.protocols, + src: [...(defaultSchema.protocols?.src ?? []), "data"] + } +}; + +/** + * Keeps embedded data-image sources so the `img` component can accept them, + * while delegating every other URL to react-markdown's safe default (which + * drops `javascript:` and other dangerous protocols). + */ +const markdownUrlTransform: UrlTransform = (value) => { + if (isEmbeddedImageSource(value)) return value; + return defaultUrlTransform(value); +}; + +/** + * Builds a React element that renders sanitized Markdown. Raw HTML is never + * enabled (no `rehype-raw`); links are routed through `onOpenLink` only when + * they are absolute http(s) URLs, and images render only when they are embedded + * data URLs. + */ +export function createMarkdownElement( + content: string, + onOpenLink?: (href: string) => void +): ReactElement { + const components: Components = { + a({ href, children }) { + if (isOpenableLink(href)) { + const target = href as string; + return createElement( + "a", + { + href: target, + target: "_blank", + rel: "noreferrer noopener", + onClick: (event: MouseEvent) => { + event.preventDefault(); + onOpenLink?.(target); + } + }, + children + ); + } + return createElement("span", { className: "file-browser-card__markdown-inert" }, children); + }, + img({ src, alt }) { + if (isEmbeddedImageSource(src)) { + return createElement("img", { src, alt }); + } + return createElement( + "span", + { className: "file-browser-card__markdown-image-placeholder" }, + alt || "image" + ); + } + }; + + return createElement( + Markdown, + { + remarkPlugins: [remarkGfm], + rehypePlugins: [rehypeHighlight, [rehypeSanitize, markdownSanitizeSchema]], + urlTransform: markdownUrlTransform, + components + }, + content + ); +} diff --git a/src/renderer/src/features/workspace/CanvasCommandPalette.tsx b/src/renderer/src/features/workspace/CanvasCommandPalette.tsx index b87783c0..1b16b0d1 100644 --- a/src/renderer/src/features/workspace/CanvasCommandPalette.tsx +++ b/src/renderer/src/features/workspace/CanvasCommandPalette.tsx @@ -29,6 +29,7 @@ interface CanvasCommandPaletteProps { onPasteFiles(): void; onFitCanvas(): void; onOpenBrowser(): void; + onOpenFiles(): void; onOpenSettings(): void; onClose(): void; } @@ -57,6 +58,7 @@ export function CanvasCommandPalette({ onPasteFiles, onFitCanvas, onOpenBrowser, + onOpenFiles, onOpenSettings, onClose }: CanvasCommandPaletteProps): React.JSX.Element { @@ -138,6 +140,15 @@ export function CanvasCommandPalette({ icon: "browser", run: onOpenBrowser }, + { + id: "open:files", + group: "actions", + kind: "action", + label: t(locale, "files"), + searchDetail: t(locale, "canvasMenuActions"), + icon: "folder", + run: onOpenFiles + }, { id: "open:settings", group: "actions", @@ -148,8 +159,9 @@ export function CanvasCommandPalette({ shortcut: window.canvasTTY.window.isMacOS ? "⌘," : "Ctrl+,", run: onOpenSettings } - ], [launcherItems, locale, onAddFiles, onCreateNote, onCreateRegion, onFitCanvas, onFocusSession, onLaunch, onOpenBrowser, + ], [launcherItems, locale, onAddFiles, onCreateNote, onCreateRegion, onFitCanvas, onFocusSession, onLaunch, onOpenBrowser, onOpenFiles, onOpenSettings, onPasteFiles, sessions]); + const filtered = useMemo(() => { const normalized = query.trim().toLocaleLowerCase(locale); if (!normalized) return commands; diff --git a/src/renderer/src/features/workspace/CanvasContextMenu.tsx b/src/renderer/src/features/workspace/CanvasContextMenu.tsx index a5adfb0b..cdd5d587 100644 --- a/src/renderer/src/features/workspace/CanvasContextMenu.tsx +++ b/src/renderer/src/features/workspace/CanvasContextMenu.tsx @@ -34,6 +34,7 @@ interface CanvasContextMenuProps { onCreateNote(): void; onLaunch(provider: ProviderId): void; onOpenBrowser(): void; + onOpenFiles(): void; onOpenSettings(): void; onRenameRegion(): void; onChangeRegionColor(color: string): void; @@ -62,6 +63,7 @@ export function CanvasContextMenu({ onCreateNote, onLaunch, onOpenBrowser, + onOpenFiles, onOpenSettings, onRenameRegion, onChangeRegionColor, @@ -192,6 +194,9 @@ export function CanvasContextMenu({ {t(locale, "canvasMenuOpenBrowser")} + + {t(locale, "files")} + ( && previous.locale === next.locale && previous.interactionMode === next.interactionMode && previous.onCameraChange === next.onCameraChange + && sameFileCards(previous.fileCards, next.fileCards) && minimapContentEqual(previous, next) )); @@ -64,6 +70,7 @@ function CanvasMinimapView({ stickyNotes, materials, pluginCanvas, + fileCards = [], browserCanvas, layerOrder, locale, @@ -97,8 +104,10 @@ function CanvasMinimapView({ ...stickyNotes.map((note) => ({ id: noteLayerId(note.id), kind: "note" as const, bounds: note })), ...materials.map((material) => ({ id: materialLayerId(material.id), kind: "material" as const, bounds: material })), ...pluginCanvas.map((instance) => ({ id: pluginLayerId(instance.id), kind: "plugin" as const, bounds: instance })), + ...fileCards.map((card) => ({ id: filesLayerId(card.id), kind: "files" as const, bounds: card })), ...(browserCanvas ? [{ id: browserLayerId, kind: "browser" as const, bounds: browserCanvas }] : []) - ], [browserCanvas, materials, pluginCanvas, sessions, stickyNotes]); + ], [browserCanvas, fileCards, materials, pluginCanvas, sessions, stickyNotes]); + const layerIndices = useMemo( () => new Map(layerOrder.map((id, index) => [id, index + 1])), [layerOrder] @@ -286,3 +295,22 @@ function areaStyle(area: { x: number; y: number; width: number; height: number } height: `${area.height * 100}%` }; } + +/** File cards carry no provider, so `minimapContentEqual` does not see them; compare their bounds here. */ +function sameFileCards( + previous: readonly FileCard[] | undefined, + next: readonly FileCard[] | undefined +): boolean { + const left = previous ?? []; + const right = next ?? []; + return left === right || ( + left.length === right.length + && left.every((card, index) => ( + card.id === right[index].id + && card.position.x === right[index].position.x + && card.position.y === right[index].position.y + && card.size.width === right[index].size.width + && card.size.height === right[index].size.height + )) + ); +} diff --git a/src/renderer/src/features/workspace/WorkspaceCanvas.tsx b/src/renderer/src/features/workspace/WorkspaceCanvas.tsx index 73fffc2e..fcf32e5e 100644 --- a/src/renderer/src/features/workspace/WorkspaceCanvas.tsx +++ b/src/renderer/src/features/workspace/WorkspaceCanvas.tsx @@ -1,4 +1,5 @@ -import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; + import type { MutableRefObject } from "react"; import { BUNDLED_CANVAS_BACKGROUND_IDS } from "../../../../shared/contracts"; import type { @@ -30,6 +31,8 @@ import { UiIcon } from "../../components/UiIcon"; import { t } from "../../lib/i18n"; import { displayCanvasNavigationBinding, isRenameInputTarget, isShortcutCaptureTarget, matchesPhysicalOrLayoutKey, matchesShortcut, shouldKeepNativeKeyboardInput } from "../../lib/shortcuts"; import { BrowserCard } from "../browser/BrowserCard"; +import type { FileBrowserCardProps, FileSessionOption } from "../files/FileBrowserCard"; +import { fileCardRuntimeFor, type FileCardRuntimeState } from "../files/fileCardRuntime"; import { attentionQueueRenderedAt, attentionSessions } from "../home/attentionQueue"; import type { LimitsLoadState } from "../home/homeModel"; import { homeGridPixelSize, homeLayoutFitsGrid } from "../home/homeLayout"; @@ -88,6 +91,7 @@ import { browserCanvasWidgetId, canvasWidgetInDirection, canvasWidgetTarget, + filesCanvasWidgetId, pluginCanvasWidgetId, terminalCanvasWidgetId, type CanvasFocusCandidate, @@ -96,7 +100,9 @@ import { import { boundsIntersect } from "./minimapGeometry"; import { browserLayerId, + filesLayerId, materialLayerId, + noteLayerId, parseCanvasLayerId, pluginLayerId, @@ -175,7 +181,9 @@ type RegionMovePreview = { pluginBounds: ReadonlyMap; browserBounds: SessionBounds | null; noteBounds: ReadonlyMap; + fileCardBounds: ReadonlyMap; materialBounds: ReadonlyMap; + }; interface WorkspaceCanvasProps { @@ -240,6 +248,18 @@ interface WorkspaceCanvasProps { onStickyNoteBoundsChange(id: string, bounds: SessionBounds): void; onStickyNoteTextChange(id: string, text: string): void; onDeleteStickyNote(id: string): void; + fileCardState: Readonly>; + fileSessionOptions: readonly FileSessionOption[]; + onOpenFiles(position?: Point): void; + onFocusFileCard(id: string): void; + onFileCardBoundsChange(id: string, bounds: SessionBounds): void; + onCloseFileCard(id: string): void; + onChooseFileFolder(id: string): void; + onRegisterFileSession(id: string, sessionId: string): void; + onOpenFileDirectory(id: string, relativePath: string): void; + onOpenFile(id: string, relativePath: string): void; + onFileQuickOpen(id: string, query: string): void; + onOpenLink(href: string): void; materials: readonly CanvasMaterial[]; onAddMaterialFiles(files: File[], point: Point): void; onPickMaterials(point: Point): void; @@ -251,8 +271,14 @@ interface WorkspaceCanvasProps { remarks: readonly MaterialRemark[]; onAddRemark(draft: RemarkDraft): Promise; onRemarkAction(remarkId: string, action: "delete"): void; + } +// The Files card and its rich renderers (react-markdown / rehype / lowlight) are +// kept out of the startup bundle: lazy-load the card so its weight is fetched +// only when a Files card exists (see tests/renderer-bundle-splitting.test.mjs). +const FileBrowserCard = lazy(() => import("../files/FileBrowserCard").then((module) => ({ default: module.FileBrowserCard }))); + export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element { const { settings, mediaData, sessions, limits, limitsLoadState, plugins, browser, @@ -267,6 +293,9 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element onCloseBrowser, onCreateCanvasRegion, onChangeCanvasRegion, onCanvasRegionBoundsChange, onDeleteCanvasRegion, onCreateStickyNote, onStickyNoteBoundsChange, onStickyNoteTextChange, onDeleteStickyNote, + fileCardState, fileSessionOptions, onOpenFiles, onFocusFileCard, onFileCardBoundsChange, + onCloseFileCard, onChooseFileFolder, onRegisterFileSession, onOpenFileDirectory, onOpenFile, + onFileQuickOpen, onOpenLink, materials, onAddMaterialFiles, onPickMaterials, onPasteMaterials, onMaterialBoundsChange, onMaterialBoundsChangeBatch, onRemoveMaterial, onMaterialCommand, remarks, onAddRemark, onRemarkAction, surfacesMounted = true } = props; @@ -283,7 +312,9 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element const suppressNextContextMenu = useRef(false); const pendingRadialContextMenu = useRef(null); const [noteEditRequest, setNoteEditRequest] = useState<{ id: string; version: number } | null>(null); + const [fileQuickOpenRequest, setFileQuickOpenRequest] = useState<{ id: string; version: number } | null>(null); const [materialRemoveRequest, setMaterialRemoveRequest] = useState<{ id: string; version: number } | null>(null); + const [regionMovePreview, setRegionMovePreview] = useState(null); const [marqueeSelection, setMarqueeSelection] = useState>(EMPTY_MARQUEE_SELECTION); const [masterPixelSkinSessionIds, setMasterPixelSkinSessionIds] = useState>(() => new Set()); @@ -336,10 +367,12 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element ? copyBounds(settings.browserCanvas) : null, noteBounds: containedBounds(settings.stickyNotes, startRegion), + fileCardBounds: containedBounds(settings.fileCards, startRegion), materialBounds: containedBounds(materials, startRegion) }; }); - }, [materials, sessions, settings.browserCanvas, settings.canvasRegions, settings.pluginCanvas, settings.stickyNotes]); + }, [materials, sessions, settings.browserCanvas, settings.canvasRegions, settings.fileCards, settings.pluginCanvas, settings.stickyNotes]); + // A press can lose its pointer (window blur, leaving Edit HOME) before it reaches a // pointer-up, so the scene drops any live preview instead of leaving it stuck. @@ -382,6 +415,14 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element const start = regionMovePreview?.noteBounds.get(note.id); return start && previewDelta ? { ...note, ...translateBounds(start, previewDelta) } : note; }), [previewDelta, regionMovePreview, settings.stickyNotes]); + const renderedFileCards = useMemo(() => settings.fileCards.map((card) => { + const start = regionMovePreview?.fileCardBounds.get(card.id); + return start && previewDelta ? { ...card, ...translateBounds(start, previewDelta) } : card; + }), [previewDelta, regionMovePreview, settings.fileCards]); + // The shortcut listener is subscribed once; this ref keeps the quick-open + // hotkey mapped to the latest rendered cards without re-subscribing. + const renderedFileCardsRef = useRef(renderedFileCards); + renderedFileCardsRef.current = renderedFileCards; const { remarkDraft, selectedRemarkId, materialNames, remarkActions, remarkingFor } = useRemarkDraft({ materials, remarks, @@ -393,6 +434,7 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element return start && previewDelta ? { ...material, ...translateBounds(start, previewDelta) } : material; }), [materials, previewDelta, regionMovePreview]); + const renderablePluginIds = useMemo(() => new Set(settings.pluginCanvas.filter((instance) => { const plugin = plugins.find((candidate) => candidate.manifest.id === instance.pluginId && candidate.enabled); return plugin?.manifest.contributions.some((candidate) => ( @@ -408,8 +450,10 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element ...renderedPluginCanvas.filter((instance) => renderablePluginIds.has(instance.id)).map((instance) => pluginLayerId(instance.id)), ...(renderedBrowserCanvas ? [browserLayerId] : []), ...renderedStickyNotes.map((note) => noteLayerId(note.id)), + ...renderedFileCards.map((card) => filesLayerId(card.id)), ...renderedMaterials.map((material) => materialLayerId(material.id)) - ], [renderablePluginIds, renderedBrowserCanvas, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); + ], [renderablePluginIds, renderedBrowserCanvas, renderedFileCards, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); + const [layerOrder, setLayerOrder] = useState(activeLayerIds); useEffect(() => { setLayerOrder((current) => reconcileCanvasLayerOrder(current, activeLayerIds)); @@ -425,23 +469,27 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element } if (renderedBrowserCanvas) result.set(browserLayerId, renderedBrowserCanvas); for (const note of renderedStickyNotes) result.set(noteLayerId(note.id), note); + for (const card of renderedFileCards) result.set(filesLayerId(card.id), card); for (const material of renderedMaterials) result.set(materialLayerId(material.id), material); return result; - }, [renderablePluginIds, renderedBrowserCanvas, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); - // Every window on the canvas, in the order they are rendered: terminals, plugin canvases, browser, notes, materials. + }, [renderablePluginIds, renderedBrowserCanvas, renderedFileCards, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); + const allWindowBounds = useMemo((): SessionBounds[] => [ ...renderedSessions, ...renderedPluginCanvas.filter((instance) => renderablePluginIds.has(instance.id)), ...(renderedBrowserCanvas ? [renderedBrowserCanvas] : []), ...renderedStickyNotes, + ...renderedFileCards, ...renderedMaterials - ], [renderablePluginIds, renderedBrowserCanvas, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); + ], [renderablePluginIds, renderedBrowserCanvas, renderedFileCards, renderedMaterials, renderedPluginCanvas, renderedSessions, renderedStickyNotes]); + const focusCandidates: CanvasFocusCandidate[] = [ ...renderedSessions.map((session) => ({ id: terminalCanvasWidgetId(session.id), bounds: session })), ...renderedPluginCanvas .filter((instance) => renderablePluginIds.has(instance.id)) .map((instance) => ({ id: pluginCanvasWidgetId(instance.id), bounds: instance })), - ...(renderedBrowserCanvas ? [{ id: browserCanvasWidgetId, bounds: renderedBrowserCanvas }] : []) + ...(renderedBrowserCanvas ? [{ id: browserCanvasWidgetId, bounds: renderedBrowserCanvas }] : []), + ...renderedFileCards.map((card) => ({ id: filesCanvasWidgetId(card.id), bounds: card })) ]; const homeBounds = useMemo((): SessionBounds => ({ @@ -523,11 +571,13 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element if (ref.kind === "terminal" && ref.targetId !== null) onSessionBoundsChange(ref.targetId, moved); else if (ref.kind === "plugin" && ref.targetId !== null) onPluginCanvasBoundsChange(ref.targetId, moved); else if (ref.kind === "note" && ref.targetId !== null) onStickyNoteBoundsChange(ref.targetId, moved); + else if (ref.kind === "files" && ref.targetId !== null) onFileCardBoundsChange(ref.targetId, moved); else if (ref.kind === "material" && ref.targetId !== null) materialBatch.push({ id: ref.targetId, bounds: moved }); else if (ref.kind === "browser" && settings.browserCanvas) onBrowserBoundsChange({ ...settings.browserCanvas, ...moved }); } if (materialBatch.length > 0) onMaterialBoundsChangeBatch(materialBatch); - }, [boundsByLayer, homeBounds, onBrowserBoundsChange, onMaterialBoundsChangeBatch, onPluginCanvasBoundsChange, + }, [boundsByLayer, homeBounds, onBrowserBoundsChange, onFileCardBoundsChange, onMaterialBoundsChangeBatch, onPluginCanvasBoundsChange, + onSessionBoundsChange, onStickyNoteBoundsChange, renderedCanvasRegions, settings.browserCanvas, settings.snapToGrid]); const focusController = useCanvasWidgetFocus({ @@ -546,7 +596,9 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element ].join(":")).join(";"), settings.pluginCanvas.map((instance) => instance.id).join(","), settings.stickyNotes.map((note) => note.id).join(","), + settings.fileCards.map((card) => card.id).join(","), materials.map((material) => material.id).join(","), + settings.homeLayout.map((placement) => placement.widgetId).join(",") ].join("|") }); @@ -886,8 +938,14 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element onFocusBrowser(); return; } - }, [focusCandidates, focusController, onFocusBrowser, onFocusPluginCanvas, onFocusSession, - renderedBrowserCanvas, renderedPluginCanvas, renderedSessions, raiseLayer, viewportCenterWorldPoint]); + const fileCard = renderedFileCards.find((candidate) => filesCanvasWidgetId(candidate.id) === target); + if (fileCard) { + raiseLayer(filesLayerId(fileCard.id)); + focusController.focus(target, "explicit"); + onFocusFileCard(fileCard.id); + } + }, [focusCandidates, focusController, onFocusBrowser, onFocusFileCard, onFocusPluginCanvas, onFocusSession, + renderedBrowserCanvas, renderedFileCards, renderedPluginCanvas, renderedSessions, raiseLayer, viewportCenterWorldPoint]); const fitCanvasRef = useRef(fitCanvas); fitCanvasRef.current = fitCanvas; @@ -947,9 +1005,27 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element setRegionEditor(null); setCommandPaletteOpen(false); onOpenSettings(); + } else if ((event.metaKey || event.ctrlKey) && !event.altKey + && matchesPhysicalOrLayoutKey(event, "KeyP", "p")) { + // Quick open belongs to the focused Files card: focus its search input + // and let its own input drive `files:search` through the App. + const focusedId = focusController.stateRef.current.id; + const activeCard = renderedFileCardsRef.current.find((card) => ( + filesCanvasWidgetId(card.id) === focusedId + )); + if (!activeCard) return; + event.preventDefault(); + setContextMenu(null); + setRegionEditor(null); + setCommandPaletteOpen(false); + setFileQuickOpenRequest((current) => ({ + id: activeCard.id, + version: (current?.version ?? 0) + 1 + })); } else if ((event.ctrlKey || event.metaKey) && !event.altKey && matchesPhysicalOrLayoutKey(event, "KeyV", "v") && !event.shiftKey && !acceptsTextInput(event.target)) { event.preventDefault(); pasteMaterialsRef.current(); + } }; window.addEventListener("keydown", handleShortcut, true); @@ -1240,13 +1316,50 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element /> ))} + {renderedFileCards.map((card) => { + const state = fileCardRuntimeFor(fileCardState, card.id); + return ( + onChooseFileFolder(card.id)} + onRegisterSession={(sessionId) => onRegisterFileSession(card.id, sessionId)} + onOpenDirectory={(relativePath) => onOpenFileDirectory(card.id, relativePath)} + onOpenFile={(relativePath) => onOpenFile(card.id, relativePath)} + onQuickOpen={(query) => onFileQuickOpen(card.id, query)} + onChangeBounds={(bounds) => onFileCardBoundsChange(card.id, bounds)} + onClose={() => onCloseFileCard(card.id)} + onOpenLink={onOpenLink} + groupSelected={marqueeSelection.has(filesLayerId(card.id))} + /> + ); + })} {renderedMaterials.map((material) => ( ))} + @@ -1351,6 +1465,10 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element onOpenBrowser(contextMenu.worldPoint); setContextMenu(null); }} + onOpenFiles={() => { + onOpenFiles(contextMenu.worldPoint); + setContextMenu(null); + }} onOpenSettings={() => { onOpenSettings(); setContextMenu(null); @@ -1468,6 +1586,7 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element onPasteFiles={() => pasteMaterialsAt(viewportCenterWorldPoint())} onFitCanvas={fitCanvas} onOpenBrowser={() => onOpenBrowser(viewportCenterWorldPoint())} + onOpenFiles={() => onOpenFiles(viewportCenterWorldPoint())} onOpenSettings={onOpenSettings} onClose={() => setCommandPaletteOpen(false)} /> @@ -1522,8 +1641,8 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element {settings.minimapPlacement === placement && ( @@ -1565,6 +1684,12 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element ); } +/** The Files card reads its zoom from the camera store, so a pan or zoom re-renders only the card, not the canvas. */ +function WorkspaceFileCard({ camera, ...props }: Omit & { camera: CameraStore }): React.JSX.Element { + const zoom = useCameraSelector(camera, (value) => value.zoom); + return ; +} + function copyBounds(bounds: SessionBounds): SessionBounds { return { position: { ...bounds.position }, diff --git a/src/renderer/src/features/workspace/canvasSelectionGesture.ts b/src/renderer/src/features/workspace/canvasSelectionGesture.ts index dd11e074..b6d74ead 100644 --- a/src/renderer/src/features/workspace/canvasSelectionGesture.ts +++ b/src/renderer/src/features/workspace/canvasSelectionGesture.ts @@ -4,7 +4,8 @@ import type { CameraState, Point, SessionBounds } from "../../../../shared/contr export const CANVAS_DRAG_THRESHOLD = 3; /** Every kind of window the canvas renders, side by side, in one scene. */ -export type CanvasLayerKind = "terminal" | "plugin" | "browser" | "note" | "material"; +export type CanvasLayerKind = "terminal" | "plugin" | "browser" | "note" | "files" | "material"; + /** A canvas layer id decoded into what it names. The browser has no target of its own. */ export interface CanvasLayerRef { @@ -27,6 +28,10 @@ export function noteLayerId(id: string): string { return `note:${id}`; } +export function filesLayerId(id: string): string { + return `files:${id}`; +} + export function materialLayerId(id: string): string { return `material:${id}`; } @@ -34,6 +39,7 @@ export function materialLayerId(id: string): string { /** * The inverse of the layer-id helpers, and the single place that decodes the scheme. * Anything that is not one of the layer prefixes — including an id with nothing + * after the colon — is not a layer id. */ export function parseCanvasLayerId(layerId: string): CanvasLayerRef | null { @@ -42,7 +48,8 @@ export function parseCanvasLayerId(layerId: string): CanvasLayerRef | null { if (separator === -1 || separator === layerId.length - 1) return null; const kind = layerId.slice(0, separator); const targetId = layerId.slice(separator + 1); - if (kind !== "terminal" && kind !== "plugin" && kind !== "note" && kind !== "material") return null; + if (kind !== "terminal" && kind !== "plugin" && kind !== "note" && kind !== "files" && kind !== "material") return null; + return { kind, targetId }; } diff --git a/src/renderer/src/features/workspace/canvasWidgetFocus.ts b/src/renderer/src/features/workspace/canvasWidgetFocus.ts index 2324d409..0fc0e9a4 100644 --- a/src/renderer/src/features/workspace/canvasWidgetFocus.ts +++ b/src/renderer/src/features/workspace/canvasWidgetFocus.ts @@ -15,6 +15,10 @@ export function pluginCanvasWidgetId(instanceId: string): string { return `plugin-canvas:${instanceId}`; } +export function filesCanvasWidgetId(cardId: string): string { + return `files-canvas:${cardId}`; +} + export function homeCanvasWidgetId(widgetId: string): string { return `home:${widgetId}`; } diff --git a/src/renderer/src/lib/i18n.ts b/src/renderer/src/lib/i18n.ts index 00047166..eb42104c 100644 --- a/src/renderer/src/lib/i18n.ts +++ b/src/renderer/src/lib/i18n.ts @@ -947,6 +947,28 @@ const ru = { enableAgentControl: "Включить эндпоинт", agentControlEnabled: "Эндпоинт оркестрации агентов", agentControlEnabledDescription: "Локальный эндпоинт только для текущего пользователя, через который встроенный CLI canvastty-control и сессии с ролью «Оркестратор» создают сессии и управляют ими. По умолчанию выключен.", + files: "Файлы", + filesChooseFolder: "Выбрать папку", + filesSelectRoot: "Выберите корень", + filesSelectRootHint: "Откройте рабочую папку сессии или выберите папку на диске.", + filesSessionRoot: "Рабочая папка сессии", + filesRootUnavailable: "Корень недоступен", + filesRootUnavailableHint: "Эта папка больше не существует или недоступна для чтения. Выберите другой корень.", + filesLoading: "Загрузка…", + filesNoFile: "Файл не выбран", + filesNoFileHint: "Выберите файл в дереве или воспользуйтесь быстрым переходом.", + filesEmptyDirectory: "Папка пуста", + filesTruncated: "Показано начало файла (усечено)", + filesUnsupported: "Предпросмотр недоступен", + filesUnsupportedBinary: "Двоичный файл", + filesUnsupportedNotPermitted: "Тип файла не разрешён", + filesUnsupportedUnavailable: "Файл недоступен", + filesTooLarge: "Файл слишком большой для предпросмотра", + filesRichDisabled: "Файл слишком большой для расширенного просмотра — показан обычный текст", + filesQuickOpen: "Быстрый переход", + filesQuickOpenPlaceholder: "Поиск файлов…", + filesNoMatches: "Нет совпадающих файлов", + filesError: "Не удалось прочитать", baseProtection: "Базовая защита", baseProtectionDescription: "Запрещает агентам (Claude Code, Codex, Qwen Code, OpenCode на этом компьютере) sudo, curl | sh, запуск скачанного, команды для дисков, форк-бомбы и запись или удаление вне рабочей папки, включая /tmp. Только запрещает, ничего не разрешает. Действует для агентов, запущенных после изменения." } as const; @@ -1898,6 +1920,28 @@ const en: Record = { enableAgentControl: "Enable endpoint", agentControlEnabled: "Agent orchestration endpoint", agentControlEnabledDescription: "Local, current-user-only endpoint through which the bundled canvastty-control CLI and Orchestrator sessions create and drive sessions. Off by default.", + files: "Files", + filesChooseFolder: "Choose folder", + filesSelectRoot: "Select a root", + filesSelectRootHint: "Open a session working directory or pick a folder on disk.", + filesSessionRoot: "Session working directory", + filesRootUnavailable: "Root unavailable", + filesRootUnavailableHint: "This folder no longer exists or cannot be read. Choose another root.", + filesLoading: "Loading…", + filesNoFile: "No file selected", + filesNoFileHint: "Choose a file from the tree or use quick open.", + filesEmptyDirectory: "Folder is empty", + filesTruncated: "Showing the beginning of the file (truncated)", + filesUnsupported: "Preview unavailable", + filesUnsupportedBinary: "Binary file", + filesUnsupportedNotPermitted: "File type not permitted", + filesUnsupportedUnavailable: "File unavailable", + filesTooLarge: "File is too large to preview", + filesRichDisabled: "File is too large for rich rendering — showing plain text", + filesQuickOpen: "Quick open", + filesQuickOpenPlaceholder: "Search files…", + filesNoMatches: "No files matched", + filesError: "Failed to read", baseProtection: "Base protection", baseProtectionDescription: "Blocks agents (Claude Code, Codex, Qwen Code, OpenCode on this computer) from sudo, curl | sh, running downloads, disk commands, fork bombs, and writing or deleting outside the working folder, /tmp included. It only blocks and never allows. Applies to agents started after a change." }; diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index 3ec79eaa..979a4c3e 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -1400,3 +1400,186 @@ button { border: 0; } .renderer-recovery strong { font-size: 16px; } .renderer-recovery p { margin: 0; max-width: 420px; color: rgba(255, 255, 255, 0.7); font-size: 13px; line-height: 1.5; } .renderer-recovery button { padding: 8px 16px; border: 0; border-radius: 10px; background: #b8cf99; color: #30313d; font: inherit; font-weight: 700; cursor: pointer; } +@media (prefers-reduced-motion: reduce) { .settings-updates__card[data-tone="busy"] .settings-updates__icon { animation: none; } } + +/* Files card: rooted directory tree on the left, file viewer on the right. + Reuses the shared 54px header, resize handles, and low-zoom summary grammar. */ +.file-browser-card { --card-header-height: calc(54px * var(--ui-scale, 1)); position: absolute; left: 0; top: 0; width: 780px; height: 520px; overflow: hidden; border: 1px solid rgba(255,255,255,.1); border-radius: 17px; color: white; background: var(--surface); box-shadow: var(--shadow-lg); transition: box-shadow .15s ease; } +.file-browser-card:focus-within, .file-browser-card--selected { box-shadow: 0 0 0 3px var(--primary), var(--shadow-lg); } +.file-browser-card__header { height: var(--card-header-height); padding: 0 calc(11px * var(--ui-scale, 1)) 0 calc(15px * var(--ui-scale, 1)); display: flex; align-items: center; justify-content: space-between; gap: calc(12px * var(--ui-scale, 1)); color: white; background: var(--surface-soft); cursor: move; font-size: calc(13px * var(--ui-scale, 1)); touch-action: none; } +.file-browser-card__title { min-width: 0; display: flex; align-items: center; gap: .77em; } +.file-browser-card__title > span { min-width: 0; display: flex; flex-direction: column; gap: .23em; } +.file-browser-card__title strong, .file-browser-card__title small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-browser-card__title strong { font-size: 1em; } +.file-browser-card__title small { max-width: 41.54em; color: var(--text-muted-on-dark); font-size: .77em; } +.file-browser-card__actions { display: flex; align-items: center; gap: .38em; } +.file-browser-card__actions button { width: 2.15em; height: 2.15em; flex: 0 0 auto; display: grid; place-items: center; border-radius: .54em; color: rgba(255,255,255,.72); background: rgba(255,255,255,.06); cursor: pointer; } +.file-browser-card__actions button:hover { color: white; background: rgba(255,255,255,.15); } +.file-browser-card__actions .file-browser-card__close:hover { background: var(--danger); } + +.file-browser-card__body { height: calc(100% - var(--card-header-height)); min-height: 0; display: grid; grid-template-columns: minmax(190px, 34%) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); overflow: hidden; } + +.file-browser-card__tree { position: relative; min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; border-right: 1px solid rgba(255,255,255,.09); background: color-mix(in srgb, var(--surface) 86%, black); } +.file-browser-card__tree-toolbar { display: flex; align-items: center; gap: .5em; padding: calc(7px * var(--ui-scale, 1)) calc(8px * var(--ui-scale, 1)); border-bottom: 1px solid rgba(255,255,255,.08); } +.file-browser-card__tree-search { min-width: 0; flex: 1; display: flex; align-items: center; gap: .45em; height: 2.1em; padding: 0 .55em; border-radius: .55em; color: var(--text-muted-on-dark); background: rgba(255,255,255,.06); } +.file-browser-card__tree-search input { width: 100%; min-width: 0; height: 100%; border: 0; outline: 0; color: white; background: transparent; font: 700 .82em var(--font-mono); } +.file-browser-card__tree-search input::placeholder { color: var(--text-muted-on-dark); } + +.file-browser-card__quick-open { position: absolute; left: 6px; right: 6px; top: calc(2.9em + 6px); z-index: 6; max-height: 60%; overflow-y: auto; padding: .3em; display: flex; flex-direction: column; gap: .15em; border: 1px solid rgba(255,255,255,.12); border-radius: .7em; background: rgba(24,26,35,.98); box-shadow: var(--shadow-md); } +.file-browser-card__quick-result { padding: .42em .5em; overflow: hidden; border-radius: .45em; color: rgba(255,255,255,.82); background: transparent; cursor: pointer; font: 700 .78em/1.2 var(--font-mono); text-align: left; text-overflow: ellipsis; white-space: nowrap; } +.file-browser-card__quick-result:hover, .file-browser-card__quick-result[aria-selected="true"] { color: white; background: color-mix(in srgb, var(--primary) 30%, transparent); } +.file-browser-card__quick-empty { margin: 0; padding: .7em .5em; color: var(--text-muted-on-dark); font-size: .78em; } + +.file-browser-card__tree-list { min-height: 0; flex: 1; overflow: auto; padding: .35em .35em .6em; } +.file-browser-card__tree-row { width: 100%; display: flex; align-items: center; gap: .4em; padding: .3em .45em; padding-left: calc(.45em + var(--tree-depth, 0) * .95em); border-radius: .45em; color: rgba(255,255,255,.82); background: transparent; cursor: pointer; font-size: .82em; font-weight: 700; text-align: left; } +.file-browser-card__tree-row:hover { background: rgba(255,255,255,.09); } +.file-browser-card__tree-row--active { color: white; background: color-mix(in srgb, var(--primary) 34%, transparent); } +.file-browser-card__tree-caret { width: .95em; flex: 0 0 .95em; display: grid; place-items: center; color: var(--text-muted-on-dark); } +.file-browser-card__tree-row--directory[aria-expanded="false"] .file-browser-card__tree-caret .ui-icon { transform: rotate(-90deg); } +.file-browser-card__tree-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-browser-card__tree-notice { margin: auto; padding: 1.2em; display: flex; flex-direction: column; align-items: center; gap: .4em; color: var(--text-muted-on-dark); text-align: center; } +.file-browser-card__tree-notice strong { color: rgba(255,255,255,.82); font-size: .85em; } +.file-browser-card__tree-notice small { font-size: .75em; line-height: 1.4; } + +.file-browser-card__viewer { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: #20222c; } +.file-browser-card__viewer-state { margin: auto; padding: 1.6em; display: flex; flex-direction: column; align-items: center; gap: .55em; color: var(--text-muted-on-dark); text-align: center; } +.file-browser-card__viewer-state strong { color: rgba(255,255,255,.85); font-size: .95em; } +.file-browser-card__viewer-state small { max-width: 34em; font-size: .78em; line-height: 1.45; overflow-wrap: anywhere; } +.file-browser-card__root-select { margin: auto; width: min(30em, 86%); display: flex; flex-direction: column; align-items: center; gap: .5em; color: var(--text-muted-on-dark); text-align: center; } +.file-browser-card__root-select strong { color: rgba(255,255,255,.88); font-size: 1em; } +.file-browser-card__root-select p { margin: 0; font-size: .8em; line-height: 1.45; } +.file-browser-card__root-select > button { margin-top: .2em; min-height: 2.3em; padding: 0 .95em; display: flex; align-items: center; gap: .45em; border-radius: .6em; color: var(--text-dark); background: var(--primary); cursor: pointer; font-size: .84em; font-weight: 850; } +.file-browser-card__root-select > button:hover { background: var(--primary-hover); } +.file-browser-card__root-select small { margin-top: .4em; color: var(--text-muted-on-dark); font-size: .72em; text-transform: uppercase; letter-spacing: .04em; } +.file-browser-card__session-options { width: 100%; max-height: 9em; overflow: auto; display: flex; flex-direction: column; gap: .25em; } +.file-browser-card__session-options button { width: 100%; padding: .45em .6em; overflow: hidden; border-radius: .5em; color: rgba(255,255,255,.85); background: rgba(255,255,255,.07); cursor: pointer; font-size: .8em; font-weight: 700; text-align: left; text-overflow: ellipsis; white-space: nowrap; } +.file-browser-card__session-options button:hover { color: white; background: rgba(255,255,255,.14); } + +.file-browser-card__viewer-banner { flex: 0 0 auto; display: flex; align-items: center; gap: .5em; padding: .5em .75em; color: var(--text-dark); background: var(--warning, #d8a24a); font-size: .78em; font-weight: 800; } +.file-browser-card__viewer-banner small { margin-left: auto; opacity: .75; } +.file-browser-card__viewer-text { min-height: 0; flex: 1; margin: 0; padding: 1em 1.1em; overflow: auto; color: #e6e8ee; background: #20222c; font: 400 .82em/1.55 var(--font-mono); white-space: pre; user-select: text; } +.file-browser-card__viewer-image-wrap { min-height: 0; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .7em; padding: 1em; } +.file-browser-card__viewer-image { max-width: 100%; max-height: calc(100% - 2em); object-fit: contain; } +.file-browser-card__viewer-image-wrap small { color: var(--text-muted-on-dark); font-size: .75em; } + +.file-browser-card__summary { position: absolute; inset: var(--card-header-height) 0 0; z-index: 10; width: 100%; height: calc(100% - var(--card-header-height)); display: none; align-items: center; justify-content: center; overflow: hidden; padding: 24px; color: white; background: var(--surface); } +.file-browser-card__summary-content { width: var(--summary-content-width); max-width: calc(100% - 48px); display: flex; flex-direction: column; align-items: center; gap: 9px; transform: scale(var(--summary-scale, 1)); text-align: center; } +.file-browser-card__summary strong, .file-browser-card__summary small { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-browser-card__summary strong { font-size: 31px; letter-spacing: -.025em; } +.file-browser-card__summary small { color: var(--text-muted-on-dark); font-size: 16px; } +.file-browser-card--summary .file-browser-card__body { visibility: hidden; } +.file-browser-card--summary .file-browser-card__summary { display: flex; } + +.file-browser-card .terminal-card__resize-handle--ne, +.file-browser-card .terminal-card__resize-handle--se, +.file-browser-card .terminal-card__resize-handle--sw, +.file-browser-card .terminal-card__resize-handle--nw { z-index: 21; } + +/* Rich file rendering: Markdown typography and highlight.js tokens. Every + colour below resolves through the `.app` palette variables (or a translucent + mix of `--text` / `--text-dark`) so the day/night/canvas themes stay in + control; nothing here hardcodes a second palette. */ +.file-browser-card .file-browser-card__rich { min-height: 0; flex: 1; display: flex; flex-direction: column; overflow: hidden; } + +.file-browser-card .file-browser-card__markdown { + min-height: 0; flex: 1; margin: 0; padding: 1em 1.1em; overflow: auto; + color: #e6e8ee; font: 400 .82em/1.55 var(--font-mono); + overflow-wrap: anywhere; word-break: break-word; user-select: text; +} +.file-browser-card .file-browser-card__markdown > :first-child { margin-top: 0; } +.file-browser-card .file-browser-card__markdown > :last-child { margin-bottom: 0; } + +.file-browser-card .file-browser-card__markdown h1, +.file-browser-card .file-browser-card__markdown h2, +.file-browser-card .file-browser-card__markdown h3, +.file-browser-card .file-browser-card__markdown h4, +.file-browser-card .file-browser-card__markdown h5, +.file-browser-card .file-browser-card__markdown h6 { margin: 1.05em 0 .5em; color: var(--text); font-weight: 800; line-height: 1.3; } +.file-browser-card .file-browser-card__markdown h1 { font-size: 1.45em; } +.file-browser-card .file-browser-card__markdown h2 { font-size: 1.28em; } +.file-browser-card .file-browser-card__markdown h3 { font-size: 1.15em; } +.file-browser-card .file-browser-card__markdown h4 { font-size: 1.05em; } +.file-browser-card .file-browser-card__markdown h5 { font-size: 1em; } +.file-browser-card .file-browser-card__markdown h6 { color: var(--text-muted-on-dark); font-size: .95em; text-transform: uppercase; letter-spacing: .04em; } + +.file-browser-card .file-browser-card__markdown p { margin: .6em 0; } +.file-browser-card .file-browser-card__markdown ul, +.file-browser-card .file-browser-card__markdown ol { margin: .55em 0; padding-left: 1.5em; } +.file-browser-card .file-browser-card__markdown li { margin: .2em 0; } +.file-browser-card .file-browser-card__markdown li > ul, +.file-browser-card .file-browser-card__markdown li > ol { margin: .2em 0; } + +.file-browser-card .file-browser-card__markdown blockquote { + margin: .7em 0; padding: .15em 0 .15em .8em; + border-left: 3px solid color-mix(in srgb, var(--primary) 55%, transparent); + color: var(--text-muted-on-dark); +} +.file-browser-card .file-browser-card__markdown hr { height: 0; margin: 1em 0; border: 0; border-top: 1px solid color-mix(in srgb, var(--text) 16%, transparent); } + +.file-browser-card .file-browser-card__markdown table { display: block; width: 100%; max-width: 100%; margin: .7em 0; overflow-x: auto; border-collapse: collapse; } +.file-browser-card .file-browser-card__markdown th, +.file-browser-card .file-browser-card__markdown td { padding: .3em .6em; border: 1px solid color-mix(in srgb, var(--text) 14%, transparent); text-align: left; vertical-align: top; } +.file-browser-card .file-browser-card__markdown thead th { background: color-mix(in srgb, var(--text) 7%, transparent); font-weight: 800; color: var(--text); } + +.file-browser-card .file-browser-card__markdown a { color: var(--blue); text-decoration: underline; text-underline-offset: .14em; } +.file-browser-card .file-browser-card__markdown a:hover { color: var(--secondary); } +.file-browser-card .file-browser-card__markdown strong { color: var(--text); font-weight: 800; } +.file-browser-card .file-browser-card__markdown em { font-style: italic; } +.file-browser-card .file-browser-card__markdown img { max-width: 100%; height: auto; } + +.file-browser-card .file-browser-card__markdown code { + padding: .12em .35em; border-radius: .35em; + background: color-mix(in srgb, var(--text) 10%, transparent); + font: .92em var(--font-mono); overflow-wrap: anywhere; word-break: break-word; +} +.file-browser-card .file-browser-card__markdown pre { + margin: .7em 0; padding: .7em .8em; overflow: auto; border-radius: .5em; + background: color-mix(in srgb, var(--text-dark) 45%, transparent); + white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; +} +.file-browser-card .file-browser-card__markdown pre > code { padding: 0; border-radius: 0; background: transparent; font-size: inherit; } + +/* Long code in the dedicated code viewer wraps instead of clipping/overflowing. */ +.file-browser-card .file-browser-card__code, +.file-browser-card .file-browser-card__code code { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } + +.file-browser-card .hljs { color: inherit; background: transparent; font-family: var(--font-mono); } +.file-browser-card .hljs-comment, +.file-browser-card .hljs-quote { color: var(--text-muted-on-dark); font-style: italic; } +.file-browser-card .hljs-keyword, +.file-browser-card .hljs-selector-tag, +.file-browser-card .hljs-selector-id, +.file-browser-card .hljs-selector-class { color: var(--pink); } +.file-browser-card .hljs-doctag, +.file-browser-card .hljs-meta { color: var(--primary); font-weight: 700; } +.file-browser-card .hljs-built_in, +.file-browser-card .hljs-type, +.file-browser-card .hljs-title, +.file-browser-card .hljs-section, +.file-browser-card .hljs-name { color: var(--yellow); } +.file-browser-card .hljs-literal, +.file-browser-card .hljs-number, +.file-browser-card .hljs-symbol, +.file-browser-card .hljs-bullet { color: var(--accent); } +.file-browser-card .hljs-string, +.file-browser-card .hljs-regexp, +.file-browser-card .hljs-addition { color: var(--success); } +.file-browser-card .hljs-attr, +.file-browser-card .hljs-attribute, +.file-browser-card .hljs-variable, +.file-browser-card .hljs-template-variable { color: var(--blue); } +.file-browser-card .hljs-tag { color: var(--text-muted-on-dark); } +.file-browser-card .hljs-deletion { color: var(--danger); } +.file-browser-card .hljs-emphasis { font-style: italic; } +.file-browser-card .hljs-strong { font-weight: 800; } +.file-browser-card .hljs-link { color: var(--blue); text-decoration: underline; } + +/* Sanitized Markdown fallbacks: muted plain text and a dashed placeholder. */ +.file-browser-card .file-browser-card__markdown-inert { color: var(--text-muted-on-dark); text-decoration: none; cursor: default; } +.file-browser-card .file-browser-card__markdown-image-placeholder { + display: inline-block; padding: 0 .3em; border-radius: .35em; + border: 1px dashed color-mix(in srgb, var(--text) 30%, transparent); + color: var(--text-muted-on-dark); font-size: .92em; +} + +.canvas-minimap__entity--files { background: rgba(184,207,153,.92); } diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 6a02a224..70d660ab 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -453,6 +453,8 @@ export interface AppSettings { canvasRegions: CanvasRegion[]; stickyNotes: StickyNote[]; pluginCanvas: PluginCanvasInstance[]; + /** Persisted Files card state (root reference, bounds, open file, tree). */ + fileCards: FileCard[]; browserCanvas: BrowserCanvasState | null; browserAgentAccess: boolean; browserShowAgentPresence: boolean; @@ -1132,6 +1134,82 @@ export interface PluginPlaylistFile { export interface BrowserCanvasState extends SessionBounds {} +/** How a Files card is rooted: a terminal session cwd or a dialog-picked folder. */ +export type FileRootKind = "session" | "folder"; + +/** + * Identifies a root without carrying a filesystem path across the bridge. + * Session roots name the terminal session whose cwd main resolves; folder roots + * are created by the native open-folder dialog in main. A folder reference may + * carry `folderPath` only on restore, to re-register a persisted folder root + * after relaunch; list/read/search never carry paths. + */ +export type FileRootReference = + | { rootType: "session"; sessionId: string } + | { rootType: "folder"; folderPath?: string }; + +/** + * Opaque main-owned handle for a registered root. `rootId` is the only way the + * renderer addresses the root; list/read/search always pair it with a relative + * path so no absolute root path ever reaches renderer payloads. + */ +export interface FileRootDescriptor { + rootId: string; + label: string; + available: boolean; + rootType: FileRootKind; + /** + * Canonical absolute folder path for folder roots, retained so a persisted + * root can be re-registered after relaunch. Session descriptors omit it, so a + * session cwd never crosses the bridge. + */ + folderPath?: string | null; +} + +export type FileEntryKind = "file" | "directory"; + +export interface FileEntry { + name: string; + relativePath: string; + kind: FileEntryKind; + /** Byte size for files; null for directories. */ + size: number | null; +} + +export type FileReadUnsupportedReason = "binary" | "not-permitted" | "unavailable"; + +/** Bounded, typed file content. Unsupported and oversized reads carry no bytes. */ +export type FileReadResult = + | { kind: "text"; content: string; truncated: boolean; size: number } + | { kind: "image"; mediaType: string; dataUrl: string; size: number } + | { kind: "unsupported"; reason: FileReadUnsupportedReason } + | { kind: "too-large"; reason: "too-large"; size: number }; + +/** Bounded filename-search output: relative file paths only. */ +export interface FileSearchResult { + relativePaths: string[]; + truncated: boolean; +} + +/** + * Persisted Files card state. `root` records how the card was rooted; folder + * roots retain `folderPath` so main can re-validate and re-open the folder after + * relaunch, while session roots resolve `sessionId` to its cwd again. Neither + * value is used in list/read/search payloads, which stay relative. + */ +export interface FileCard extends SessionBounds { + id: string; + root: FileRootReference; + /** Display label for the root; null when unknown. Never an absolute path. */ + label: string | null; + /** Absolute folder path for folder roots; null for session roots. */ + folderPath: string | null; + /** Relative path of the file shown in the viewer, or null. */ + activeFile: string | null; + /** Relative paths of expanded tree folders. */ + expandedFolders: string[]; +} + export interface BrowserViewportClipBounds extends Size { x: number; y: number; @@ -1720,6 +1798,17 @@ export interface CanvasTTYApi { media: { read(path: string): Promise; }; + files: { + listRoots(): Promise; + /** Registers a session or folder root reference; resolves null when unavailable. */ + registerRoot(reference: FileRootReference): Promise; + /** Opens the native folder picker; resolves null when canceled. */ + openFolder(): Promise; + list(rootId: string, relativePath: string): Promise; + read(rootId: string, relativePath: string): Promise; + search(rootId: string, query: string): Promise; + closeRoot(rootId: string): Promise; + }; materials: { snapshot(): Promise; addFiles(files: File[], point: Point): Promise; @@ -1736,6 +1825,7 @@ export interface CanvasTTYApi { updateRemark(id: string, patch: RemarkPatch): Promise; deleteRemark(id: string): Promise; onChanged(listener: (snapshot: MaterialsSnapshot) => void): () => void; + }; limits: { get(): Promise; @@ -1895,6 +1985,13 @@ export const IPC = { dialogPickDirectory: "dialog:pick-directory", dialogPickMedia: "dialog:pick-media", mediaRead: "media:read", + filesListRoots: "files:list-roots", + filesRegisterRoot: "files:register-root", + filesOpenFolder: "files:open-folder", + filesList: "files:list", + filesRead: "files:read", + filesSearch: "files:search", + filesCloseRoot: "files:close-root", materialsSnapshot: "materials:snapshot", materialsAddPaths: "materials:add-paths", materialsPick: "materials:pick", @@ -1910,6 +2007,7 @@ export const IPC = { materialsUpdateRemark: "materials:update-remark", materialsDeleteRemark: "materials:delete-remark", materialsChanged: "materials:changed", + limitsGet: "limits:get", pluginsList: "plugins:list", pluginsSearch: "plugins:search", diff --git a/tests/browser-smoke-readiness.test.mjs b/tests/browser-smoke-readiness.test.mjs index e2290eab..e46acf16 100644 --- a/tests/browser-smoke-readiness.test.mjs +++ b/tests/browser-smoke-readiness.test.mjs @@ -7,7 +7,8 @@ import { isBrowserSmokeWheelReady, waitForBrowserSmokeWheelReady, runBrowserSmokeCleanup, - requireBrowserSmokeScrollBaseline + requireBrowserSmokeScrollBaseline, + freezeEndedBeforeIdle } from "../src/main/services/browser/BrowserSmokeReadiness.ts"; const page = { @@ -22,6 +23,15 @@ const page = { const ready = { ownerVisible: true, ownerFocused: true, page }; const point = { x: 700, y: 300 }; +test("freeze smoke distinguishes a transition end from normal wheel idle", () => { + const active = { active: true, dataUrlLength: 42, observedAt: 1_000 }; + assert.equal(freezeEndedBeforeIdle([active, { active: false, observedAt: 1_180 }], 1_000, 250), true); + assert.equal(freezeEndedBeforeIdle([active, { active: false, observedAt: 1_249 }], 1_000, 250), true); + assert.equal(freezeEndedBeforeIdle([active, { active: false, observedAt: 1_250 }], 1_000, 250), false); + assert.equal(freezeEndedBeforeIdle([active, { active: false, observedAt: 1_260 }], 1_000, 250), false); + assert.equal(freezeEndedBeforeIdle([{ active: false, observedAt: 900 }, active], 1_000, 250), false); +}); + test("physical wheel smoke requires native window and document focus on a visible loaded page", () => { assert.equal(isBrowserSmokeWheelReady(ready), true); // The observed locked-display case has usable geometry but no actual window/document focus. diff --git a/tests/canvas-selection-gestures.test.mjs b/tests/canvas-selection-gestures.test.mjs index de18433f..ce12de33 100644 --- a/tests/canvas-selection-gestures.test.mjs +++ b/tests/canvas-selection-gestures.test.mjs @@ -11,7 +11,9 @@ import { canvasMarqueeRect, canvasPressIntent, canvasWorldRect, + filesLayerId, materialLayerId, + noteLayerId, parseCanvasLayerId, pastCanvasDragThreshold, @@ -23,7 +25,8 @@ const pointerNavigationPath = new URL("../src/renderer/src/features/workspace/us const workspacePath = new URL("../src/renderer/src/features/workspace/WorkspaceCanvas.tsx", import.meta.url); /** Every window kind, each named the way its own card root names it. */ -const everyLayerId = [terminalLayerId("a"), pluginLayerId("p"), browserLayerId, noteLayerId("n"), materialLayerId("m")]; +const everyLayerId = [terminalLayerId("a"), pluginLayerId("p"), browserLayerId, noteLayerId("n"), filesLayerId("f"), materialLayerId("m")]; + /** A primary press on empty canvas, overridden per test. */ function press(overrides = {}) { @@ -125,13 +128,17 @@ test("a layer id round-trips every window kind and nothing else parses", () => { [pluginLayerId("p-1"), { kind: "plugin", targetId: "p-1" }], [browserLayerId, { kind: "browser", targetId: null }], [noteLayerId("n-1"), { kind: "note", targetId: "n-1" }], + [filesLayerId("f-1"), { kind: "files", targetId: "f-1" }], [materialLayerId("m-1"), { kind: "material", targetId: "m-1" }] + ]; for (const [layerId, expected] of cases) { assert.deepEqual(parseCanvasLayerId(layerId), expected, `${layerId} must resolve to its own kind`); } assert.deepEqual(parseCanvasLayerId(terminalLayerId("s:1")), { kind: "terminal", targetId: "s:1" }); - for (const junk of ["", "terminal", "terminal:", "plugin:", "note:", "material:", ":a", "session:a", "browser:", "browser:first", "browser:second", "Browser"]) { + assert.deepEqual(parseCanvasLayerId(filesLayerId("f:1")), { kind: "files", targetId: "f:1" }); + for (const junk of ["", "terminal", "terminal:", "plugin:", "note:", "files:", "material:", ":a", "session:a", "browser:", "browser:first", "browser:second", "Browser", "Files"]) { + assert.equal(parseCanvasLayerId(junk), null, `"${junk}" is not a layer id`); } }); diff --git a/tests/canvas-widget-focus.test.mjs b/tests/canvas-widget-focus.test.mjs index b3daa3d0..b589e4fa 100644 --- a/tests/canvas-widget-focus.test.mjs +++ b/tests/canvas-widget-focus.test.mjs @@ -16,6 +16,17 @@ test("canvas widget ids are stable across each focusable input surface", () => { assert.equal(browserCanvasWidgetId, "browser"); }); +test("a files layer routes directional focus exactly like a note layer", () => { + const candidates = [ + { id: "terminal:left", bounds: { position: { x: 0, y: 0 }, size: { width: 200, height: 100 } } }, + { id: "note:n", bounds: { position: { x: 400, y: 0 }, size: { width: 200, height: 100 } } }, + { id: "files:f", bounds: { position: { x: 800, y: 0 }, size: { width: 200, height: 100 } } } + ]; + assert.equal(canvasWidgetInDirection(candidates, "terminal:left", "right", { x: 0, y: 0 }), "note:n"); + assert.equal(canvasWidgetInDirection(candidates, "note:n", "right", { x: 0, y: 0 }), "files:f"); + assert.equal(canvasWidgetInDirection(candidates, "files:f", "left", { x: 0, y: 0 }), "note:n"); +}); + test("only a click outside every widget clears logical input focus", () => { assert.equal(canvasWidgetFocusAfterClick("terminal:one", { isWidget: true, diff --git a/tests/file-access-service.test.mjs b/tests/file-access-service.test.mjs new file mode 100644 index 00000000..472b5b0d --- /dev/null +++ b/tests/file-access-service.test.mjs @@ -0,0 +1,493 @@ +import assert from "node:assert/strict"; +import { realpathSync } from "node:fs"; +import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import test from "node:test"; +import { FileAccessService } from "../src/main/services/FileAccessService.ts"; + +async function makeTempDir(prefix) { + return mkdtemp(join(tmpdir(), prefix)); +} + +function createService({ cwd, limits } = {}) { + return new FileAccessService({ + resolveSessionCwd: (sessionId) => (sessionId === "session-1" ? cwd : undefined), + limits + }); +} + +test("registers a session root from the injected cwd lookup and hides the absolute path", async () => { + const root = await makeTempDir("canvastty-files-session-"); + try { + const service = createService({ cwd: root }); + const descriptor = service.registerSessionRoot("session-1"); + assert.ok(descriptor); + assert.equal(descriptor.rootType, "session"); + assert.equal(descriptor.available, true); + assert.equal(descriptor.label, basename(root)); + assert.equal("path" in descriptor, false); + assert.equal(JSON.stringify(descriptor).includes(root), false); + assert.deepEqual(service.listRoots(), [descriptor]); + assert.equal(service.registerSessionRoot("missing-session"), null); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("registers a folder root and resolves it through registerRoot", async () => { + const root = await makeTempDir("canvastty-files-folder-"); + try { + const service = createService(); + const descriptor = service.registerFolderRoot(root); + assert.equal(descriptor.rootType, "folder"); + assert.equal(descriptor.available, true); + assert.equal(descriptor.label, basename(root)); + + const viaReference = await service.registerRoot({ rootType: "folder" }); + assert.equal(viaReference, null); + + assert.throws(() => service.registerFolderRoot(join(root, "nope")), /unavailable/); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("exposes a canonical folder path only on folder root descriptors", async () => { + const root = await makeTempDir("canvastty-files-descriptor-path-"); + try { + const service = createService({ cwd: root }); + const session = service.registerSessionRoot("session-1"); + assert.equal("folderPath" in session, false); + assert.equal(JSON.stringify(session).includes(root), false); + + const folder = service.registerFolderRoot(root); + assert.equal(folder.folderPath, realpathSync(root)); + assert.equal(service.listRoots().find((entry) => entry.rootId === folder.rootId)?.folderPath, realpathSync(root)); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("restores a persisted folder root by path through registerRoot", async () => { + const root = await makeTempDir("canvastty-files-restore-"); + try { + const service = createService(); + const descriptor = await service.registerRoot({ rootType: "folder", folderPath: root }); + assert.ok(descriptor); + assert.equal(descriptor.rootType, "folder"); + assert.equal(descriptor.available, true); + assert.equal(descriptor.label, basename(root)); + + const missing = await service.registerRoot({ rootType: "folder", folderPath: join(root, "nope") }); + assert.equal(missing, null); + + const pathless = await service.registerRoot({ rootType: "folder" }); + assert.equal(pathless, null); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("rejects absolute paths and parent traversal on read", async () => { + const root = await makeTempDir("canvastty-files-traversal-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "inside.txt"), "inside"); + + assert.deepEqual(await service.read(rootId, "/etc/passwd"), { + kind: "unsupported", + reason: "not-permitted" + }); + assert.deepEqual(await service.read(rootId, "../escape.txt"), { + kind: "unsupported", + reason: "not-permitted" + }); + assert.deepEqual(await service.read(rootId, "sub/../../escape.txt"), { + kind: "unsupported", + reason: "not-permitted" + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("rejects symlinks whose real target escapes the root", async () => { + const root = await makeTempDir("canvastty-files-symlink-root-"); + const outside = await makeTempDir("canvastty-files-symlink-outside-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(outside, "secret.txt"), "top secret"); + await symlink(join(outside, "secret.txt"), join(root, "escape.txt")); + await symlink(outside, join(root, "escape-dir")); + + assert.deepEqual(await service.read(rootId, "escape.txt"), { + kind: "unsupported", + reason: "not-permitted" + }); + assert.deepEqual(await service.read(rootId, "escape-dir/secret.txt"), { + kind: "unsupported", + reason: "not-permitted" + }); + } finally { + await rm(root, { recursive: true, force: true }); + await rm(outside, { recursive: true, force: true }); + } +}); + +test("rejects non-regular files such as directories", async () => { + const root = await makeTempDir("canvastty-files-nonregular-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await mkdir(join(root, "subdir")); + assert.deepEqual(await service.read(rootId, "subdir"), { + kind: "unsupported", + reason: "not-permitted" + }); + assert.deepEqual(await service.read(rootId, ""), { + kind: "unsupported", + reason: "not-permitted" + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("lists immediate entries with directories before files and file sizes", async () => { + const root = await makeTempDir("canvastty-files-list-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await mkdir(join(root, "gamma")); + await mkdir(join(root, "zeta")); + await mkdir(join(root, "nested")); + await writeFile(join(root, "alpha.txt"), "hello"); + await writeFile(join(root, "beta.txt"), "hi"); + await writeFile(join(root, "nested", "child.txt"), "child"); + + const entries = await service.list(rootId, ""); + assert.deepEqual(entries, [ + { name: "gamma", relativePath: "gamma", kind: "directory", size: null }, + { name: "nested", relativePath: "nested", kind: "directory", size: null }, + { name: "zeta", relativePath: "zeta", kind: "directory", size: null }, + { name: "alpha.txt", relativePath: "alpha.txt", kind: "file", size: 5 }, + { name: "beta.txt", relativePath: "beta.txt", kind: "file", size: 2 } + ]); + + const nested = await service.list(rootId, "nested"); + assert.deepEqual(nested, [ + { name: "child.txt", relativePath: "nested/child.txt", kind: "file", size: 5 } + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("caps directory listings at maxDirectoryEntries", async () => { + const root = await makeTempDir("canvastty-files-list-cap-"); + try { + const service = createService({ cwd: root, limits: { maxDirectoryEntries: 2 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "a.txt"), "a"); + await writeFile(join(root, "b.txt"), "b"); + await writeFile(join(root, "c.txt"), "c"); + assert.equal((await service.list(rootId, "")).length, 2); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("throws when listing a missing directory or a bad path", async () => { + const root = await makeTempDir("canvastty-files-list-bad-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await assert.rejects(service.list(rootId, "missing"), /unavailable/); + await assert.rejects(service.list(rootId, "../"), /outside|unavailable/); + await assert.rejects(service.list(rootId, "/etc"), /outside|unavailable/); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reads a small UTF-8 text file", async () => { + const root = await makeTempDir("canvastty-files-text-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "notes.txt"), "hello world"); + assert.deepEqual(await service.read(rootId, "notes.txt"), { + kind: "text", + content: "hello world", + truncated: false, + size: 11 + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reads UTF-8 text whose final character is multi-byte", async () => { + const root = await makeTempDir("canvastty-files-text-multibyte-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "README.md"), "# Привет"); + const cyrillic = await service.read(rootId, "README.md"); + assert.equal(cyrillic.kind, "text"); + assert.equal(cyrillic.content, "# Привет"); + await writeFile(join(root, "emoji.md"), "done 😀"); + const emoji = await service.read(rootId, "emoji.md"); + assert.equal(emoji.kind, "text"); + assert.equal(emoji.content, "done 😀"); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("tolerates a bounded read that splits the final multi-byte code point", async () => { + const root = await makeTempDir("canvastty-files-text-split-"); + try { + const service = createService({ cwd: root, limits: { maxTextBytes: 3 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "emoji.txt"), "😀"); + const result = await service.read(rootId, "emoji.txt"); + assert.equal(result.kind, "text"); + assert.equal(result.truncated, true); + assert.equal(result.size, 4); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("truncates oversized text and reports the observed total size", async () => { + const root = await makeTempDir("canvastty-files-text-trunc-"); + try { + const service = createService({ cwd: root, limits: { maxTextBytes: 4 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "long.txt"), "abcdefghij"); + const result = await service.read(rootId, "long.txt"); + assert.equal(result.kind, "text"); + assert.equal(result.content, "abcd"); + assert.equal(result.truncated, true); + assert.equal(result.size, 10); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reports binary content as unsupported without returning bytes", async () => { + const root = await makeTempDir("canvastty-files-binary-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "data.bin"), Buffer.from([0x68, 0x00, 0x69])); + assert.deepEqual(await service.read(rootId, "data.bin"), { + kind: "unsupported", + reason: "binary" + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reports NUL-free binary content as unsupported", async () => { + const root = await makeTempDir("canvastty-files-binary-nonul-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "latin.bin"), Buffer.from([0xff, 0xfe, 0xfd, 0xfc, 0xfb])); + assert.deepEqual(await service.read(rootId, "latin.bin"), { + kind: "unsupported", + reason: "binary" + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("returns an allow-listed image as a base64 data URL", async () => { + const root = await makeTempDir("canvastty-files-image-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + await writeFile(join(root, "pic.png"), bytes); + + const result = await service.read(rootId, "pic.png"); + assert.equal(result.kind, "image"); + assert.equal(result.mediaType, "image/png"); + assert.equal(result.size, bytes.byteLength); + assert.match(result.dataUrl, /^data:image\/png;base64,/); + assert.deepEqual( + Buffer.from(result.dataUrl.split(",")[1], "base64"), + bytes + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reports an oversized image as too large", async () => { + const root = await makeTempDir("canvastty-files-image-large-"); + try { + const service = createService({ cwd: root, limits: { maxImageBytes: 2 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "big.png"), Buffer.from([1, 2, 3])); + assert.deepEqual(await service.read(rootId, "big.png"), { + kind: "too-large", + reason: "too-large", + size: 3 + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reports a disallowed non-image binary type as unsupported binary", async () => { + const root = await makeTempDir("canvastty-files-disallowed-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "archive.zip"), Buffer.from([0x50, 0x4b, 0x00, 0x04])); + assert.deepEqual(await service.read(rootId, "archive.zip"), { + kind: "unsupported", + reason: "binary" + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("searches file names case-insensitively and returns relative paths", async () => { + const root = await makeTempDir("canvastty-files-search-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await mkdir(join(root, "src")); + await writeFile(join(root, "README.md"), "readme"); + await writeFile(join(root, "src", "Widget.tsx"), "widget"); + await writeFile(join(root, "src", "other.ts"), "other"); + + assert.deepEqual(await service.search(rootId, "widget"), { + relativePaths: ["src/Widget.tsx"], + truncated: false + }); + assert.deepEqual(await service.search(rootId, "missing"), { + relativePaths: [], + truncated: false + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("skips heavy directories and symlinked directories during search", async () => { + const root = await makeTempDir("canvastty-files-search-skip-"); + const outside = await makeTempDir("canvastty-files-search-skip-out-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await mkdir(join(root, "node_modules"), { recursive: true }); + await mkdir(join(root, ".git"), { recursive: true }); + await mkdir(join(root, "dist"), { recursive: true }); + await mkdir(join(root, "src"), { recursive: true }); + await writeFile(join(root, "node_modules", "find-me.txt"), "x"); + await writeFile(join(root, ".git", "find-me.txt"), "x"); + await writeFile(join(root, "dist", "find-me.txt"), "x"); + await writeFile(join(root, "src", "find-me.txt"), "x"); + await writeFile(join(outside, "find-me.txt"), "x"); + await symlink(outside, join(root, "linked")); + + assert.deepEqual(await service.search(rootId, "find-me"), { + relativePaths: ["src/find-me.txt"], + truncated: false + }); + } finally { + await rm(root, { recursive: true, force: true }); + await rm(outside, { recursive: true, force: true }); + } +}); + +test("does not search beyond maxSearchDepth", async () => { + const root = await makeTempDir("canvastty-files-search-depth-"); + try { + const service = createService({ cwd: root, limits: { maxSearchDepth: 1 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await mkdir(join(root, "a", "b"), { recursive: true }); + await writeFile(join(root, "match.txt"), "x"); + await writeFile(join(root, "a", "match.txt"), "x"); + await writeFile(join(root, "a", "b", "match.txt"), "x"); + + assert.deepEqual(await service.search(rootId, "match"), { + relativePaths: ["a/match.txt", "match.txt"], + truncated: false + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("caps search results and marks the result truncated", async () => { + const root = await makeTempDir("canvastty-files-search-cap-"); + try { + const service = createService({ cwd: root, limits: { maxSearchResults: 1 } }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "match-one.txt"), "1"); + await writeFile(join(root, "match-two.txt"), "2"); + const result = await service.search(rootId, "match"); + assert.equal(result.relativePaths.length, 1); + assert.equal(result.truncated, true); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("releases a root and rejects later operations on it", async () => { + const root = await makeTempDir("canvastty-files-release-"); + try { + const service = createService({ cwd: root }); + const { rootId } = service.registerSessionRoot("session-1"); + await writeFile(join(root, "notes.txt"), "hello"); + + service.releaseRoot(rootId); + assert.deepEqual(service.listRoots(), []); + assert.deepEqual(await service.read(rootId, "notes.txt"), { + kind: "unsupported", + reason: "not-permitted" + }); + await assert.rejects(service.list(rootId, ""), /not registered/); + await assert.rejects(service.search(rootId, "notes"), /not registered/); + assert.equal(service.revalidateRoot(rootId), false); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("reports a missing root as unavailable and fails re-validation", async () => { + const root = await makeTempDir("canvastty-files-missing-"); + const service = createService(); + const descriptor = service.registerFolderRoot(root); + await rm(root, { recursive: true, force: true }); + + assert.deepEqual(await service.read(descriptor.rootId, "anything.txt"), { + kind: "unsupported", + reason: "unavailable" + }); + assert.equal(service.revalidateRoot(descriptor.rootId), false); + assert.equal(service.listRoots()[0].available, false); +}); + +test("rejects list and search on a root that failed re-validation", async () => { + const root = await makeTempDir("canvastty-files-revalidate-"); + const service = createService(); + const descriptor = service.registerFolderRoot(root); + await rm(root, { recursive: true, force: true }); + assert.equal(service.revalidateRoot(descriptor.rootId), false); + + await assert.rejects(service.list(descriptor.rootId, ""), /unavailable/); + await assert.rejects(service.search(descriptor.rootId, "anything"), /unavailable/); +}); diff --git a/tests/file-browser-selectors.test.mjs b/tests/file-browser-selectors.test.mjs new file mode 100644 index 00000000..596e341e --- /dev/null +++ b/tests/file-browser-selectors.test.mjs @@ -0,0 +1,299 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + buildFileTree, + flattenFileTree, + matchQuickOpen, + orderFileEntries, + toggleExpandedFolder +} from "../src/renderer/src/features/files/fileTree.ts"; +import { + classifyFileRead, + formatFileSize, + resolveActiveFileRead +} from "../src/renderer/src/features/files/fileViewer.ts"; + +const dir = (name, relativePath = name) => ({ name, relativePath, kind: "directory", size: null }); +const file = (name, relativePath = name, size = 10) => ({ name, relativePath, kind: "file", size }); + +test("orders directories before files and names case-insensitively", () => { + assert.deepEqual( + orderFileEntries([ + file("b.txt"), + dir("Zeta"), + file("a.txt"), + dir("alpha") + ]).map((entry) => `${entry.kind}:${entry.name}`), + ["directory:alpha", "directory:Zeta", "file:a.txt", "file:b.txt"] + ); +}); + +test("orders the same name by exact case as a stable tiebreak", () => { + assert.deepEqual( + orderFileEntries([file("Beta"), file("alpha"), file("Alpha")]).map((entry) => entry.name), + ["Alpha", "alpha", "Beta"] + ); +}); + +test("merges loaded directory children without mutating unloaded directories", () => { + const tree = buildFileTree( + [file("readme.md"), dir("src"), dir(".git")], + { + src: [file("main.ts", "src/main.ts"), dir("components", "src/components")], + "src/components": [file("Button.tsx", "src/components/Button.tsx")] + } + ); + + assert.deepEqual(tree, [ + { entry: dir(".git"), children: null }, + { + entry: dir("src"), + children: [ + { + entry: dir("components", "src/components"), + children: [ + { entry: file("Button.tsx", "src/components/Button.tsx"), children: null } + ] + }, + { entry: file("main.ts", "src/main.ts"), children: null } + ] + }, + { entry: file("readme.md"), children: null } + ]); +}); + +test("a loaded but empty directory keeps an empty children array", () => { + assert.deepEqual(buildFileTree([dir("empty")], { empty: [] }), [ + { entry: dir("empty"), children: [] } + ]); +}); + +test("toggling an expanded folder adds, removes, and never mutates the input", () => { + const original = ["src", "docs"]; + assert.deepEqual(toggleExpandedFolder(original, "src/components"), ["src", "docs", "src/components"]); + assert.deepEqual(toggleExpandedFolder(original, "src"), ["docs"]); + assert.deepEqual(original, ["src", "docs"]); +}); + +test("flattens only expanded, loaded directories with correct depth", () => { + const tree = buildFileTree( + [file("readme.md"), dir("src"), dir(".git")], + { + src: [file("main.ts", "src/main.ts"), dir("components", "src/components")], + "src/components": [file("Button.tsx", "src/components/Button.tsx")] + } + ); + + assert.deepEqual( + flattenFileTree(tree, ["src"]).map((row) => `${row.depth}:${row.entry.name}`), + ["0:.git", "0:src", "1:components", "1:main.ts", "0:readme.md"] + ); + + assert.deepEqual( + flattenFileTree(tree, ["src", "src/components"]).map((row) => `${row.depth}:${row.entry.name}`), + ["0:.git", "0:src", "1:components", "2:Button.tsx", "1:main.ts", "0:readme.md"] + ); +}); + +test("expanded directories without loaded children show no child rows", () => { + const tree = buildFileTree([dir(".git")], {}); + const rows = flattenFileTree(tree, [".git"]); + assert.equal(rows.length, 1); + assert.equal(rows[0].isExpanded, true); + assert.equal(rows[0].hasLoadedChildren, false); + assert.equal(rows[0].depth, 0); +}); + +test("quick open ranks exact, prefix, substring, and path-only matches", () => { + assert.deepEqual( + matchQuickOpen(["main", "main.ts", "src/main"], "main"), + ["main", "src/main", "main.ts"] + ); + assert.deepEqual( + matchQuickOpen(["src/util/main-helper.ts", "src/main.ts"], "util"), + ["src/util/main-helper.ts"] + ); +}); + +test("quick open is case-insensitive and rejects empty or unmatched queries", () => { + assert.deepEqual(matchQuickOpen(["src/components/Button.tsx"], "BUTTON"), ["src/components/Button.tsx"]); + assert.deepEqual(matchQuickOpen(["a.txt"], " "), []); + assert.deepEqual(matchQuickOpen(["a.txt"], "zzz"), []); +}); + +test("formats byte sizes with powers of 1024", () => { + assert.equal(formatFileSize(0), "0 B"); + assert.equal(formatFileSize(-5), "0 B"); + assert.equal(formatFileSize(900), "900 B"); + assert.equal(formatFileSize(1024), "1 KB"); + assert.equal(formatFileSize(1536), "1.5 KB"); + assert.equal(formatFileSize(1048576), "1 MB"); + assert.equal(formatFileSize(10485760), "10 MB"); +}); + +test("classifies a truncated text read with a size label", () => { + assert.deepEqual( + classifyFileRead({ kind: "text", content: "hello", truncated: true, size: 2048 }), + { + kind: "text", + renderKind: "plain", + content: "hello", + truncated: true, + size: 2048, + sizeLabel: "2 KB", + language: null, + highlightable: true, + richDisabled: false + } + ); +}); + +test("classifies an image read with its data URL and media type", () => { + assert.deepEqual( + classifyFileRead({ + kind: "image", + mediaType: "image/png", + dataUrl: "data:image/png;base64,AAAA", + size: 3072 + }), + { + kind: "image", + renderKind: "image", + mediaType: "image/png", + dataUrl: "data:image/png;base64,AAAA", + size: 3072, + sizeLabel: "3 KB" + } + ); +}); + +test("classifies unsupported reads with a reason label and carries no bytes", () => { + assert.deepEqual( + classifyFileRead({ kind: "unsupported", reason: "binary" }), + { kind: "unsupported", renderKind: "unsupported", reason: "binary", reasonLabel: "Binary file" } + ); + assert.deepEqual( + classifyFileRead({ kind: "unsupported", reason: "not-permitted" }), + { + kind: "unsupported", + renderKind: "unsupported", + reason: "not-permitted", + reasonLabel: "File type not permitted" + } + ); + assert.deepEqual( + classifyFileRead({ kind: "unsupported", reason: "unavailable" }), + { + kind: "unsupported", + renderKind: "unsupported", + reason: "unavailable", + reasonLabel: "File unavailable" + } + ); +}); + +test("classifies a too-large read with its size label", () => { + assert.deepEqual( + classifyFileRead({ kind: "too-large", reason: "too-large", size: 10485760 }), + { + kind: "too-large", + renderKind: "too-large", + reason: "too-large", + size: 10485760, + sizeLabel: "10 MB" + } + ); +}); + +test("classifies a small recognized-language file as highlightable code", () => { + assert.deepEqual( + classifyFileRead({ kind: "text", content: "const x = 1;", truncated: false, size: 12 }, "src/a.ts"), + { + kind: "text", + renderKind: "code", + content: "const x = 1;", + truncated: false, + size: 12, + sizeLabel: "12 B", + language: "typescript", + highlightable: true, + richDisabled: false + } + ); +}); + +test("classifies a markdown file as markdown when under the size threshold", () => { + const model = classifyFileRead( + { kind: "text", content: "# Hi", truncated: false, size: 4 }, + "docs/readme.md" + ); + assert.equal(model.renderKind, "markdown"); + assert.equal(model.language, null); + assert.equal(model.highlightable, true); + assert.equal(model.richDisabled, false); +}); + +test("classifies unknown and extensionless text as plain without a note", () => { + for (const path of ["notes.txt", "LICENSE", ""]) { + const model = classifyFileRead( + { kind: "text", content: "hello", truncated: false, size: 5 }, + path + ); + assert.equal(model.renderKind, "plain"); + assert.equal(model.language, null); + assert.equal(model.highlightable, true); + assert.equal(model.richDisabled, false); + } +}); + +test("disables rich rendering for a recognized language above the size threshold", () => { + const size = 300 * 1024; + const model = classifyFileRead( + { kind: "text", content: "const x = 1;", truncated: false, size }, + "src/big.ts" + ); + assert.equal(model.renderKind, "plain"); + assert.equal(model.language, "typescript"); + assert.equal(model.highlightable, false); + assert.equal(model.richDisabled, true); +}); + +test("disables rich rendering for markdown above the size threshold", () => { + const size = 300 * 1024; + const model = classifyFileRead( + { kind: "text", content: "# Big", truncated: false, size }, + "docs/big.md" + ); + assert.equal(model.renderKind, "plain"); + assert.equal(model.language, null); + assert.equal(model.highlightable, false); + assert.equal(model.richDisabled, true); +}); + +test("resolves a read only for the exact path it was bound to", () => { + const binding = { + relativePath: "src/main.ts", + result: { kind: "text", content: "x", truncated: false, size: 1 } + }; + assert.deepEqual(resolveActiveFileRead("src/main.ts", binding), { + kind: "text", + renderKind: "code", + content: "x", + truncated: false, + size: 1, + sizeLabel: "1 B", + language: "typescript", + highlightable: true, + richDisabled: false + }); +}); + +test("a stale or missing read binding resolves to null so no old content can render", () => { + const binding = { + relativePath: "old.txt", + result: { kind: "text", content: "old", truncated: false, size: 3 } + }; + assert.equal(resolveActiveFileRead("new.txt", binding), null); + assert.equal(resolveActiveFileRead(null, binding), null); + assert.equal(resolveActiveFileRead("old.txt", null), null); +}); diff --git a/tests/file-card-restore.test.mjs b/tests/file-card-restore.test.mjs new file mode 100644 index 00000000..5e817e8e --- /dev/null +++ b/tests/file-card-restore.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + fileCardToRootReference, + isFileRootUsable +} from "../src/renderer/src/features/files/fileCardRestore.ts"; + +test("maps a persisted session card to a session root reference", () => { + assert.deepEqual( + fileCardToRootReference({ root: { rootType: "session", sessionId: "session-1" }, folderPath: null }), + { rootType: "session", sessionId: "session-1" } + ); +}); + +test("maps a folder card to a folder reference and prefers the persisted folder path", () => { + assert.deepEqual( + fileCardToRootReference({ root: { rootType: "folder" }, folderPath: "/tmp/canvastty-project" }), + { rootType: "folder", folderPath: "/tmp/canvastty-project" } + ); + // Falls back to a path embedded in the root reference when the card field is empty. + assert.deepEqual( + fileCardToRootReference({ root: { rootType: "folder", folderPath: "/tmp/notes" }, folderPath: null }), + { rootType: "folder", folderPath: "/tmp/notes" } + ); +}); + +test("a folder card that never picked a folder has no restorable root", () => { + assert.equal(fileCardToRootReference({ root: { rootType: "folder" }, folderPath: null }), null); + assert.equal( + fileCardToRootReference({ root: { rootType: "folder", folderPath: null }, folderPath: null }), + null + ); +}); + +test("only a non-null, explicitly available descriptor is usable", () => { + assert.equal(isFileRootUsable(null), false); + assert.equal( + isFileRootUsable({ rootId: "r", label: "root", available: false, rootType: "folder" }), + false + ); + assert.equal( + isFileRootUsable({ rootId: "r", label: "root", available: true, rootType: "folder" }), + true + ); + assert.equal( + isFileRootUsable({ rootId: "r", label: "root", available: true, rootType: "session" }), + true + ); +}); diff --git a/tests/file-code-highlight.test.mjs b/tests/file-code-highlight.test.mjs new file mode 100644 index 00000000..5099325a --- /dev/null +++ b/tests/file-code-highlight.test.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { + highlightCode, + isRegisteredLanguage +} from "../src/renderer/src/features/files/codeHighlight.ts"; +import { COMMON_LANGUAGES } from "../src/renderer/src/features/files/fileLanguage.ts"; + +test("every language id produced by detection is registered with lowlight", () => { + for (const id of COMMON_LANGUAGES) { + assert.equal(isRegisteredLanguage(id), true, `expected ${id} to be registered`); + } +}); + +test("highlights recognized languages and returns null for unknown ones", () => { + const typescript = highlightCode("const x: number = 1", "typescript"); + assert.notEqual(typescript, null); + assert.match(renderToStaticMarkup(typescript), /hljs-keyword/); + + assert.notEqual(highlightCode("{}", "json"), null); + + assert.equal(highlightCode("const x = 1", "not-a-real-language"), null); + assert.equal(highlightCode("x", null), null); +}); + +test("unknown and empty languages are reported as unregistered", () => { + assert.equal(isRegisteredLanguage("definitely-not-a-language"), false); + assert.equal(isRegisteredLanguage(null), false); + assert.equal(isRegisteredLanguage(undefined), false); + assert.equal(isRegisteredLanguage(""), false); +}); diff --git a/tests/file-language.test.mjs b/tests/file-language.test.mjs new file mode 100644 index 00000000..19c0534b --- /dev/null +++ b/tests/file-language.test.mjs @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + COMMON_LANGUAGES, + MARKDOWN_EXTENSIONS, + detectFileLanguage, + isMarkdownPath +} from "../src/renderer/src/features/files/fileLanguage.ts"; + +test("detects known extensions case-insensitively", () => { + assert.equal(detectFileLanguage("component.tsx"), "typescript"); + assert.equal(detectFileLanguage("script.py"), "python"); + assert.equal(detectFileLanguage("data.json"), "json"); + assert.equal(detectFileLanguage("styles.css"), "css"); + assert.equal(detectFileLanguage("page.html"), "xml"); + assert.equal(detectFileLanguage("config.yaml"), "yaml"); + assert.equal(detectFileLanguage("run.sh"), "bash"); +}); + +test("strips directories before reading the extension", () => { + assert.equal(detectFileLanguage("src/components/Button.tsx"), "typescript"); + assert.equal(detectFileLanguage("a.dir/notes.md"), null); +}); + +test("recognizes markdown extensions and excludes them from language detection", () => { + assert.equal(isMarkdownPath("notes.md"), true); + assert.equal(isMarkdownPath("README.markdown"), true); + assert.equal(isMarkdownPath("docs/guide.mdown"), true); + assert.equal(isMarkdownPath("docs/guide.mkd"), true); + assert.equal(detectFileLanguage("notes.md"), null); + assert.equal(detectFileLanguage("README.markdown"), null); +}); + +test("detection is case-insensitive on the extension", () => { + assert.equal(detectFileLanguage("MAIN.TS"), "typescript"); + assert.equal(isMarkdownPath("readme.MD"), true); + assert.equal(detectFileLanguage("readme.MD"), null); + assert.equal(detectFileLanguage("Data.JSON"), "json"); +}); + +test("unknown or missing extensions return null", () => { + assert.equal(detectFileLanguage("archive.xyz"), null); + assert.equal(detectFileLanguage("Makefile"), null); + assert.equal(detectFileLanguage("LICENSE"), null); + assert.equal(detectFileLanguage("trailingdot."), null); + assert.equal(isMarkdownPath("archive.xyz"), false); + assert.equal(isMarkdownPath("Makefile"), false); +}); + +test("COMMON_LANGUAGES is sorted, de-duplicated, and covers every mapped language", () => { + const produced = new Set(); + const samples = [ + "a.ts", + "a.tsx", + "a.js", + "a.jsx", + "a.mjs", + "a.cjs", + "a.py", + "a.json", + "a.jsonc", + "a.css", + "a.scss", + "a.html", + "a.htm", + "a.xml", + "a.svg", + "a.yaml", + "a.yml", + "a.sh", + "a.bash", + "a.zsh", + "a.go", + "a.rs", + "a.java", + "a.c", + "a.h", + "a.cpp", + "a.cc", + "a.cxx", + "a.hpp", + "a.cs", + "a.rb", + "a.php", + "a.sql", + "a.toml", + "a.kt", + "a.swift", + "a.lua", + "a.r", + "a.pl", + "a.diff", + "a.patch", + "a.makefile" + ]; + for (const sample of samples) { + const id = detectFileLanguage(sample); + assert.ok(id !== null, `expected a language id for ${sample}`); + produced.add(id); + } + for (const id of produced) { + assert.ok(COMMON_LANGUAGES.includes(id), `COMMON_LANGUAGES missing ${id}`); + } + assert.equal(COMMON_LANGUAGES.includes("markdown"), false); + assert.equal(new Set(COMMON_LANGUAGES).size, COMMON_LANGUAGES.length); + assert.deepEqual(COMMON_LANGUAGES, [...COMMON_LANGUAGES].sort()); + assert.equal(MARKDOWN_EXTENSIONS.length, 4); +}); diff --git a/tests/file-links.test.mjs b/tests/file-links.test.mjs new file mode 100644 index 00000000..7cac5d7a --- /dev/null +++ b/tests/file-links.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + isEmbeddedImageSource, + isOpenableLink +} from "../src/renderer/src/features/files/fileLinks.ts"; + +test("isOpenableLink accepts absolute http and https URLs", () => { + assert.equal(isOpenableLink("https://example.com"), true); + assert.equal(isOpenableLink("http://example.com/path?q=1#frag"), true); +}); + +test("isOpenableLink rejects non-http(s) and non-absolute links", () => { + assert.equal(isOpenableLink("javascript:alert(1)"), false); + assert.equal(isOpenableLink("mailto:user@example.com"), false); + assert.equal(isOpenableLink("#anchor"), false); + assert.equal(isOpenableLink("a/b"), false); + assert.equal(isOpenableLink("data:text/html,hi"), false); + assert.equal(isOpenableLink("ftp://example.com"), false); + assert.equal(isOpenableLink(null), false); + assert.equal(isOpenableLink(undefined), false); + assert.equal(isOpenableLink("http://"), false); +}); + +test("isEmbeddedImageSource accepts only data:image sources", () => { + assert.equal(isEmbeddedImageSource("data:image/png;base64,AAAA"), true); + assert.equal(isEmbeddedImageSource("https://example.com/x.png"), false); + assert.equal(isEmbeddedImageSource("relative/x.png"), false); + assert.equal(isEmbeddedImageSource(""), false); + assert.equal(isEmbeddedImageSource(null), false); +}); diff --git a/tests/file-rendering-safety.test.mjs b/tests/file-rendering-safety.test.mjs new file mode 100644 index 00000000..6803e8d7 --- /dev/null +++ b/tests/file-rendering-safety.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { createMarkdownElement } from "../src/renderer/src/features/files/markdownPipeline.ts"; + +test("strips a script element from rendered markdown", () => { + const html = renderToStaticMarkup(createMarkdownElement("before\n\n\n\nafter")); + assert.ok(!html.includes(" { + const html = renderToStaticMarkup(createMarkdownElement('')); + assert.ok(!/onerror/i.test(html), `expected no onerror in: ${html}`); +}); + +test("renders a javascript: link without a javascript: href", () => { + const html = renderToStaticMarkup(createMarkdownElement("[link](javascript:alert(1))")); + assert.ok(!/javascript:/i.test(html), `expected no javascript: in: ${html}`); +}); + +test("renders a normal https link with its href", () => { + const html = renderToStaticMarkup(createMarkdownElement("[site](https://example.com)")); + assert.match(html, /https:\/\/example\.com/); +}); + +test("keeps a data: image and drops a remote image", () => { + const remote = renderToStaticMarkup(createMarkdownElement("![remote](https://evil.example/x.png)")); + assert.ok(!remote.includes("evil.example"), `remote host leaked: ${remote}`); + assert.ok(!remote.includes('src="https://'), `remote src leaked: ${remote}`); + + const embedded = renderToStaticMarkup( + createMarkdownElement("![d](data:image/png;base64,AAAA)") + ); + assert.match(embedded, /data:image\/png/); +}); diff --git a/tests/files-ipc-security.test.mjs b/tests/files-ipc-security.test.mjs new file mode 100644 index 00000000..ac52bee0 --- /dev/null +++ b/tests/files-ipc-security.test.mjs @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import { realpathSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { FileAccessService } from "../src/main/services/FileAccessService.ts"; + +const ipcPath = new URL("../src/main/ipc/registerIpc.ts", import.meta.url); + +const FILE_CHANNELS = [ + "filesListRoots", + "filesRegisterRoot", + "filesOpenFolder", + "filesList", + "filesRead", + "filesSearch", + "filesCloseRoot" +]; + +test("every files IPC handler validates the trusted main renderer", async () => { + const source = await readFile(ipcPath, "utf8"); + + assert.match(source, /function assertMainRenderer/); + assert.match(source, /event\.sender !== expected\.webContents/); + assert.match(source, /event\.senderFrame !== expected\.webContents\.mainFrame/); + + for (const channel of FILE_CHANNELS) { + const match = new RegExp(`IPC\\.${channel}\\b`).exec(source); + assert.ok(match, `${channel} handler is missing`); + const handler = source.slice(match.index, match.index + 600); + assert.match(handler, /assertMainRenderer\(event, getMainWindow\)/, `${channel} must validate its sender`); + } +}); + +test("list, read, and search payloads expose no absolute root path", async () => { + const root = await mkdtemp(join(tmpdir(), "canvastty-files-payload-")); + try { + const service = new FileAccessService({ + resolveSessionCwd: (sessionId) => (sessionId === "session-1" ? root : undefined) + }); + const descriptor = service.registerSessionRoot("session-1"); + await writeFile(join(root, "notes.txt"), "hello"); + + const payloads = { + list: await service.list(descriptor.rootId, ""), + read: await service.read(descriptor.rootId, "notes.txt"), + search: await service.search(descriptor.rootId, "notes") + }; + + for (const [name, payload] of Object.entries(payloads)) { + const serialized = JSON.stringify(payload); + assert.equal(serialized.includes(root), false, `${name} must not embed the absolute root path`); + } + + assert.match(JSON.stringify(payloads.list), /"relativePath"/); + assert.match(JSON.stringify(payloads.search), /"relativePaths"/); + // Session descriptors never leak the resolved cwd. + assert.equal("folderPath" in descriptor, false); + assert.equal(JSON.stringify(descriptor).includes(root), false); + assert.doesNotMatch(JSON.stringify(service.listRoots()), /"folderPath"/); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("folder descriptors carry the canonical folder path while list/read/search stay relative", async () => { + const root = await mkdtemp(join(tmpdir(), "canvastty-files-folder-payload-")); + try { + const service = new FileAccessService({ resolveSessionCwd: () => undefined }); + const descriptor = service.registerFolderRoot(root); + await writeFile(join(root, "notes.txt"), "hello"); + + // The absolute folder path is intentionally allowed on folder descriptors so + // that restore can re-register the root after relaunch. + assert.equal(descriptor.folderPath, realpathSync(root)); + assert.equal(service.listRoots()[0]?.folderPath, realpathSync(root)); + + const payloads = [ + await service.list(descriptor.rootId, ""), + await service.read(descriptor.rootId, "notes.txt"), + await service.search(descriptor.rootId, "notes") + ]; + for (const payload of payloads) { + assert.equal(JSON.stringify(payload).includes(root), false, "operation payload must not embed the absolute root path"); + } + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/tests/renderer-bundle-splitting.test.mjs b/tests/renderer-bundle-splitting.test.mjs index 4aeee5f8..91a34d32 100644 --- a/tests/renderer-bundle-splitting.test.mjs +++ b/tests/renderer-bundle-splitting.test.mjs @@ -77,9 +77,10 @@ test("Settings and the launch/link dialogs are split out of the app's static imp assert.ok(settingsBytes > 100_000, `SettingsPanel chunk should carry real weight, got ${settingsBytes} bytes`); // Before this change, App.tsx bundled to a single ~1.6 MB chunk with no on-demand chunk for - // Settings at all. First-paint code must stay near that size; the materials UI loads on demand. + // Settings, materials, and Files have on-demand UI, so first-paint code stays bounded. assert.ok( eagerBytes < 1_700_000, `code statically reachable from App.js should stay bounded now that heavy UI loads on demand, got ${eagerBytes} bytes` + ); });