diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b22b2d..7b79f4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,5 +16,49 @@ jobs: - run: bun install --frozen-lockfile - run: bun test - run: bunx tsc -p . - - run: sh -n install.sh + - run: sh -n install.sh && bash -n install.sh - run: bun build --compile src/main.ts --outfile dist/hqterm && ./dist/hqterm --version + + desktop: + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm ci + - run: npm test + - name: Build AppImage + run: npm run dist + - name: Smoke test under Xvfb (window, PTYs, images, split, layout restore) + run: | + ls -la dist + app="$(ls dist/*.AppImage | head -n1)" + smoke() { + HQTERM_DESKTOP_SMOKE=10000 HQTERM_DESKTOP_SMOKE_SHOT="$PWD/$1" XDG_CONFIG_HOME="$RUNNER_TEMP/cfg" \ + xvfb-run -a -s "-screen 0 1280x800x24" "$app" --appimage-extract-and-run --no-sandbox 2>&1 | grep -v appimage_extracted | tee "$1.log" + } + # First launch: one shell, then Ctrl+Shift+D splits it (2 PTYs). + smoke smoke-1.png + grep -q "hqterm-desktop smoke: ok (ptys=2)" smoke-1.png.log + cat "$RUNNER_TEMP/cfg/hqterm/desktop-layout.json" + # Second launch restores both panes, then splits again (3 PTYs). + smoke smoke-2.png + grep -q "hqterm-desktop smoke: ok (ptys=3)" smoke-2.png.log + - uses: actions/upload-artifact@v4 + if: always() + with: + name: hqterm-desktop-smoke-screenshot + path: desktop/smoke-*.png + retention-days: 7 + - uses: actions/upload-artifact@v4 + with: + name: hqterm-desktop-linux-x86_64 + path: desktop/dist/*.AppImage + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a393555..663bf43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,52 @@ permissions: contents: write jobs: + desktop: + strategy: + matrix: + include: + - runner: ubuntu-latest + arch: x86_64 + - runner: ubuntu-24.04-arm + arch: aarch64 + runs-on: ${{ matrix.runner }} + # arm64 is a bonus: its failure must not hold back the x86_64 release. + continue-on-error: ${{ matrix.arch == 'aarch64' }} + defaults: + run: + working-directory: desktop + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Tag matches desktop/package.json + run: | + want="v$(node -p 'require("./package.json").version')" + [ "$want" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME but desktop/package.json says $want"; exit 1; } + - run: npm ci + - run: npm test + - run: npm run dist + - name: Name the AppImage + run: | + app="$(ls dist/*.AppImage | head -n1)" + mv "$app" "dist/hqterm-desktop-linux-${{ matrix.arch }}.AppImage" + ls -la dist + - name: Smoke test under Xvfb + run: | + if ! command -v xvfb-run >/dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq xvfb; fi + HQTERM_DESKTOP_SMOKE=8000 XDG_CONFIG_HOME="$RUNNER_TEMP/cfg" xvfb-run -a "dist/hqterm-desktop-linux-${{ matrix.arch }}.AppImage" --appimage-extract-and-run --no-sandbox 2>&1 | grep -v appimage_extracted | tee smoke.log + grep -q "hqterm-desktop smoke: ok" smoke.log + - uses: actions/upload-artifact@v4 + with: + name: desktop-${{ matrix.arch }} + path: desktop/dist/hqterm-desktop-linux-${{ matrix.arch }}.AppImage + release: + needs: desktop runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -28,6 +73,14 @@ jobs: target="bun-${t%%:*}"; name="hqterm-${t##*:}" bun build --compile --minify --target="$target" src/main.ts --outfile "dist/$name" done + - uses: actions/download-artifact@v4 + with: + pattern: desktop-* + path: dist + merge-multiple: true + - name: Checksums + run: | + cp desktop/build/icon.png dist/hqterm-desktop.png cd dist && sha256sum hqterm-* > SHA256SUMS && cat SHA256SUMS - name: Smoke test run: ./dist/hqterm-linux-amd64 --version diff --git a/README.md b/README.md index 3c9673c..a3b19a4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,46 @@ In the manager: Enter or one click attaches, `n` new session, `a` add host, `i` install hqsh on the host, `r` refresh, `q` quit. Inside a session, `Ctrl-^ .` detaches and returns to hqterm. +## Desktop app + +`hqterm desktop [--host H] [--session S]` opens hqterm's own terminal window +(Electron + xterm.js): tabs, split panes, and hqtui/qc images and HD emoji drawn +via iTerm2 inline images and sixel, plus the OpenEmoji colour font, so it works +where Konsole and friends cannot. Every pane gets `TERM_PROGRAM=hqterm`, +`HQTUI_IMAGES=iterm` and `QC_HD=iterm`. + +```sh +curl -fsSL https://hqterm.sh/install | sh -s -- --desktop +hqterm desktop +``` + +The installer puts the AppImage in `~/.local/opt/hqterm-desktop/` and adds a +launcher entry (`~/.local/share/applications/hqterm.desktop`). A plain install +does this too on Linux when `$DISPLAY` or `$WAYLAND_DISPLAY` is set; +`--no-desktop` skips it. + +| Keys | | +| --- | --- | +| Ctrl+Shift+T / W | new tab / close tab; Ctrl+Tab, Ctrl+PgUp/PgDn switch | +| Ctrl+Shift+D / E | split right / split down | +| Ctrl+Shift+Arrows | move between panes (or one click); drag dividers to resize | +| Ctrl+Shift+X | close pane | +| Ctrl+Shift+C / V | copy / paste | +| Ctrl+= / Ctrl+- / Ctrl+0 | zoom | + +The `+` button opens a shell, the hqterm session manager, or any host from +`~/.ssh/config` and `hosts.json` (as `hqsh --session main`). Splitting a +remote pane opens a new session on the same host (`main-2`, ...). + +The layout (tabs, splits, each remote pane's host and session) is saved to +`~/.config/hqterm/desktop-layout.json`, and the next launch reattaches every +remote pane to its hqsh session; local panes come back as fresh shells. +Settings: `~/.config/hqterm/desktop.json`, e.g. +`{"fontFamily": "JetBrains Mono", "fontSize": 14, "theme": {"background": "#000"}, "restore": true}`. + +Develop: `cd desktop && npm install && npm start` (needs bun and a C++ +toolchain for node-pty); `npm run dist` builds the AppImage. + ## Develop ```sh @@ -36,7 +76,8 @@ bun run build # single binary in dist/ ``` Releases: push a `v*` tag matching package.json; CI builds -`hqterm-{linux,darwin}-{amd64,arm64}` with `SHA256SUMS`. +`hqterm-{linux,darwin}-{amd64,arm64}`, `hqterm-desktop-linux-{x86_64,aarch64}.AppImage` +and `SHA256SUMS` (bump `desktop/package.json` too). `site/index.html` is the hqterm.sh landing page; `install.sh` is served at https://hqterm.sh/install. diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..e1e6194 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +lib/ +fonts/ diff --git a/desktop/build/icon.png b/desktop/build/icon.png new file mode 100644 index 0000000..1268538 Binary files /dev/null and b/desktop/build/icon.png differ diff --git a/desktop/main.js b/desktop/main.js new file mode 100644 index 0000000..c442b87 --- /dev/null +++ b/desktop/main.js @@ -0,0 +1,292 @@ +// hqterm desktop: Electron main process. Owns the PTYs, the config and the +// saved layout; the renderer (src/app.js) draws tabs and panes with xterm.js. +"use strict"; +const { app, BrowserWindow, clipboard, ipcMain, Menu, shell } = require("electron"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +// Chromium's setuid sandbox is not configured inside an AppImage. +if (process.platform === "linux") app.commandLine.appendSwitch("no-sandbox"); +app.setName("hqterm"); + +const VERSION = require("./package.json").version; +const HOST_RE = /^[A-Za-z0-9._@:\-\[\]]+$/; +const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + +const configDir = () => path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "hqterm"); +const configFile = () => path.join(configDir(), "desktop.json"); +const layoutFile = () => path.join(configDir(), "desktop-layout.json"); + +function readJson(file) { + try { + return JSON.parse(fs.readFileSync(file, "utf8")); + } catch { + return undefined; + } +} + +function loadConfig() { + const c = readJson(configFile()) || {}; + return { + fontFamily: typeof c.fontFamily === "string" && c.fontFamily.trim() ? c.fontFamily : "monospace", + fontSize: Number.isFinite(c.fontSize) && c.fontSize >= 6 && c.fontSize <= 72 ? c.fontSize : 14, + theme: c.theme && typeof c.theme === "object" ? c.theme : undefined, + restore: c.restore !== false, + }; +} + +/** `--host H --session S` (or --host=H) anywhere in argv. */ +function parseArgv(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const m = /^--(host|session)(?:=(.*))?$/.exec(argv[i]); + if (!m) continue; + const v = m[2] !== undefined ? m[2] : argv[++i]; + if (m[1] === "host" && v && HOST_RE.test(v) && !v.startsWith("-")) out.host = v; + if (m[1] === "session" && v && SESSION_RE.test(v)) out.session = v; + } + return out.host ? { kind: "remote", host: out.host, session: out.session || "main" } : undefined; +} + +let hostsModule; +function listHosts() { + try { + hostsModule = hostsModule || require("./lib/hosts.cjs"); + return hostsModule.listHosts(); + } catch (err) { + console.error("hqterm: host list unavailable:", err && err.message); + return []; + } +} + +function loginShell() { + let sh = process.env.SHELL; + if (!sh) { + try { + sh = os.userInfo().shell; + } catch {} + } + return sh && fs.existsSync(sh) ? sh : "/bin/sh"; +} + +/** The environment every pane starts with. */ +function paneEnv() { + const env = { ...process.env }; + for (const k of Object.keys(env)) if (k.startsWith("ELECTRON_") || k.startsWith("CHROME_")) delete env[k]; + // AppImage runtime variables would leak into every child. + for (const k of ["APPDIR", "APPIMAGE", "ARGV0", "OWD", "NO_AT_BRIDGE"]) delete env[k]; + if (env.LD_LIBRARY_PATH && process.env.APPDIR && env.LD_LIBRARY_PATH.includes(process.env.APPDIR)) delete env.LD_LIBRARY_PATH; + const localBin = path.join(os.homedir(), ".local", "bin"); + const parts = (env.PATH || "/usr/local/bin:/usr/bin:/bin").split(":"); + if (!parts.includes(localBin)) env.PATH = [localBin, ...parts].join(":"); + Object.assign(env, { + TERM: "xterm-256color", + COLORTERM: "truecolor", + TERM_PROGRAM: "hqterm", + TERM_PROGRAM_VERSION: VERSION, + HQTUI_IMAGES: "iterm", + QC_HD: "iterm", + // The one hint that crosses ssh (SendEnv/AcceptEnv LC_*): iTerm2 inline images work here. + LC_TERMINAL: "iTerm2", + }); + return env; +} + +function startDir() { + const owd = process.env.OWD; + if (owd && owd !== "/" && fs.existsSync(owd)) return owd; + const cwd = process.cwd(); + return cwd && cwd !== "/" ? cwd : os.homedir(); +} + +/** [file, args] for a pane spec; host and session are checked before they reach a shell. */ +function command(spec) { + if (spec && spec.kind === "remote" && HOST_RE.test(spec.host || "") && !spec.host.startsWith("-")) { + const session = SESSION_RE.test(spec.session || "") ? spec.session : "main"; + // hqsh forwards only TERM. sshd accepts LC_* by default (AcceptEnv LANG LC_*), + // a new session's shell inherits it, and hqtui reads LC_TERMINAL=iTerm2 as + // "draw iTerm2 inline images": so remote hqtui/qc draw HD images here too. + return ["/bin/sh", ["-lc", 'exec hqsh "$0" --session "$1" -- -o SetEnv=LC_TERMINAL=iTerm2', spec.host, session]]; + } + if (spec && spec.kind === "hqterm") return ["/bin/sh", ["-lc", "exec hqterm"]]; + return [loginShell(), ["-l"]]; +} + +let pty; +const ptys = new Map(); +let nextPty = 1; +let win; +let pendingSpec = parseArgv(process.argv.slice(1)); + +function createWindow() { + win = new BrowserWindow({ + width: 1100, + height: 720, + backgroundColor: "#0f1115", + title: "hqterm", + icon: path.join(__dirname, "src", "icon.png"), + autoHideMenuBar: true, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + spellcheck: false, + // The smoke test runs without a window manager, where an unfocused window stops painting. + backgroundThrottling: !process.env.HQTERM_DESKTOP_SMOKE, + }, + }); + if (process.env.HQTERM_DESKTOP_SMOKE) { + win.webContents.on("console-message", (e) => console.log(`renderer ${e.level}: ${e.message}`)); + } + win.loadFile(path.join(__dirname, "src", "index.html")); + // Nothing navigates away from the terminal; links open in the browser. + win.webContents.on("will-navigate", (e) => e.preventDefault()); + win.webContents.setWindowOpenHandler(({ url }) => { + if (/^https?:\/\//.test(url)) shell.openExternal(url); + return { action: "deny" }; + }); + win.on("closed", () => { + win = undefined; + for (const p of ptys.values()) { + try { + p.kill(); + } catch {} + } + ptys.clear(); + }); +} + +ipcMain.handle("app:init", () => { + const config = loadConfig(); + const layout = config.restore ? readJson(layoutFile()) : undefined; + const open = pendingSpec; + pendingSpec = undefined; + return { config, layout, open, version: VERSION, platform: process.platform }; +}); + +ipcMain.handle("hosts:list", () => listHosts()); + +ipcMain.handle("layout:save", (_e, data) => { + try { + fs.mkdirSync(configDir(), { recursive: true }); + const tmp = `${layoutFile()}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 }); + fs.renameSync(tmp, layoutFile()); + return true; + } catch (err) { + console.error("hqterm: could not save layout:", err && err.message); + return false; + } +}); + +ipcMain.handle("pty:spawn", (e, { spec, cols, rows }) => { + pty = pty || require("node-pty"); + const [file, args] = command(spec); + const id = nextPty++; + const p = pty.spawn(file, args, { + name: "xterm-256color", + cols: Math.max(2, cols | 0) || 80, + rows: Math.max(2, rows | 0) || 24, + cwd: spec && spec.kind === "shell" ? startDir() : os.homedir(), + env: paneEnv(), + }); + ptys.set(id, p); + const sender = e.sender; + p.onData((data) => { + if (!sender.isDestroyed()) sender.send("pty:data", id, data); + }); + p.onExit(({ exitCode, signal }) => { + ptys.delete(id); + if (!sender.isDestroyed()) sender.send("pty:exit", id, exitCode, signal); + }); + return id; +}); + +ipcMain.on("pty:write", (_e, id, data) => { + const p = ptys.get(id); + if (p && typeof data === "string") p.write(data); +}); + +ipcMain.on("pty:resize", (_e, id, cols, rows) => { + const p = ptys.get(id); + if (p && cols > 1 && rows > 1) { + try { + p.resize(cols | 0, rows | 0); + } catch {} + } +}); + +ipcMain.on("pty:kill", (_e, id) => { + const p = ptys.get(id); + if (p) { + ptys.delete(id); + try { + p.kill(); + } catch {} + } +}); + +ipcMain.handle("clipboard:read", () => clipboard.readText()); +ipcMain.on("clipboard:write", (_e, text) => { + if (typeof text === "string") clipboard.writeText(text); +}); +ipcMain.on("open:url", (_e, url) => { + if (typeof url === "string" && /^https?:\/\//.test(url)) shell.openExternal(url); +}); + +if (!app.requestSingleInstanceLock()) { + // An hqterm window is already open: it gets our --host/--session. + app.quit(); +} else { + app.on("second-instance", (_e, argv) => { + const spec = parseArgv(argv); + if (win) { + if (win.isMinimized()) win.restore(); + win.focus(); + win.webContents.send("app:open", spec || { kind: "shell" }); + } + }); + app.whenReady().then(() => { + // No menu: Ctrl+W, Ctrl+R and friends belong to the shell. + Menu.setApplicationMenu(null); + createWindow(); + if (process.env.HQTERM_DESKTOP_SMOKE) { + // CI: prove the window and a PTY come up, draw emoji and an iTerm2 + // inline image, optionally screenshot it ($HQTERM_DESKTOP_SMOKE_SHOT), exit. + const wait = Number(process.env.HQTERM_DESKTOP_SMOKE) || 8000; + setTimeout(() => { + const [id, p] = [...ptys.entries()][0] || []; + if (!p || !win) return; + p.write("printf '\\033[2J\\033[Hhqterm smoke: emoji \\360\\237\\230\\200 \\360\\237\\226\\245\\357\\270\\217 \\342\\234\\205 wide|\\n'\r"); + // The same escape hqtui emits (emoji art: 2 cells; then a bigger one). + const png = fs.readFileSync(path.join(__dirname, "src", "icon.png")); + const iip = (w, h) => `\x1b]1337;File=inline=1;size=${png.length};width=${w};height=${h};preserveAspectRatio=1:${png.toString("base64")}\x07`; + setTimeout(() => { + if (win) win.webContents.send("pty:data", id, `\r\nemoji art: ${iip(2, 1)} | image:\r\n${iip(12, 6)}\r\nafter image\r\n`); + }, 1000); + }, wait / 2); + // Split right with the real key binding: one more pane, one more PTY. + setTimeout(() => { + if (!win) return; + win.webContents.sendInputEvent({ type: "keyDown", keyCode: "D", modifiers: ["control", "shift"] }); + win.webContents.sendInputEvent({ type: "keyUp", keyCode: "D", modifiers: ["control", "shift"] }); + }, wait * 0.7); + setTimeout(async () => { + const shot = process.env.HQTERM_DESKTOP_SMOKE_SHOT; + if (shot && win) { + try { + fs.writeFileSync(shot, (await win.webContents.capturePage()).toPNG()); + console.log(`hqterm-desktop smoke: screenshot ${shot}`); + } catch (err) { + console.log(`hqterm-desktop smoke: screenshot failed: ${err && err.message}`); + } + } + console.log(`hqterm-desktop smoke: ok (ptys=${ptys.size})`); + app.exit(ptys.size > 0 ? 0 : 3); + }, wait); + } + }); + app.on("window-all-closed", () => app.quit()); +} diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..16065f3 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,3665 @@ +{ + "name": "hqterm-desktop", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hqterm-desktop", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-image": "^0.9.0", + "@xterm/addon-unicode11": "^0.9.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^6.0.0", + "node-pty": "^1.1.0" + }, + "devDependencies": { + "electron": "^44.5.1", + "electron-builder": "^26.15.3" + } + }, + "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": "BSD-2-Clause", + "engines": { + "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" + }, + "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.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "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": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "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": ">=10" + } + }, + "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, + "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" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "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": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "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": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": "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": ">=12.0.0" + } + }, + "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", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "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.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/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": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.10.0.tgz", + "integrity": "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "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": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/addon-image": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0.tgz", + "integrity": "sha512-oYWA8/QAr5/Emwl1xL7WCoOqeG3IZfpzEz/OVq1j4Oi9934TQmHiyubClikRf0D/jL3JNiNuz/Lsqx0kXQ02BA==", + "license": "MIT" + }, + "node_modules/@xterm/addon-unicode11": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0.tgz", + "integrity": "sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw==", + "license": "MIT" + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", + "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", + "license": "MIT" + }, + "node_modules/@xterm/addon-webgl": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", + "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/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/dir-compare/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/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": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "44.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.5.1.tgz", + "integrity": "sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.36.0.tgz", + "integrity": "sha512-zrvE1Qy5ElbZVqmJFSiNfwzNZzUCepasgp/4dw0Z0fsn40LJkAk9FA9oMh2SKMeNFRRe/3IfeOKvWwWr8u4SmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz", + "integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "website" + ], + "dependencies": { + "@noble/hashes": "1.8.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "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": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..433bb64 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,59 @@ +{ + "name": "hqterm-desktop", + "productName": "hqterm", + "version": "0.2.0", + "description": "hqterm desktop: a terminal window that shows hqtui/qc images and HD emoji (xterm.js + node-pty).", + "license": "MIT", + "author": "Profullstack, Inc. ", + "homepage": "https://hqterm.sh", + "private": true, + "main": "main.js", + "desktopName": "hqterm.desktop", + "scripts": { + "prepare-assets": "node scripts/prepare.mjs", + "start": "npm run prepare-assets && electron .", + "test": "node --test test/layout-tests.js", + "dist": "npm run prepare-assets && electron-builder --linux AppImage --publish never" + }, + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-image": "^0.9.0", + "@xterm/addon-unicode11": "^0.9.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^6.0.0", + "node-pty": "^1.1.0" + }, + "devDependencies": { + "electron": "^44.5.1", + "electron-builder": "^26.15.3" + }, + "build": { + "appId": "sh.hqterm.desktop", + "productName": "hqterm", + "executableName": "hqterm-desktop", + "files": [ + "main.js", + "preload.js", + "lib/**", + "src/**", + "fonts/**", + "package.json" + ], + "asarUnpack": [ + "**/node_modules/node-pty/**" + ], + "linux": { + "target": "AppImage", + "category": "System;TerminalEmulator", + "icon": "build/icon.png", + "artifactName": "hqterm-desktop-linux-${arch}.${ext}", + "executableArgs": ["--no-sandbox"] + }, + "mac": { + "target": "zip", + "icon": "build/icon.png", + "identity": null + } + } +} diff --git a/desktop/preload.js b/desktop/preload.js new file mode 100644 index 0000000..7d46ba3 --- /dev/null +++ b/desktop/preload.js @@ -0,0 +1,19 @@ +// The renderer's only door to the main process. +"use strict"; +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("hq", { + init: () => ipcRenderer.invoke("app:init"), + hosts: () => ipcRenderer.invoke("hosts:list"), + saveLayout: (data) => ipcRenderer.invoke("layout:save", data), + spawn: (spec, cols, rows) => ipcRenderer.invoke("pty:spawn", { spec, cols, rows }), + write: (id, data) => ipcRenderer.send("pty:write", id, data), + resize: (id, cols, rows) => ipcRenderer.send("pty:resize", id, cols, rows), + kill: (id) => ipcRenderer.send("pty:kill", id), + onData: (fn) => ipcRenderer.on("pty:data", (_e, id, data) => fn(id, data)), + onExit: (fn) => ipcRenderer.on("pty:exit", (_e, id, code, signal) => fn(id, code, signal)), + onOpen: (fn) => ipcRenderer.on("app:open", (_e, spec) => fn(spec)), + readClipboard: () => ipcRenderer.invoke("clipboard:read"), + writeClipboard: (text) => ipcRenderer.send("clipboard:write", text), + openUrl: (url) => ipcRenderer.send("open:url", url), +}); diff --git a/desktop/scripts/prepare.mjs b/desktop/scripts/prepare.mjs new file mode 100644 index 0000000..314707e --- /dev/null +++ b/desktop/scripts/prepare.mjs @@ -0,0 +1,44 @@ +// Build-time assets for the desktop app: +// lib/hosts.cjs the TUI's host-list code (../src), bundled for Node by bun +// fonts/OpenEmoji-CBDT.ttf the OpenEmoji colour font (skip: HQTERM_SKIP_FONT=1) +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(dirname(fileURLToPath(import.meta.url))); +const FONT_URL = "https://raw.githubusercontent.com/profullstack/openemoji/main/font/OpenEmoji-CBDT.ttf"; + +mkdirSync(join(here, "lib"), { recursive: true }); +const r = spawnSync( + "bun", + ["build", join(here, "..", "src", "hostlist.ts"), "--target=node", "--format=cjs", "--outfile", join(here, "lib", "hosts.cjs")], + { stdio: "inherit" }, +); +if (r.status !== 0) { + console.error("prepare: bun build of the host list failed (is bun installed?)"); + process.exit(1); +} + +const font = join(here, "fonts", "OpenEmoji-CBDT.ttf"); +if (process.env.HQTERM_SKIP_FONT) { + console.log("prepare: skipping the OpenEmoji font (HQTERM_SKIP_FONT)"); +} else if (existsSync(font) && statSync(font).size > 1_000_000) { + console.log(`prepare: ${font} already here`); +} else { + mkdirSync(dirname(font), { recursive: true }); + console.log(`prepare: downloading ${FONT_URL}`); + const res = await fetch(FONT_URL); + if (!res.ok) { + console.error(`prepare: font download failed: HTTP ${res.status}`); + process.exit(1); + } + const buf = Buffer.from(await res.arrayBuffer()); + if (buf.length < 1_000_000 || buf.subarray(0, 4).toString("hex") !== "00010000") { + console.error(`prepare: that does not look like the font (${buf.length} bytes)`); + process.exit(1); + } + writeFileSync(`${font}.part`, buf); + renameSync(`${font}.part`, font); + console.log(`prepare: font ${buf.length} bytes`); +} diff --git a/desktop/src/app.js b/desktop/src/app.js new file mode 100644 index 0000000..8d75ade --- /dev/null +++ b/desktop/src/app.js @@ -0,0 +1,597 @@ +// hqterm desktop renderer: tabs of split panes, each pane an xterm.js terminal +// on a PTY in the main process. Layout lives in src/layout.js (HqLayout). +"use strict"; +(async function () { + const L = window.HqLayout; + const hq = window.hq; + const $ = (sel) => document.querySelector(sel); + const INSTALL = "curl -fsSL https://hqterm.sh/install | sh"; + const MAX_WEBGL = 12; + + const init = await hq.init(); + const config = init.config; + let fontSize = config.fontSize; + const theme = Object.assign( + { + background: "#0f1115", + foreground: "#d8dee9", + cursor: "#5ea1ff", + cursorAccent: "#0f1115", + selectionBackground: "#5ea1ff55", + }, + config.theme || {}, + ); + const fontFamily = `${config.fontFamily}, "OpenEmoji"`; + // The colour emoji font must be loaded before xterm measures and draws glyphs. + await Promise.race([document.fonts.load(`${fontSize}px "OpenEmoji"`, "\u{1F600}").catch(() => {}), new Promise((r) => setTimeout(r, 3000))]); + + /** @type {{id:number, root:any, focus:string, view:HTMLElement}[]} */ + const tabs = []; + let active = 0; + let nextTab = 1; + const runtimes = new Map(); // pane id -> runtime + const byPty = new Map(); // pty id -> runtime + const early = new Map(); // pty id -> { data: string[], exit?: [code, signal] } + let webglCount = 0; + + // ---------- PTY plumbing ---------- + + hq.onData((id, data) => { + const rt = byPty.get(id); + if (rt) rt.term.write(data); + else (early.get(id) || early.set(id, { data: [] }).get(id)).data.push(data); + }); + hq.onExit((id, code, signal) => { + const rt = byPty.get(id); + if (rt) exited(rt, code, signal); + else (early.get(id) || early.set(id, { data: [] }).get(id)).exit = [code, signal]; + }); + + async function spawn(rt) { + rt.exited = undefined; + const id = await hq.spawn(rt.spec, rt.term.cols, rt.term.rows); + if (rt.disposed) return hq.kill(id); + rt.ptyId = id; + byPty.set(id, rt); + const pending = early.get(id); + if (pending) { + early.delete(id); + for (const d of pending.data) rt.term.write(d); + if (pending.exit) exited(rt, ...pending.exit); + } + } + + function exited(rt, code, signal) { + byPty.delete(rt.ptyId); + rt.ptyId = undefined; + if (rt.disposed) return; + if (code === 0 && !signal) return closePane(rt.pane.id); + rt.exited = { code, signal }; + let msg = `\r\n\x1b[2m[hqterm] ${rt.spec.kind === "remote" ? "hqsh" : "process"} exited (${signal ? `signal ${signal}` : code}).`; + if (code === 127) msg += ` ${rt.spec.kind === "remote" ? "hqsh" : "hqterm"} not found; install with: ${INSTALL}`; + msg += ` Enter ${rt.spec.kind === "remote" ? "reconnects" : "restarts"}, Esc closes.\x1b[0m\r\n`; + rt.term.write(msg); + } + + // ---------- panes ---------- + + function createRuntime(pane) { + const el = document.createElement("div"); + el.className = "pane"; + el.dataset.pane = pane.id; + const term = new Terminal({ + fontFamily, + fontSize, + theme, + allowProposedApi: true, + scrollback: 10000, + cursorBlink: true, + macOptionIsMeta: true, + allowTransparency: false, + }); + const rt = { pane, spec: pane.spec, el, term, fit: new FitAddon.FitAddon(), title: "", opened: false }; + term.loadAddon(rt.fit); + const unicode = new Unicode11Addon.Unicode11Addon(); + term.loadAddon(unicode); + term.unicode.activeVersion = "11"; + term.loadAddon(new WebLinksAddon.WebLinksAddon((_e, uri) => hq.openUrl(uri))); + term.onData((d) => { + if (rt.exited) { + if (d === "\r") { + term.write("\r\n"); + spawn(rt); + } else if (d === "\x1b") closePane(pane.id); + return; + } + if (rt.ptyId !== undefined) hq.write(rt.ptyId, d); + }); + term.onBinary((d) => rt.ptyId !== undefined && hq.write(rt.ptyId, d)); + term.onResize(({ cols, rows }) => rt.ptyId !== undefined && hq.resize(rt.ptyId, cols, rows)); + term.onTitleChange((t) => { + rt.title = t; + renderBar(); + }); + // One click focuses a pane. + el.addEventListener("mousedown", () => setFocus(pane.id), true); + el.addEventListener("focusin", () => setFocus(pane.id, false)); + rt.observer = new ResizeObserver(() => fitSoon(rt)); + rt.observer.observe(el); + runtimes.set(pane.id, rt); + return rt; + } + + function open(rt) { + if (rt.opened) return; + rt.opened = true; + rt.term.open(rt.el); + if (webglCount < MAX_WEBGL) { + try { + const gl = new WebglAddon.WebglAddon(); + gl.onContextLoss(() => { + gl.dispose(); + webglCount--; + }); + rt.term.loadAddon(gl); + webglCount++; + rt.webgl = gl; + } catch { + // DOM renderer it is. + } + } + // After the renderer, so images draw on top of it. + const images = new ImageAddon.ImageAddon({ iipSupport: true, sixelSupport: true, enableSizeReports: true }); + rt.term.loadAddon(images); + iipCursorLikeITerm(rt.term, images); + fit(rt); + spawn(rt); + } + + /** + * iTerm2, WezTerm and kitty leave the cursor just right of an inline image, + * and hqtui's emoji art relies on it ("hi " + art + " there"). addon-image + * leaves it at the image's left edge, so the next text erases the art. After + * an iTerm2 image (not sixel), move the cursor past it. + */ + function iipCursorLikeITerm(term, addon) { + const storage = addon._storage; + if (!storage || typeof storage.addImage !== "function" || !term._core) return; + let pending = false; + // Registered after the addon, so this runs first; false lets the addon draw it. + term.parser.registerOscHandler(1337, (data) => { + pending = data.startsWith("File="); + return false; + }); + const addImage = storage.addImage.bind(storage); + storage.addImage = (img) => { + const buffer = term._core.buffer; + const x0 = buffer.x; + addImage(img); + if (!pending) return; + pending = false; + const cell = addon._renderer && addon._renderer.cellSize; + const w = cell && cell.width > 0 ? cell.width : 10; + buffer.x = Math.min(x0 + Math.ceil(img.width / w), term.cols); + }; + } + + function fit(rt) { + if (!rt.opened || rt.disposed || !rt.el.isConnected) return; + if (rt.el.clientWidth < 10 || rt.el.clientHeight < 10) return; + try { + rt.fit.fit(); + } catch {} + } + + function fitSoon(rt) { + if (rt.fitQueued) return; + rt.fitQueued = true; + requestAnimationFrame(() => { + rt.fitQueued = false; + fit(rt); + }); + } + + function disposeRuntime(rt) { + rt.disposed = true; + if (rt.ptyId !== undefined) { + hq.kill(rt.ptyId); + byPty.delete(rt.ptyId); + } + rt.observer.disconnect(); + if (rt.webgl) webglCount--; + rt.term.dispose(); + rt.el.remove(); + runtimes.delete(rt.pane.id); + } + + // ---------- tabs and layout ---------- + + const tabOf = (paneId) => tabs.find((t) => L.find(t.root, paneId)); + const current = () => tabs[active]; + const focusedRt = () => current() && runtimes.get(current().focus); + const allSpecs = () => tabs.flatMap((t) => L.panes(t.root).map((p) => p.spec)); + + function addTab(root, focus) { + const view = document.createElement("div"); + view.className = "view"; + $("#views").appendChild(view); + const tab = { id: nextTab++, root, focus: focus || L.panes(root)[0].id, view }; + tabs.push(tab); + renderTab(tab); + return tab; + } + + function newTab(spec) { + const tab = addTab(L.pane(spec)); + activate(tabs.indexOf(tab)); + save(); + } + + /** Open a spec, or focus the pane already showing that host's session. */ + function openSpec(spec) { + const clean = L.cleanSpec(spec) || { kind: "shell" }; + if (clean.kind === "remote") { + for (const t of tabs) { + const p = L.panes(t.root).find((p) => p.spec.kind === "remote" && p.spec.host === clean.host && p.spec.session === clean.session); + if (p) { + activate(tabs.indexOf(t)); + return setFocus(p.id); + } + } + } + newTab(clean); + } + + function closeTab(i) { + const tab = tabs[i]; + if (!tab) return; + for (const p of L.panes(tab.root)) { + const rt = runtimes.get(p.id); + if (rt) disposeRuntime(rt); + } + tab.view.remove(); + tabs.splice(i, 1); + if (!tabs.length) { + save(true); + return window.close(); + } + activate(Math.min(i, tabs.length - 1)); + save(); + } + + function closePane(paneId) { + const tab = tabOf(paneId); + if (!tab) return; + const list = L.panes(tab.root); + const idx = list.findIndex((p) => p.id === paneId); + const rt = runtimes.get(paneId); + if (rt) disposeRuntime(rt); + tab.root = L.remove(tab.root, paneId); + if (!tab.root) return closeTab(tabs.indexOf(tab)); + if (tab.focus === paneId) { + const rest = L.panes(tab.root); + tab.focus = rest[Math.max(0, idx - 1)] ? rest[Math.max(0, idx - 1)].id : rest[0].id; + } + renderTab(tab); + if (tab === current()) focusTerm(); + renderBar(); + save(); + } + + function splitFocused(dir) { + const tab = current(); + const rt = focusedRt(); + if (!tab || !rt) return; + const added = L.pane(L.splitSpec(rt.spec, allSpecs())); + tab.root = L.split(tab.root, rt.pane.id, dir, added); + tab.focus = added.id; + renderTab(tab); + focusTerm(); + renderBar(); + save(); + } + + function moveFocus(dir) { + const tab = current(); + if (!tab) return; + const rects = {}; + for (const p of L.panes(tab.root)) { + const rt = runtimes.get(p.id); + if (rt) rects[p.id] = rt.el.getBoundingClientRect(); + } + const next = L.neighbor(rects, tab.focus, dir); + if (next) setFocus(next); + } + + function setFocus(paneId, focusTerminal = true) { + const tab = tabOf(paneId); + if (!tab) return; + if (tab.focus !== paneId) { + tab.focus = paneId; + save(); + } + for (const p of L.panes(tab.root)) { + const rt = runtimes.get(p.id); + if (rt) rt.el.classList.toggle("focused", p.id === paneId); + } + if (tab === current()) { + if (focusTerminal) focusTerm(); + renderBar(); + } + } + + function focusTerm() { + const rt = focusedRt(); + if (rt) requestAnimationFrame(() => rt.term.focus()); + } + + function activate(i) { + if (!tabs.length) return; + active = (i + tabs.length) % tabs.length; + tabs.forEach((t, j) => t.view.classList.toggle("inactive", j !== active)); + const tab = current(); + for (const p of L.panes(tab.root)) { + const rt = runtimes.get(p.id); + if (rt) fitSoon(rt); + } + setFocus(tab.focus); + renderBar(); + save(); + } + + /** (Re)build a tab's pane tree in its view; panes keep their terminals. */ + function renderTab(tab) { + const build = (node) => { + if (node.type === "pane") { + const rt = runtimes.get(node.id) || createRuntime(node); + rt.el.classList.toggle("focused", node.id === tab.focus); + return rt.el; + } + const box = document.createElement("div"); + box.className = `split ${node.dir}`; + const a = document.createElement("div"); + const b = document.createElement("div"); + a.className = b.className = "slot"; + const size = () => { + a.style.flex = `${node.ratio} 1 0`; + b.style.flex = `${1 - node.ratio} 1 0`; + }; + size(); + a.appendChild(build(node.a)); + b.appendChild(build(node.b)); + const div = document.createElement("div"); + div.className = "divider"; + div.addEventListener("mousedown", (e) => { + e.preventDefault(); + const r = box.getBoundingClientRect(); + div.classList.add("dragging"); + document.body.classList.add(`dragging-${node.dir}`); + const move = (ev) => { + const f = node.dir === "row" ? (ev.clientX - r.left) / r.width : (ev.clientY - r.top) / r.height; + node.ratio = Math.min(0.95, Math.max(0.05, f)); + size(); + }; + const up = () => { + window.removeEventListener("mousemove", move); + window.removeEventListener("mouseup", up); + div.classList.remove("dragging"); + document.body.classList.remove(`dragging-${node.dir}`); + save(); + focusTerm(); + }; + window.addEventListener("mousemove", move); + window.addEventListener("mouseup", up); + }); + box.append(a, div, b); + return box; + }; + tab.view.replaceChildren(build(tab.root)); + tab.view.classList.toggle("multi", L.panes(tab.root).length > 1); + for (const p of L.panes(tab.root)) { + const rt = runtimes.get(p.id); + if (rt) (rt.opened ? fitSoon(rt) : open(rt)); + } + } + + function tabLabel(tab) { + const rt = runtimes.get(tab.focus); + const spec = rt ? rt.spec : L.panes(tab.root)[0].spec; + const n = L.panes(tab.root).length; + const base = (rt && rt.title) || L.label(spec); + return n > 1 ? `${base} (${n})` : base; + } + + function renderBar() { + const bar = $("#tabs"); + bar.replaceChildren( + ...tabs.map((tab, i) => { + const el = document.createElement("div"); + el.className = `tab${i === active ? " active" : ""}`; + el.setAttribute("role", "tab"); + const rt = runtimes.get(tab.focus); + const icon = document.createElement("img"); + icon.className = "kind"; + icon.src = rt && rt.spec.kind === "remote" ? "icons/server.svg" : "icons/terminal.svg"; + icon.alt = ""; + const label = document.createElement("span"); + label.className = "label"; + label.textContent = tabLabel(tab); + el.title = label.textContent; + const close = document.createElement("button"); + close.className = "icon"; + close.title = "Close tab (Ctrl+Shift+W)"; + close.innerHTML = 'Close'; + close.addEventListener("click", (e) => { + e.stopPropagation(); + closeTab(tabs.indexOf(tab)); + }); + el.addEventListener("mousedown", (e) => { + if (e.button === 1) { + e.preventDefault(); + closeTab(tabs.indexOf(tab)); + } else if (e.button === 0 && e.target !== close && !close.contains(e.target)) activate(tabs.indexOf(tab)); + }); + el.append(icon, label, close); + return el; + }), + ); + const rt = focusedRt(); + const t = current(); + document.title = t ? `${(rt && rt.title) || L.label(rt ? rt.spec : undefined)} — hqterm` : "hqterm"; + } + + let saveTimer; + function save(now) { + clearTimeout(saveTimer); + const run = () => hq.saveLayout(L.serialize(tabs.map((t) => ({ root: t.root, focus: t.focus })), active)); + if (now) run(); + else saveTimer = setTimeout(run, 300); + } + + // ---------- menu ---------- + + const menu = $("#menu"); + function closeMenu() { + menu.hidden = true; + } + async function showMenu() { + if (!menu.hidden) return closeMenu(); + const item = (icon, text, key, fn) => { + const el = document.createElement("div"); + el.className = "item"; + el.setAttribute("role", "menuitem"); + el.innerHTML = `${key ? `${key}` : ""}`; + el.querySelector("span").textContent = text; + el.addEventListener("click", () => { + closeMenu(); + fn(); + }); + return el; + }; + const sep = () => Object.assign(document.createElement("div"), { className: "sep" }); + const items = [ + item("terminal", "New shell", "Ctrl+Shift+T", () => newTab({ kind: "shell" })), + item("terminal", "hqterm sessions", "", () => newTab({ kind: "hqterm" })), + ]; + if (focusedRt()) { + items.push(sep(), item("add", "Split right", "Ctrl+Shift+D", () => splitFocused("row")), item("add", "Split down", "Ctrl+Shift+E", () => splitFocused("column"))); + } + let hosts = []; + try { + hosts = await hq.hosts(); + } catch {} + if (hosts.length) { + const head = Object.assign(document.createElement("div"), { className: "head", textContent: "Connect host (hqsh, session main)" }); + items.push(sep(), head, ...hosts.map((h) => item("server", h, "", () => openSpec({ kind: "remote", host: h, session: "main" })))); + } + menu.replaceChildren(...items); + const r = $("#new").getBoundingClientRect(); + menu.style.left = `${Math.min(r.left, window.innerWidth - 240)}px`; + menu.style.top = `${r.bottom + 2}px`; + menu.hidden = false; + } + $("#new").addEventListener("click", (e) => { + e.stopPropagation(); + showMenu(); + }); + window.addEventListener("mousedown", (e) => { + if (!menu.hidden && !menu.contains(e.target) && e.target.closest("#new") === null) closeMenu(); + }); + + // ---------- keys ---------- + + function zoom(delta) { + fontSize = delta === 0 ? config.fontSize : Math.min(72, Math.max(6, fontSize + delta)); + for (const rt of runtimes.values()) { + rt.term.options.fontSize = fontSize; + fitSoon(rt); + } + } + + async function paste() { + const rt = focusedRt(); + const text = await hq.readClipboard(); + if (rt && text && rt.ptyId !== undefined) rt.term.paste(text); + } + + window.addEventListener( + "keydown", + (e) => { + if (e.key === "Escape" && !menu.hidden) return closeMenu(); + let handled = true; + if (e.ctrlKey && e.key === "Tab") activate(active + (e.shiftKey ? -1 : 1)); + else if (e.ctrlKey && !e.shiftKey && !e.altKey && e.key === "PageDown") activate(active + 1); + else if (e.ctrlKey && !e.shiftKey && !e.altKey && e.key === "PageUp") activate(active - 1); + else if (e.ctrlKey && e.shiftKey && !e.altKey) { + switch (e.code) { + case "KeyT": + newTab({ kind: "shell" }); + break; + case "KeyW": + closeTab(active); + break; + case "KeyC": { + const rt = focusedRt(); + const sel = rt && rt.term.getSelection(); + if (sel) hq.writeClipboard(sel); + break; + } + case "KeyV": + paste(); + break; + case "KeyD": + splitFocused("row"); + break; + case "KeyE": + splitFocused("column"); + break; + case "KeyX": { + const rt = focusedRt(); + if (rt) closePane(rt.pane.id); + break; + } + case "ArrowLeft": + moveFocus("left"); + break; + case "ArrowRight": + moveFocus("right"); + break; + case "ArrowUp": + moveFocus("up"); + break; + case "ArrowDown": + moveFocus("down"); + break; + case "Equal": + zoom(1); // Ctrl+Shift+= is Ctrl++ on US layouts + break; + default: + handled = false; + } + } else if (e.ctrlKey && !e.altKey && (e.key === "=" || e.key === "+")) zoom(1); + else if (e.ctrlKey && !e.altKey && !e.shiftKey && e.key === "-") zoom(-1); + else if (e.ctrlKey && !e.altKey && !e.shiftKey && e.key === "0") zoom(0); + else handled = false; + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }, + true, + ); + + window.addEventListener("resize", () => { + for (const rt of runtimes.values()) fitSoon(rt); + }); + + // ---------- start ---------- + + const restored = L.restore(init.layout); + for (const t of restored.tabs) addTab(t.root, t.focus); + if (tabs.length) activate(restored.active); + if (init.open) openSpec(init.open); + if (!tabs.length) newTab({ kind: "shell" }); + hq.onOpen((spec) => openSpec(spec)); +})().catch((err) => { + document.body.textContent = `hqterm failed to start: ${err && err.stack ? err.stack : err}`; +}); diff --git a/desktop/src/icon.png b/desktop/src/icon.png new file mode 100644 index 0000000..1268538 Binary files /dev/null and b/desktop/src/icon.png differ diff --git a/desktop/src/icons/add.svg b/desktop/src/icons/add.svg new file mode 100644 index 0000000..a31cd80 --- /dev/null +++ b/desktop/src/icons/add.svg @@ -0,0 +1 @@ +Add diff --git a/desktop/src/icons/close.svg b/desktop/src/icons/close.svg new file mode 100644 index 0000000..223d2e1 --- /dev/null +++ b/desktop/src/icons/close.svg @@ -0,0 +1 @@ +Close diff --git a/desktop/src/icons/server.svg b/desktop/src/icons/server.svg new file mode 100644 index 0000000..c2853df --- /dev/null +++ b/desktop/src/icons/server.svg @@ -0,0 +1 @@ +Server diff --git a/desktop/src/icons/terminal.svg b/desktop/src/icons/terminal.svg new file mode 100644 index 0000000..7776fcf --- /dev/null +++ b/desktop/src/icons/terminal.svg @@ -0,0 +1 @@ +Terminal diff --git a/desktop/src/index.html b/desktop/src/index.html new file mode 100644 index 0000000..7489fce --- /dev/null +++ b/desktop/src/index.html @@ -0,0 +1,31 @@ + + + + + + hqterm + + + + +
+
+ +
+ +
+ + + + + + + + + + diff --git a/desktop/src/layout.js b/desktop/src/layout.js new file mode 100644 index 0000000..e9e43dd --- /dev/null +++ b/desktop/src/layout.js @@ -0,0 +1,177 @@ +/* + * Pane trees for hqterm desktop, without DOM or PTYs so it can be tested. + * + * A tab's layout is a binary tree: + * { type: "pane", id, spec } spec: { kind: "shell" | "remote" | "hqterm", host?, session? } + * { type: "split", dir: "row" | "column", ratio, a, b } row = side by side, column = stacked + * + * Saved layouts (~/.config/hqterm/desktop-layout.json) keep the tree without + * ids: { version: 1, active, tabs: [{ root, focus }] }. + */ +(function (root, factory) { + const api = factory(); + if (typeof module === "object" && module.exports) module.exports = api; + else root.HqLayout = api; +})(typeof self !== "undefined" ? self : this, function () { + const HOST_RE = /^[A-Za-z0-9._@:\-\[\]]+$/; + const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + + let nextId = 1; + const newId = () => `p${nextId++}`; + + function pane(spec) { + return { type: "pane", id: newId(), spec: cleanSpec(spec) || { kind: "shell" } }; + } + + /** A spec we are willing to run, or undefined. */ + function cleanSpec(spec) { + if (!spec || typeof spec !== "object") return undefined; + if (spec.kind === "shell" || spec.kind === "hqterm") return { kind: spec.kind }; + if (spec.kind === "remote" && typeof spec.host === "string" && HOST_RE.test(spec.host) && !spec.host.startsWith("-")) { + const session = typeof spec.session === "string" && SESSION_RE.test(spec.session) ? spec.session : "main"; + return { kind: "remote", host: spec.host, session }; + } + return undefined; + } + + function panes(node, out = []) { + if (!node) return out; + if (node.type === "pane") out.push(node); + else { + panes(node.a, out); + panes(node.b, out); + } + return out; + } + + function find(node, id) { + return panes(node).find((p) => p.id === id); + } + + /** Replace pane `id` with a split of it and `added`. Returns the new root. */ + function split(root, id, dir, added) { + const walk = (node) => { + if (node.type === "pane") return node.id === id ? { type: "split", dir, ratio: 0.5, a: node, b: added } : node; + return { ...node, a: walk(node.a), b: walk(node.b) }; + }; + return walk(root); + } + + /** Remove pane `id`; its sibling takes the parent's place. Returns the new root (null when empty). */ + function remove(root, id) { + const walk = (node) => { + if (node.type === "pane") return node.id === id ? null : node; + const a = walk(node.a); + const b = walk(node.b); + if (!a) return b; + if (!b) return a; + return a === node.a && b === node.b ? node : { ...node, a, b }; + }; + return root ? walk(root) : null; + } + + /** Session name for a new pane on `host`: main, main-2, main-3 … not used in `taken`. */ + function freeSession(host, taken) { + const used = new Set(taken.filter((s) => s.kind === "remote" && s.host === host).map((s) => s.session)); + if (!used.has("main")) return "main"; + for (let n = 2; ; n++) if (!used.has(`main-${n}`)) return `main-${n}`; + } + + /** What a split of `spec` opens: remote panes get a fresh session on the same host, the rest a shell. */ + function splitSpec(spec, taken) { + if (spec && spec.kind === "remote") return { kind: "remote", host: spec.host, session: freeSession(spec.host, taken) }; + return { kind: "shell" }; + } + + function clampRatio(r) { + return typeof r === "number" && isFinite(r) ? Math.min(0.95, Math.max(0.05, r)) : 0.5; + } + + function serializeNode(node) { + if (node.type === "pane") return { type: "pane", spec: node.spec }; + return { type: "split", dir: node.dir, ratio: clampRatio(node.ratio), a: serializeNode(node.a), b: serializeNode(node.b) }; + } + + /** tabs: [{ root, focus (pane id) }] */ + function serialize(tabs, active) { + return { + version: 1, + active: Math.max(0, Math.min(active | 0, tabs.length - 1)), + tabs: tabs.map((t) => ({ + root: serializeNode(t.root), + focus: Math.max(0, panes(t.root).findIndex((p) => p.id === t.focus)), + })), + }; + } + + function restoreNode(data, depth) { + if (!data || typeof data !== "object" || depth > 32) return null; + if (data.type === "pane") return pane(cleanSpec(data.spec) || { kind: "shell" }); + if (data.type === "split" && (data.dir === "row" || data.dir === "column")) { + const a = restoreNode(data.a, depth + 1); + const b = restoreNode(data.b, depth + 1); + if (!a) return b; + if (!b) return a; + return { type: "split", dir: data.dir, ratio: clampRatio(data.ratio), a, b }; + } + return null; + } + + /** From saved JSON (text or object) to { tabs: [{ root, focus }], active }; empty tabs when unusable. */ + function restore(saved) { + let data = saved; + if (typeof saved === "string") { + try { + data = JSON.parse(saved); + } catch { + data = null; + } + } + const tabs = []; + if (data && data.version === 1 && Array.isArray(data.tabs)) { + for (const t of data.tabs.slice(0, 64)) { + const root = restoreNode(t && t.root, 0); + if (!root) continue; + const list = panes(root); + const focus = (list[(t.focus | 0)] || list[0]).id; + tabs.push({ root, focus }); + } + } + const active = data && tabs.length ? Math.max(0, Math.min(data.active | 0, tabs.length - 1)) : 0; + return { tabs, active }; + } + + /** + * The pane next to `from` in direction dir ("left" | "right" | "up" | "down"), + * given rects { id: {left, top, right, bottom} }. Picks the closest pane that + * overlaps on the other axis, else the closest by centre. + */ + function neighbor(rects, from, dir) { + const r = rects[from]; + if (!r) return undefined; + const horiz = dir === "left" || dir === "right"; + let best; + let bestScore = Infinity; + for (const [id, o] of Object.entries(rects)) { + if (id === from) continue; + const ahead = + dir === "left" ? o.right <= r.left + 1 : dir === "right" ? o.left >= r.right - 1 : dir === "up" ? o.bottom <= r.top + 1 : o.top >= r.bottom - 1; + if (!ahead) continue; + const gap = dir === "left" ? r.left - o.right : dir === "right" ? o.left - r.right : dir === "up" ? r.top - o.bottom : o.top - r.bottom; + const overlap = horiz ? Math.min(r.bottom, o.bottom) - Math.max(r.top, o.top) : Math.min(r.right, o.right) - Math.max(r.left, o.left); + const cross = horiz ? Math.abs((o.top + o.bottom) / 2 - (r.top + r.bottom) / 2) : Math.abs((o.left + o.right) / 2 - (r.left + r.right) / 2); + const score = (overlap > 0 ? 0 : 1e6) + gap * 1000 + cross; + if (score < bestScore) (bestScore = score), (best = id); + } + return best; + } + + function label(spec) { + if (!spec) return "shell"; + if (spec.kind === "remote") return spec.session && spec.session !== "main" ? `${spec.host}:${spec.session}` : spec.host; + if (spec.kind === "hqterm") return "hqterm"; + return "shell"; + } + + return { pane, cleanSpec, panes, find, split, remove, freeSession, splitSpec, serialize, restore, neighbor, label }; +}); diff --git a/desktop/src/style.css b/desktop/src/style.css new file mode 100644 index 0000000..7e3665d --- /dev/null +++ b/desktop/src/style.css @@ -0,0 +1,248 @@ +@font-face { + font-family: "OpenEmoji"; + src: url("../fonts/OpenEmoji-CBDT.ttf") format("truetype"); + font-display: block; +} + +:root { + --bg: #0f1115; + --bar: #171a21; + --tab: #1e222b; + --tab-active: #2a2f3a; + --fg: #d8dee9; + --dim: #8a93a6; + --accent: #5ea1ff; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--fg); + font: 13px system-ui, sans-serif; + overflow: hidden; + user-select: none; +} + +body { + display: flex; + flex-direction: column; +} + +#bar { + display: flex; + align-items: stretch; + height: 32px; + background: var(--bar); + flex: none; +} + +#tabs { + display: flex; + overflow-x: auto; + min-width: 0; + scrollbar-width: none; +} + +.tab { + display: flex; + align-items: center; + gap: 6px; + padding: 0 6px 0 10px; + max-width: 220px; + min-width: 90px; + background: var(--tab); + border-right: 1px solid var(--bar); + color: var(--dim); + cursor: default; +} + +.tab.active { + background: var(--tab-active); + color: var(--fg); + box-shadow: inset 0 -2px var(--accent); +} + +.tab img.kind { + width: 14px; + height: 14px; + filter: invert(0.8); + flex: none; +} + +.tab .label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +button.icon { + background: none; + border: 0; + padding: 0 8px; + cursor: pointer; + display: flex; + align-items: center; +} + +button.icon img { + width: 14px; + height: 14px; + filter: invert(0.7); +} + +button.icon:hover img { + filter: invert(1); +} + +.tab button.icon { + padding: 2px; + border-radius: 3px; +} + +.tab button.icon:hover { + background: #3a404d; +} + +#menu { + position: fixed; + z-index: 10; + min-width: 220px; + max-height: 70vh; + overflow-y: auto; + background: var(--tab); + border: 1px solid #333a47; + border-radius: 6px; + padding: 4px 0; + box-shadow: 0 8px 24px #0008; +} + +#menu .item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + cursor: pointer; +} + +#menu .item:hover { + background: var(--tab-active); +} + +#menu .item img { + width: 14px; + height: 14px; + filter: invert(0.8); +} + +#menu .item .key { + margin-left: auto; + color: var(--dim); + font-size: 11px; +} + +#menu .sep { + height: 1px; + margin: 4px 0; + background: #333a47; +} + +#menu .head { + padding: 4px 12px; + color: var(--dim); + font-size: 11px; + text-transform: uppercase; +} + +#views { + flex: 1; + position: relative; + min-height: 0; +} + +.view { + position: absolute; + inset: 0; + display: flex; +} + +/* Hidden tabs stay laid out, so their terminals keep a real size. */ +.view.inactive { + visibility: hidden; + pointer-events: none; +} + +.split { + display: flex; + flex: 1; + min-width: 0; + min-height: 0; +} + +.split.row { + flex-direction: row; +} + +.split.column { + flex-direction: column; +} + +.slot { + display: flex; + min-width: 0; + min-height: 0; +} + +.divider { + flex: none; + background: #2a2f3a; +} + +.split.row > .divider { + width: 4px; + cursor: col-resize; +} + +.split.column > .divider { + height: 4px; + cursor: row-resize; +} + +.divider:hover, +.divider.dragging { + background: var(--accent); +} + +.pane { + flex: 1; + min-width: 0; + min-height: 0; + padding: 4px 0 0 6px; + position: relative; + outline: 1px solid transparent; + outline-offset: -1px; +} + +.view.multi .pane.focused { + outline-color: #5ea1ff66; +} + +.pane .xterm { + height: 100%; +} + +body.dragging-row, +body.dragging-row * { + cursor: col-resize !important; +} + +body.dragging-column, +body.dragging-column * { + cursor: row-resize !important; +} diff --git a/desktop/test/layout-tests.js b/desktop/test/layout-tests.js new file mode 100644 index 0000000..0ab58e8 --- /dev/null +++ b/desktop/test/layout-tests.js @@ -0,0 +1,92 @@ +"use strict"; +const test = require("node:test"); +const assert = require("node:assert/strict"); +const L = require("../src/layout.js"); + +const shell = { kind: "shell" }; +const remote = (host, session = "main") => ({ kind: "remote", host, session }); + +test("split and remove keep pane identity and collapse to the sibling", () => { + const a = L.pane(shell); + const b = L.pane(remote("dev2")); + let root = L.split(a, a.id, "row", b); + assert.equal(root.type, "split"); + assert.equal(root.dir, "row"); + const c = L.pane(shell); + root = L.split(root, b.id, "column", c); + assert.deepEqual(L.panes(root).map((p) => p.id), [a.id, b.id, c.id]); + assert.equal(L.find(root, b.id), b); + root = L.remove(root, b.id); + assert.deepEqual(L.panes(root).map((p) => p.id), [a.id, c.id]); + assert.equal(root.dir, "row"); + root = L.remove(root, a.id); + assert.equal(root, c); + assert.equal(L.remove(root, c.id), null); +}); + +test("a split of a remote pane opens a fresh session on that host", () => { + const taken = [remote("dev2"), remote("dev2", "main-2"), remote("box")]; + assert.deepEqual(L.splitSpec(remote("dev2"), taken), remote("dev2", "main-3")); + assert.deepEqual(L.splitSpec(remote("new"), taken), remote("new", "main")); + assert.deepEqual(L.splitSpec(shell, taken), shell); + assert.deepEqual(L.splitSpec({ kind: "hqterm" }, taken), shell); +}); + +test("serialize then restore gives the same tree, specs and focus", () => { + const a = L.pane(remote("dev2", "work")); + const b = L.pane(shell); + const root = { ...L.split(a, a.id, "column", b), ratio: 0.3 }; + const saved = JSON.parse(JSON.stringify(L.serialize([{ root, focus: b.id }, { root: L.pane({ kind: "hqterm" }), focus: "x" }], 1))); + assert.equal(saved.version, 1); + assert.equal(saved.active, 1); + assert.equal(JSON.stringify(saved).includes('"id"'), false); + const { tabs, active } = L.restore(JSON.stringify(saved)); + assert.equal(active, 1); + assert.equal(tabs.length, 2); + const [t0] = tabs; + assert.equal(t0.root.dir, "column"); + assert.equal(t0.root.ratio, 0.3); + assert.deepEqual(L.panes(t0.root).map((p) => p.spec), [remote("dev2", "work"), shell]); + assert.equal(t0.focus, L.panes(t0.root)[1].id); + assert.deepEqual(L.panes(tabs[1].root)[0].spec, { kind: "hqterm" }); +}); + +test("restore rejects junk and unsafe specs", () => { + assert.deepEqual(L.restore("not json"), { tabs: [], active: 0 }); + assert.deepEqual(L.restore({ version: 2, tabs: [] }), { tabs: [], active: 0 }); + const { tabs } = L.restore({ + version: 1, + active: 99, + tabs: [ + { root: { type: "pane", spec: { kind: "remote", host: "-oProxyCommand=evil" } } }, + { root: { type: "pane", spec: { kind: "remote", host: "dev2", session: "$(rm -rf ~)" } } }, + { root: { type: "split", dir: "diagonal", a: {}, b: {} } }, + { root: { type: "split", dir: "row", ratio: 7, a: { type: "pane", spec: { kind: "exec", cmd: "x" } }, b: null } }, + ], + }); + assert.equal(tabs.length, 3); + assert.deepEqual(L.panes(tabs[0].root)[0].spec, shell); + assert.deepEqual(L.panes(tabs[1].root)[0].spec, remote("dev2", "main")); + assert.deepEqual(L.panes(tabs[2].root)[0].spec, shell); +}); + +test("neighbor picks the adjacent pane in each direction", () => { + // left | right-top / right-bottom + const rects = { + l: { left: 0, top: 0, right: 100, bottom: 100 }, + rt: { left: 100, top: 0, right: 200, bottom: 50 }, + rb: { left: 100, top: 50, right: 200, bottom: 100 }, + }; + assert.equal(L.neighbor(rects, "l", "right"), "rt"); + assert.equal(L.neighbor(rects, "rb", "left"), "l"); + assert.equal(L.neighbor(rects, "rt", "down"), "rb"); + assert.equal(L.neighbor(rects, "rb", "up"), "rt"); + assert.equal(L.neighbor(rects, "l", "left"), undefined); + assert.equal(L.neighbor(rects, "rt", "up"), undefined); +}); + +test("labels", () => { + assert.equal(L.label(remote("dev2")), "dev2"); + assert.equal(L.label(remote("dev2", "w")), "dev2:w"); + assert.equal(L.label(shell), "shell"); +}); diff --git a/install.sh b/install.sh index d5ceb20..9351719 100755 --- a/install.sh +++ b/install.sh @@ -1,29 +1,53 @@ #!/bin/sh # hqterm installer: https://hqterm.sh # -# curl -fsSL https://hqterm.sh/install | sh # hqterm + hqsh -# curl -fsSL https://hqterm.sh/install | sh -s -- --server # hqsh only (remote hosts) +# curl -fsSL https://hqterm.sh/install | sh # hqterm + hqsh (+ desktop app on a Linux desktop) +# curl -fsSL https://hqterm.sh/install | sh -s -- --desktop # also the desktop app, display or not +# curl -fsSL https://hqterm.sh/install | sh -s -- --server # hqsh only (remote hosts) # # Installs prebuilt binaries from the latest GitHub releases of # profullstack/hqterm and profullstack/hqsh into ~/.local/bin (or $HQTERM_BIN). -# No sudo, safe to run again. Pin versions with HQTERM_VERSION / HQSH_VERSION -# (e.g. v0.1.0). +# The desktop app (Linux x86_64/arm64 AppImage) goes to +# ~/.local/opt/hqterm-desktop with an app-launcher entry. No sudo, safe to run +# again. Pin versions with HQTERM_VERSION / HQSH_VERSION (e.g. v0.2.0). set -eu BIN="${HQTERM_BIN:-$HOME/.local/bin}" SERVER=0 +DESKTOP=auto for arg in "$@"; do case "$arg" in --server) SERVER=1 ;; + --desktop) DESKTOP=1 ;; + --no-desktop) DESKTOP=0 ;; --bin=*) BIN="${arg#--bin=}" ;; -h|--help) - echo "usage: install.sh [--server] [--bin=DIR]" - echo " --server install only hqsh (for hosts you connect to)" - echo " --bin=DIR install into DIR (default ~/.local/bin, or \$HQTERM_BIN)" + echo "usage: install.sh [--server] [--desktop|--no-desktop] [--bin=DIR]" + echo " --server install only hqsh (for hosts you connect to)" + echo " --desktop also install the hqterm desktop app (Linux AppImage)" + echo " --no-desktop never install the desktop app" + echo " (default: install it on Linux when a display is present)" + echo " --bin=DIR install into DIR (default ~/.local/bin, or \$HQTERM_BIN)" exit 0 ;; *) echo "hqterm install: unknown option $arg" >&2; exit 2 ;; esac done +if [ "$SERVER" = 1 ]; then + if [ "$DESKTOP" = 1 ]; then + echo "hqterm install: --server and --desktop do not mix" >&2 + exit 2 + fi + DESKTOP=0 +fi +DESKTOP_AUTO=0 +if [ "$DESKTOP" = auto ]; then + DESKTOP_AUTO=1 + if [ "$(uname -s)" = Linux ] && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then DESKTOP=1; else DESKTOP=0; fi +fi +if [ -n "${HQTERM_INSTALL_PARSE_ONLY:-}" ]; then + echo "bin=$BIN server=$SERVER desktop=$DESKTOP auto=$DESKTOP_AUTO" + exit 0 +fi say() { printf '%s\n' "hqterm install: $*"; } die() { printf '%s\n' "hqterm install: error: $*" >&2; exit 1; } @@ -59,35 +83,45 @@ TMP="$(mktemp -d 2>/dev/null || mktemp -d -t hqterm)" trap 'rm -rf "$TMP"' EXIT INT TERM mkdir -p "$BIN" || die "cannot create $BIN" -# install_one REPO NAME VERSION: download NAME-OS-ARCH, verify, install as BIN/NAME. -# Returns 1 (without exiting) when the release or asset does not exist. -install_one() { - repo="$1"; name="$2"; version="$3" - asset="$name-$OS-$ARCH" - if [ -n "$version" ]; then - base="https://github.com/profullstack/$repo/releases/download/$version" +# release_base REPO VERSION: the download URL prefix of a release. +release_base() { + if [ -n "$2" ]; then + echo "https://github.com/profullstack/$1/releases/download/$2" else - base="https://github.com/profullstack/$repo/releases/latest/download" + echo "https://github.com/profullstack/$1/releases/latest/download" fi - say "downloading $asset from github.com/profullstack/$repo${version:+ ($version)}" - if ! fetch "$base/$asset" "$TMP/$asset"; then - return 1 - fi - if fetch "$base/SHA256SUMS" "$TMP/SHA256SUMS.$name" 2>/dev/null; then - want="$(grep " \*\{0,1\}$asset\$" "$TMP/SHA256SUMS.$name" | cut -d' ' -f1 | head -n1)" - got="$(sha256 "$TMP/$asset")" +} + +# verify BASE ASSET REPO: check $TMP/ASSET against the release's SHA256SUMS. +verify() { + if fetch "$1/SHA256SUMS" "$TMP/SHA256SUMS.$2" 2>/dev/null; then + want="$(grep " \*\{0,1\}$2\$" "$TMP/SHA256SUMS.$2" | cut -d' ' -f1 | head -n1)" + got="$(sha256 "$TMP/$2")" if [ -z "$want" ]; then - say "warning: $asset is not listed in SHA256SUMS; not verified" + say "warning: $2 is not listed in SHA256SUMS; not verified" elif [ -z "$got" ]; then - say "warning: no sha256sum/shasum here; $asset not verified" + say "warning: no sha256sum/shasum here; $2 not verified" elif [ "$want" != "$got" ]; then - die "checksum mismatch for $asset (expected $want, got $got)" + die "checksum mismatch for $2 (expected $want, got $got)" else - say "verified $asset (sha256 $got)" + say "verified $2 (sha256 $got)" fi else - say "warning: no SHA256SUMS in the $repo release; $asset not verified" + say "warning: no SHA256SUMS in the $3 release; $2 not verified" fi +} + +# install_one REPO NAME VERSION: download NAME-OS-ARCH, verify, install as BIN/NAME. +# Returns 1 (without exiting) when the release or asset does not exist. +install_one() { + repo="$1"; name="$2"; version="$3" + asset="$name-$OS-$ARCH" + base="$(release_base "$repo" "$version")" + say "downloading $asset from github.com/profullstack/$repo${version:+ ($version)}" + if ! fetch "$base/$asset" "$TMP/$asset"; then + return 1 + fi + verify "$base" "$asset" "$repo" chmod 755 "$TMP/$asset" # macOS on Apple silicon refuses unsigned binaries: ad-hoc sign if needed. if [ "$OS" = darwin ] && ! "$TMP/$asset" --version >/dev/null 2>&1 && command -v codesign >/dev/null 2>&1; then @@ -100,6 +134,63 @@ install_one() { return 0 } +# install_desktop: the AppImage, an icon and an app-launcher entry (Linux only). +install_desktop() { + if [ "$OS" != linux ]; then + say "the desktop app is Linux-only for now; skipping it" + return 1 + fi + case "$ARCH" in + amd64) aarch=x86_64 ;; + *) aarch=aarch64 ;; + esac + asset="hqterm-desktop-linux-$aarch.AppImage" + base="$(release_base hqterm "${HQTERM_VERSION:-}")" + say "downloading $asset" + fetch "$base/$asset" "$TMP/$asset" || return 1 + verify "$base" "$asset" hqterm + dir="$HOME/.local/opt/hqterm-desktop" + mkdir -p "$dir" + chmod 755 "$TMP/$asset" + mv -f "$TMP/$asset" "$dir/hqterm-desktop.AppImage.new.$$" + mv -f "$dir/hqterm-desktop.AppImage.new.$$" "$dir/hqterm-desktop.AppImage" + say "installed $dir/hqterm-desktop.AppImage" + + icons="$HOME/.local/share/icons" + apps="$HOME/.local/share/applications" + mkdir -p "$icons" "$apps" + if fetch "$base/hqterm-desktop.png" "$TMP/hqterm.png" 2>/dev/null \ + || fetch "https://raw.githubusercontent.com/profullstack/openemoji/main/png/256/1f5a5-fe0f.png" "$TMP/hqterm.png" 2>/dev/null; then + mv -f "$TMP/hqterm.png" "$icons/hqterm.png" + fi + # AppImages mount through FUSE; without it they can unpack and run instead. + extra="" + if ! command -v fusermount >/dev/null 2>&1 && ! command -v fusermount3 >/dev/null 2>&1; then + extra=" --appimage-extract-and-run" + say "note: no FUSE (fusermount) here, so the launcher uses --appimage-extract-and-run" + fi + cat > "$apps/hqterm.desktop" </dev/null 2>&1; then update-desktop-database "$apps" >/dev/null 2>&1 || true; fi + for k in kbuildsycoca6 kbuildsycoca5; do + if command -v "$k" >/dev/null 2>&1; then "$k" >/dev/null 2>&1 || true; break; fi + done + say "added app launcher entry $apps/hqterm.desktop" + return 0 +} + STATUS=0 if [ "$SERVER" = 0 ]; then install_one hqterm hqterm "${HQTERM_VERSION:-}" || die "could not download hqterm for $OS-$ARCH (no release asset yet?)" @@ -112,6 +203,17 @@ if ! install_one hqsh hqsh "${HQSH_VERSION:-}"; then STATUS=0 fi +DESKTOP_OK=0 +if [ "$DESKTOP" = 1 ]; then + if install_desktop; then + DESKTOP_OK=1 + elif [ "$DESKTOP_AUTO" = 1 ] || [ "$OS" != linux ]; then + say "warning: the desktop app was not installed; hqterm and hqsh are" + else + die "could not download the desktop app for $OS-$ARCH (no release asset yet?)" + fi +fi + case ":$PATH:" in *":$BIN:"*) ;; *) @@ -125,5 +227,6 @@ if [ "$SERVER" = 1 ]; then say "done: hqsh is ready on this host" else say "done. Try: hqterm doctor then: hqterm" + if [ "$DESKTOP_OK" = 1 ]; then say "desktop app: run hqterm desktop or pick hqterm in your app launcher"; fi fi exit "$STATUS" diff --git a/package.json b/package.json index 91a29aa..e477ab5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hqterm", - "version": "0.1.0", + "version": "0.2.0", "description": "Terminal session manager: persistent hqsh sessions with images and HD emoji, built on hqtui.", "license": "MIT", "private": true, diff --git a/site/index.html b/site/index.html index 4623436..73c4572 100644 --- a/site/index.html +++ b/site/index.html @@ -79,6 +79,20 @@

Just SSH

+

hqterm desktop

+

Our own terminal window for Linux (KDE, GNOME, anything with a display): tabs, split panes that come back where you left them, and every hqtui/qc image and HD emoji drawn in full, even where Konsole cannot.

+
curl -fsSL https://hqterm.sh/install | sh -s -- --desktop
+

Then run hqterm desktop (or hqterm desktop --host dev2 --session main), or pick hqterm in your app launcher. Linux x86_64 and arm64 AppImage; the plain install adds it automatically on a Linux desktop. Direct download: hqterm-desktop-linux-x86_64.AppImage.

+ + + + + + + + +
Ctrl+Shift+T / WNew tab / close tab (Ctrl+Tab switches)
Ctrl+Shift+D / ESplit right / split down (a split of a remote pane opens a new hqsh session on that host)
Ctrl+Shift+ArrowsMove between panes (or one click)
Ctrl+Shift+XClose the pane
Ctrl+Shift+C / VCopy / paste
Ctrl+= / Ctrl+-Zoom
+

Quick start

hqterm                      # hosts from ~/.ssh/config, their sessions, one click to attach
 hqterm connect dev2 main     # straight into session "main" on dev2
diff --git a/src/desktop.ts b/src/desktop.ts
new file mode 100644
index 0000000..53b4f5f
--- /dev/null
+++ b/src/desktop.ts
@@ -0,0 +1,117 @@
+/**
+ * `hqterm desktop`: find the hqterm desktop app (an Electron terminal that
+ * shows hqtui/qc images and HD emoji) and launch it, detached.
+ */
+import { spawn } from "node:child_process";
+import { existsSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { validHostName } from "./hosts.ts";
+import { INSTALL_URL } from "./run.ts";
+
+export const DESKTOP_INSTALL = `curl -fsSL ${INSTALL_URL} | sh -s -- --desktop`;
+
+export interface DesktopOpts {
+  host?: string;
+  session?: string;
+}
+
+export type ParsedDesktopArgs = { ok: true; opts: DesktopOpts } | { ok: false; error: string };
+
+export const validSessionName = (s: string) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(s);
+
+/** `[--host H] [--session S]`, also `--host=H`. A session without a host is an error. */
+export function parseDesktopArgs(args: string[]): ParsedDesktopArgs {
+  const opts: DesktopOpts = {};
+  for (let i = 0; i < args.length; i++) {
+    const a = args[i]!;
+    const m = /^--(host|session)(?:=(.*))?$/.exec(a);
+    if (!m) return { ok: false, error: `unknown option ${a}` };
+    const value = m[2] ?? args[++i];
+    if (!value) return { ok: false, error: `--${m[1]} needs a value` };
+    if (m[1] === "host") {
+      if (!validHostName(value)) return { ok: false, error: `bad host name ${value}` };
+      opts.host = value;
+    } else {
+      if (!validSessionName(value)) return { ok: false, error: `bad session name ${value}` };
+      opts.session = value;
+    }
+  }
+  if (opts.session && !opts.host) return { ok: false, error: "--session needs --host" };
+  return { ok: true, opts };
+}
+
+/** Where the app may live, in the order we look. */
+export function desktopCandidates(env: NodeJS.ProcessEnv = process.env, platform = process.platform, home = homedir()): string[] {
+  const out: string[] = [];
+  if (env.HQTERM_DESKTOP) out.push(env.HQTERM_DESKTOP);
+  if (platform === "darwin") {
+    out.push("/Applications/hqterm.app", join(home, "Applications", "hqterm.app"));
+  } else {
+    const opt = join(home, ".local", "opt", "hqterm-desktop");
+    out.push(join(opt, "hqterm-desktop.AppImage"), join(opt, "hqterm-desktop"));
+  }
+  return out;
+}
+
+export function findDesktop(
+  env: NodeJS.ProcessEnv = process.env,
+  platform = process.platform,
+  home = homedir(),
+  exists: (p: string) => boolean = existsSync,
+): string | undefined {
+  return desktopCandidates(env, platform, home).find((p) => exists(p));
+}
+
+/** The argv that launches the app at `path`. */
+export function desktopLaunch(
+  path: string,
+  opts: DesktopOpts,
+  platform = process.platform,
+  hasFuse = true,
+): string[] {
+  const appArgs: string[] = [];
+  if (opts.host) appArgs.push("--host", opts.host);
+  if (opts.session) appArgs.push("--session", opts.session);
+  if (platform === "darwin" && path.endsWith(".app")) return ["open", "-n", "-a", path, "--args", ...appArgs];
+  const pre: string[] = [];
+  // AppImages mount with FUSE; without it they can still unpack and run.
+  if (path.endsWith(".AppImage") && !hasFuse) pre.push("--appimage-extract-and-run");
+  // Chromium's setuid sandbox is not set up inside an AppImage.
+  return [path, ...pre, ...(platform === "linux" ? ["--no-sandbox"] : []), ...appArgs];
+}
+
+export function hasDisplay(env: NodeJS.ProcessEnv = process.env, platform = process.platform): boolean {
+  return platform !== "linux" || Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
+}
+
+export async function desktop(args: string[]): Promise {
+  const parsed = parseDesktopArgs(args);
+  if (!parsed.ok) {
+    console.error(`hqterm desktop: ${parsed.error}\nusage: hqterm desktop [--host H] [--session S]`);
+    return 2;
+  }
+  const path = findDesktop();
+  if (!path) {
+    console.error(`hqterm desktop: the desktop app is not installed. Install it with:\n  ${DESKTOP_INSTALL}`);
+    return 127;
+  }
+  if (!hasDisplay()) {
+    console.error("hqterm desktop: no display ($DISPLAY / $WAYLAND_DISPLAY unset). Run it from your desktop session.");
+    return 2;
+  }
+  const hasFuse = Boolean(Bun.which("fusermount") || Bun.which("fusermount3"));
+  const [cmd, ...rest] = desktopLaunch(path, parsed.opts, process.platform, hasFuse);
+  const child = spawn(cmd!, rest, { detached: true, stdio: "ignore", env: { ...process.env, ELECTRON_DISABLE_SANDBOX: "1" } });
+  const failed = await new Promise((resolve) => {
+    child.once("error", resolve);
+    child.once("spawn", () => resolve(undefined));
+  });
+  if (failed) {
+    console.error(`hqterm desktop: could not start ${path}: ${failed.message}`);
+    return 1;
+  }
+  child.unref();
+  console.log(`hqterm desktop: started ${path}${parsed.opts.host ? ` (${parsed.opts.host}${parsed.opts.session ? `/${parsed.opts.session}` : ""})` : ""}`);
+  return 0;
+}
diff --git a/src/hostlist.ts b/src/hostlist.ts
new file mode 100644
index 0000000..d73ac2f
--- /dev/null
+++ b/src/hostlist.ts
@@ -0,0 +1,12 @@
+/**
+ * The host list the TUI shows, as plain names. Bundled for Node into
+ * desktop/lib/hosts.cjs so the desktop app's "+" menu lists the same hosts.
+ */
+import { loadUserHosts, mergeHosts, validHostName } from "./hosts.ts";
+import { sshConfigHosts } from "./sshconfig.ts";
+
+export function listHosts(): string[] {
+  return mergeHosts(sshConfigHosts(), loadUserHosts()).map((h) => h.name);
+}
+
+export { validHostName };
diff --git a/src/main.ts b/src/main.ts
index e786ea5..5db3a7c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,6 +5,7 @@
  * can show them.
  */
 import { emojiFontStatus, installEmojiFont, removeEmojiFont } from "@profullstack/hqtui";
+import { desktop } from "./desktop.ts";
 import { doctor } from "./doctor.ts";
 import { addUserHost, loadUserHosts, mergeHosts, validHostName } from "./hosts.ts";
 import { connect, findHqsh, installArgs, INSTALL_URL, pressEnter, runInteractive, connectArgs } from "./run.ts";
@@ -17,6 +18,9 @@ const HELP = `hqterm ${VERSION}: persistent terminal sessions that survive disco
 Usage:
   hqterm                         full-screen session manager
   hqterm connect  [name]   attach to (or create) session  on  via hqsh
+  hqterm desktop [--host H] [--session S]
+                                 open the hqterm desktop app (tabs, split panes,
+                                 images and HD emoji); optionally attached to H/S
   hqterm doctor [--try kitty|iterm]
                                  what this terminal supports, with a test image
   hqterm fonts install|status|remove
@@ -31,6 +35,7 @@ Keys in the manager:
 
 Hosts come from ~/.ssh/config and ~/.config/hqterm/hosts.json.
 Install: curl -fsSL ${INSTALL_URL} | sh
+Desktop app: curl -fsSL ${INSTALL_URL} | sh -s -- --desktop
 `;
 
 async function manager(): Promise {
@@ -118,6 +123,8 @@ export async function main(argv: string[]): Promise {
       }
       return connect(host, session);
     }
+    case "desktop":
+      return desktop(rest);
     case "doctor":
       return doctor(rest);
     case "fonts":
diff --git a/src/sshconfig.ts b/src/sshconfig.ts
index 54de69c..34b288c 100644
--- a/src/sshconfig.ts
+++ b/src/sshconfig.ts
@@ -3,9 +3,9 @@
  * is a concrete host (no `*`, `?` or `!` pattern). Order is kept, duplicates
  * are dropped. `Include` is followed when a reader is given.
  */
-import { readFileSync } from "node:fs";
+import { readdirSync, readFileSync } from "node:fs";
 import { homedir } from "node:os";
-import { join, isAbsolute } from "node:path";
+import { basename, dirname, join, isAbsolute } from "node:path";
 
 export type ReadFile = (path: string) => string | undefined;
 
@@ -74,12 +74,31 @@ function expandInclude(pattern: string, home: string): string[] {
   if (!isAbsolute(p)) p = join(home, ".ssh", p);
   if (!/[*?[]/.test(p)) return [p];
   try {
-    return [...new Bun.Glob(p).scanSync({ absolute: true, onlyFiles: true })].sort();
+    if (typeof Bun !== "undefined") return [...new Bun.Glob(p).scanSync({ absolute: true, onlyFiles: true })].sort();
+    return globBasename(p);
   } catch {
     return [];
   }
 }
 
+/** Without Bun (the desktop app runs this under Node): wildcards in the last path segment only. */
+export function globBasename(p: string, list: (dir: string) => string[] = (d) => readdirSync(d)): string[] {
+  const dir = dirname(p);
+  if (/[*?[]/.test(dir)) return [];
+  const re = new RegExp(
+    "^" +
+      basename(p)
+        .replace(/[.+^${}()|\\]/g, "\\$&")
+        .replace(/\*/g, "[^/]*")
+        .replace(/\?/g, "[^/]") +
+      "$",
+  );
+  return list(dir)
+    .filter((n) => re.test(n))
+    .sort()
+    .map((n) => join(dir, n));
+}
+
 const readText: ReadFile = (path) => {
   try {
     return readFileSync(path, "utf8");
diff --git a/test/desktop.test.ts b/test/desktop.test.ts
new file mode 100644
index 0000000..32b8baf
--- /dev/null
+++ b/test/desktop.test.ts
@@ -0,0 +1,111 @@
+import { describe, expect, test } from "bun:test";
+import { join } from "node:path";
+import { desktopCandidates, desktopLaunch, findDesktop, hasDisplay, parseDesktopArgs } from "../src/desktop.ts";
+import { globBasename } from "../src/sshconfig.ts";
+import { main } from "../src/main.ts";
+
+describe("hqterm desktop", () => {
+  test("parses --host and --session", () => {
+    expect(parseDesktopArgs([])).toEqual({ ok: true, opts: {} });
+    expect(parseDesktopArgs(["--host", "dev2", "--session=work"])).toEqual({ ok: true, opts: { host: "dev2", session: "work" } });
+    expect(parseDesktopArgs(["--session", "x"]).ok).toBe(false);
+    expect(parseDesktopArgs(["--host"]).ok).toBe(false);
+    expect(parseDesktopArgs(["--host", "-oProxyCommand=x"]).ok).toBe(false);
+    expect(parseDesktopArgs(["--host", "dev2", "--session", "a;b"]).ok).toBe(false);
+    expect(parseDesktopArgs(["dev2"]).ok).toBe(false);
+  });
+
+  test("looks in $HQTERM_DESKTOP, then the install dir (Linux) or /Applications (macOS)", () => {
+    expect(desktopCandidates({}, "linux", "/h")).toEqual([
+      "/h/.local/opt/hqterm-desktop/hqterm-desktop.AppImage",
+      "/h/.local/opt/hqterm-desktop/hqterm-desktop",
+    ]);
+    expect(desktopCandidates({ HQTERM_DESKTOP: "/x/app" }, "linux", "/h")[0]).toBe("/x/app");
+    expect(desktopCandidates({}, "darwin", "/h")).toEqual(["/Applications/hqterm.app", "/h/Applications/hqterm.app"]);
+    const have = new Set(["/h/.local/opt/hqterm-desktop/hqterm-desktop"]);
+    expect(findDesktop({}, "linux", "/h", (p) => have.has(p))).toBe("/h/.local/opt/hqterm-desktop/hqterm-desktop");
+    expect(findDesktop({}, "linux", "/h", () => false)).toBeUndefined();
+  });
+
+  test("launch argv", () => {
+    const app = "/h/.local/opt/hqterm-desktop/hqterm-desktop.AppImage";
+    expect(desktopLaunch(app, {}, "linux", true)).toEqual([app, "--no-sandbox"]);
+    expect(desktopLaunch(app, { host: "dev2", session: "w" }, "linux", false)).toEqual([
+      app,
+      "--appimage-extract-and-run",
+      "--no-sandbox",
+      "--host",
+      "dev2",
+      "--session",
+      "w",
+    ]);
+    expect(desktopLaunch("/Applications/hqterm.app", { host: "dev2" }, "darwin")).toEqual([
+      "open",
+      "-n",
+      "-a",
+      "/Applications/hqterm.app",
+      "--args",
+      "--host",
+      "dev2",
+    ]);
+  });
+
+  test("display check", () => {
+    expect(hasDisplay({}, "linux")).toBe(false);
+    expect(hasDisplay({ WAYLAND_DISPLAY: "wayland-0" }, "linux")).toBe(true);
+    expect(hasDisplay({}, "darwin")).toBe(true);
+  });
+
+  test("is a known command", async () => {
+    const prev = process.env.HQTERM_DESKTOP;
+    process.env.HQTERM_DESKTOP = "/nonexistent/hqterm-desktop";
+    const home = process.env.HOME;
+    process.env.HOME = "/nonexistent-home";
+    try {
+      // Not installed here: exits 127 with the install hint, not "unknown command" (2).
+      expect(await main(["desktop"])).toBe(127);
+      expect(await main(["desktop", "--bogus"])).toBe(2);
+    } finally {
+      if (prev === undefined) delete process.env.HQTERM_DESKTOP;
+      else process.env.HQTERM_DESKTOP = prev;
+      process.env.HOME = home;
+    }
+  });
+});
+
+describe("ssh Include globs without Bun.Glob", () => {
+  test("matches wildcards in the last segment", () => {
+    const list = () => ["work", "work.conf", "home.conf", ".hidden.conf"];
+    expect(globBasename("/h/.ssh/conf.d/*.conf", list)).toEqual(["/h/.ssh/conf.d/.hidden.conf", "/h/.ssh/conf.d/home.conf", "/h/.ssh/conf.d/work.conf"]);
+    expect(globBasename("/h/.ssh/conf.d/w?rk", list)).toEqual(["/h/.ssh/conf.d/work"]);
+    expect(globBasename("/h/*/x", list)).toEqual([]);
+  });
+});
+
+describe("install.sh flags", () => {
+  const run = (args: string[], env: Record = {}) => {
+    const p = Bun.spawnSync(["sh", join(import.meta.dir, "..", "install.sh"), ...args], {
+      env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: "/h", HQTERM_INSTALL_PARSE_ONLY: "1", ...env },
+    });
+    return { code: p.exitCode, out: p.stdout.toString().trim(), err: p.stderr.toString() };
+  };
+  const linux = process.platform === "linux";
+
+  test("defaults: desktop only on a Linux display", () => {
+    expect(run([]).out).toBe("bin=/h/.local/bin server=0 desktop=0 auto=1");
+    expect(run([], { DISPLAY: ":0" }).out).toBe(`bin=/h/.local/bin server=0 desktop=${linux ? 1 : 0} auto=1`);
+    expect(run([], { WAYLAND_DISPLAY: "wayland-0" }).out).toBe(`bin=/h/.local/bin server=0 desktop=${linux ? 1 : 0} auto=1`);
+  });
+
+  test("--desktop, --no-desktop, --server, --bin", () => {
+    expect(run(["--desktop"]).out).toBe("bin=/h/.local/bin server=0 desktop=1 auto=0");
+    expect(run(["--no-desktop"], { DISPLAY: ":0" }).out).toBe("bin=/h/.local/bin server=0 desktop=0 auto=0");
+    expect(run(["--server"], { DISPLAY: ":0" }).out).toBe("bin=/h/.local/bin server=1 desktop=0 auto=0");
+    expect(run(["--bin=/opt/b"]).out).toBe("bin=/opt/b server=0 desktop=0 auto=1");
+  });
+
+  test("bad combinations fail", () => {
+    expect(run(["--server", "--desktop"]).code).toBe(2);
+    expect(run(["--nope"]).code).toBe(2);
+  });
+});