commit 941ce4baabd93f0d30d83cc72b4042eec21dce06 Author: guanjihuan Date: Sat Sep 12 13:59:12 2026 +0800 update diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..277d86d --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# 剪贴板 + +基于 Electron 的轻量剪贴板管理器,灵感来自 [Ditto](https://github.com/sabrogden/Ditto)。 + +## 快速开始 + +环境要求:Windows 10 / 11 · Node.js ≥ 18 + +```bash +npm install +npm start +``` + +启动后程序常驻系统托盘。复制任意文本或图片即自动记录;单击托盘图标唤出窗口,选中条目后双击或按回车写入系统剪贴板,再切到目标窗口 `Ctrl+V` 粘贴。 + +如安装时 `better-sqlite3` 编译失败,可安装 [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) 后执行 `npm run rebuild`。 + +## 功能 + +- **自动记录**:默认每秒轮询系统剪贴板,文本和图片都自动入库 +- **收藏**:独立 Tab 永久存放,源记录被删除后仍可复制,上限 1000 条 +- **搜索**:实时过滤文本,支持中文子串 +- **置顶**:点过「复制」的项自动置顶,避免被自动清理或「清空历史」误删 +- **全局唤起**:默认 `Ctrl+Alt+V`,设置里可改或禁用 +- **主题**:6 套主题 + 5 种强调色,支持跟随系统 +- **开机自启动**、单实例、窗口置顶切换 + +## 快捷键 + +全局:默认 `Ctrl+Alt+V` 唤起或隐藏历史窗口(设置中可更改或禁用)。 + +主窗口内: + +| 快捷键 | 行为 | +| --- | --- | +| `↑` `↓` | 上下选择 | +| `PageUp` `PageDown` | 翻页 | +| `Home` `End` | 跳到首项或末项 | +| `Enter` | 复制当前选中项 | +| `1` ~ `9` | 直接复制对应项 | +| `Ctrl+L` | 聚焦搜索框 | +| `F5` | 刷新列表 | +| `Esc` | 清空搜索或隐藏窗口 | +| `F12` | 开发者工具 | + +设置窗口:`←` / `→` 切换标签页,`Esc` 关闭。 + +## 数据存储 + +默认存放在 `D:\Clipboard Data`,独立于项目目录: + +``` +D:\Clipboard Data\ +├── config.json +├── history.db +└── favorites.db +``` + +- D 盘不可用时自动降级到 `%USERPROFILE%\Clipboard Data` +- 设置 → 「更改…」可迁到其他位置;「打开文件夹」直达目录 +- 启动前设 `CLIPBOARD_APP_DATA_DIR=D:\some\dir` 可临时覆盖 +- 首次启动会自动从 `%USERPROFILE%\.clipboard-app` 迁移历史数据 + +## 开发 + +```bash +npm test # 单元测试(自动发现 test/*.test.js) +npm run test:ui # UI 测试(需 Electron 窗口) +npm run test:all # 上面两套一起跑 +npm run rebuild # 重新编译 better-sqlite3 到当前 Electron ABI +npm run build # 打包为 NSIS 安装包 +npm run build:dir # 仅生成解压目录 +``` + +测试由 `test/run.js` 自动发现,新增用例放入 `test/.test.js` 即可。 + +## 常见问题 + +**点选条目后没有自动粘贴** +设计如此:只写入系统剪贴板,需手动 `Ctrl+V`。避免隐藏窗口导致的焦点闪烁。 + +**全局快捷键无效** +组合键被其他程序占用。在设置中更换档位,或释放占用方后重启。 + +**看不到托盘图标** +Windows 11 默认折叠托盘图标,需在「设置 → 系统 → 通知」中开启。 + +**数据库占用或损坏** +可直接删除 `D:\Clipboard Data` 下的 db 文件,重启后自动重建(配置和收藏数据除外)。 + +## 已知限制 + +- 不自动粘贴,必须手动 `Ctrl+V` +- 收藏上限 1000 条 + +## 致谢 + +- 灵感来自 [Ditto](https://github.com/sabrogden/Ditto) +- 开发者:[关济寰](https://www.guanjihuan.com/about) diff --git a/config.js b/config.js new file mode 100644 index 0000000..8eeac95 --- /dev/null +++ b/config.js @@ -0,0 +1,247 @@ +'use strict'; +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { isSameDir } = require('./lib/same-dir'); + +const APP_DIRNAME = '.clipboard-app'; +const DEFAULT_DATA_DIR = 'D:\\Clipboard Data'; +// 默认目录不可用(D 盘不存在 / 只读)时降级到的目录。 +// 走 home 而不是 APPDATA:APPDATA 是 Roaming,重装系统 / 换账号会丢; +// home 下的文件夹随账号走,更符合「剪贴板历史跟着用户走」的预期。 +const FALLBACK_DATA_DIRNAME = 'Clipboard Data'; + +// v1.0 之前的数据目录:%USERPROFILE%\.clipboard-app +// 现在只作为"一次性迁移"的来源,不再是默认值。 +function legacyDataDir() { + return path.join(os.homedir(), APP_DIRNAME); +} + +/** + * 探测某个目录能不能真正写入。 + * + * `mkdirSync(p, { recursive: true })` 只能创建中间文件夹,盘符本身不存在 + * (如没 D 盘的机器)会直接抛 ENOENT —— 这正是启动失败的原因。盘符存在 + * 但只读(CD-ROM、网络盘、权限不足)时 mkdir 反而会过,必须再写一个探针 + * 文件才能拦住。任一步抛错都判为不可用。 + * + * 探针文件在 mkdir 成功后立即 unlink —— 不在用户目录下留垃圾。 + * 抛错时已经半创建的祖先目录无伤大雅,留着下次还能复用。 + * + * @param {string} dir + * @returns {boolean} + */ +function isDirUsable(dir) { + if (!dir) return false; + try { + fs.mkdirSync(dir, { recursive: true }); + const probe = path.join(dir, '.clipboard-write-test'); + fs.writeFileSync(probe, ''); + fs.unlinkSync(probe); + return true; + } catch (_) { + return false; + } +} + +// 默认数据目录固定在 D:\Clipboard Data。 +// 选固定路径而不是「项目根/data」是为了: +// 1. 装/卸/重装项目时数据不丢; +// 2. 项目目录可以放进云同步 / 临时盘而不被数据库占空间; +// 3. 重装系统前备份一个文件夹就够了。 +// D 盘不一定每台机器都有(笔记本、无光驱、D: 是不可写设备)—— +// 不可用时降级到 home 下的 Clipboard Data,保证首次启动一定成功。 +// 想用别的位置:设置 → 「更换目录」,或在启动前设环境变量 +// CLIPBOARD_APP_DATA_DIR(测试也用它隔离)。 +let _defaultDataDirCache = null; +function defaultDataDir() { + if (_defaultDataDirCache !== null) return _defaultDataDirCache; + const env = process.env.CLIPBOARD_APP_DATA_DIR; + if (typeof env === 'string' && env.trim()) { + _defaultDataDirCache = path.resolve(env.trim()); + return _defaultDataDirCache; + } + _defaultDataDirCache = isDirUsable(DEFAULT_DATA_DIR) + ? DEFAULT_DATA_DIR + : path.join(os.homedir(), FALLBACK_DATA_DIRNAME); + return _defaultDataDirCache; +} + +const DEFAULTS = { + schemaVersion: 3, + autoLaunch: false, + maxItems: 0, // 0 = 不限制(数据库最大保留条数) + displayLimit: 300, // 界面(历史 / 收藏)一次渲染的条数上限 + pollMs: 1000, + dataDir: defaultDataDir(), + theme: 'linear', + accent: 'purple', + followSystem: false, + alwaysOnTop: true, // 窗口置顶开关,默认开启 + hotkey: 'Ctrl+Alt+V', // 全局唤起面板快捷键;'' = 禁用 +}; + +const ALLOWED_MAX = [0, 200, 500, 1000]; // 0 = 不限制 +// 界面显示条数:只作用于渲染层,不裁数据库。不给「不限制」档, +// 避免一次建几千个 DOM 节点导致首屏卡顿。 +const ALLOWED_DISPLAY = [100, 300, 500, 1000]; +const ALLOWED_POLL = [500, 1000, 2000]; // 默认 1000,500/1000/2000 三档 +const ALLOWED_THEMES = ['linear', 'midnight', 'carbon', 'solarized', 'github', 'light']; +const ALLOWED_ACCENTS = ['purple', 'blue', 'green', 'orange', 'red']; +// 全局唤起快捷键预设。'' = 禁用;其余必须是 Electron globalShortcut +// 能解析的 accelerator。给白名单而不是自由输入:非法组合键 register +// 会抛异常,被别的程序占用的永远注册不上 —— 不如只给确定能用的。 +// 默认 Ctrl+Alt+V 而不是 Ctrl+Shift+V —— 后者是浏览器 / VS Code 的 +// 「粘贴为纯文本」,全局劫持掉会惹骂。 +const ALLOWED_HOTKEYS = ['', 'Ctrl+Alt+V', 'Ctrl+Shift+V', 'Ctrl+Alt+C', 'Alt+Shift+V']; + +// config.json 固定在默认目录,不随 dataDir 迁移 +function getConfigPath() { + return path.join(defaultDataDir(), 'config.json'); +} + +function sanitize(raw) { + const r = raw && typeof raw === 'object' ? raw : {}; + return { + autoLaunch: r.autoLaunch === true, + maxItems: ALLOWED_MAX.includes(r.maxItems) ? r.maxItems : DEFAULTS.maxItems, + displayLimit: ALLOWED_DISPLAY.includes(r.displayLimit) ? r.displayLimit : DEFAULTS.displayLimit, + pollMs: ALLOWED_POLL.includes(r.pollMs) ? r.pollMs : DEFAULTS.pollMs, + dataDir: (typeof r.dataDir === 'string' && r.dataDir.trim()) ? r.dataDir : defaultDataDir(), + theme: ALLOWED_THEMES.includes(r.theme) ? r.theme : DEFAULTS.theme, + accent: ALLOWED_ACCENTS.includes(r.accent) ? r.accent : DEFAULTS.accent, + followSystem: r.followSystem === true, + // 缺省视为开启 —— 老用户首次升上来也是"置顶" + alwaysOnTop: r.alwaysOnTop === undefined ? DEFAULTS.alwaysOnTop : r.alwaysOnTop === true, + // 老配置没有 hotkey 字段 → 回落默认(等于老用户升上来自动获得快捷键) + hotkey: ALLOWED_HOTKEYS.includes(r.hotkey) ? r.hotkey : DEFAULTS.hotkey, + schemaVersion: 3, // always forward-fill to current + }; +} + +function loadConfig() { + try { + const txt = fs.readFileSync(getConfigPath(), 'utf8'); + return sanitize(JSON.parse(txt)); + } catch (e) { + return sanitize({}); + } +} + +function saveConfig(cfg) { + const clean = sanitize(cfg); + const p = getConfigPath(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + const tmp = p + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(clean, null, 2), 'utf8'); + fs.renameSync(tmp, p); +} + +// 迁移时要搬的数据文件。 +// -shm 不搬:它是 WAL 的共享内存索引,SQLite 打开时会自己重建, +// 搬一个不匹配的过去反而更糟。db + -wal 一起搬,SQLite 能自行恢复。 +const DATA_FILES = ['history.db', 'history.db-wal', 'favorites.db', 'favorites.db-wal']; + +/** + * 一次性迁移:把老目录(%USERPROFILE%\.clipboard-app,或老 config.json 里 + * 指向的自定义目录)的数据复制到新的默认目录(D:\Clipboard Data)。 + * + * 只在新目录还是空的时候做,所以是幂等的;老文件保留不删,出问题可以回退。 + * 复制而不是移动 —— 万一新位置有权限问题,老数据还在。 + * + * 必须在拿到单实例锁之后调用:否则第二个实例会去复制正在被写的 WAL。 + * + * @returns {{migrated: boolean, reason?: string, from?: string, to?: string, files?: string[]}} + */ +function migrateLegacyData() { + const target = defaultDataDir(); + const legacy = legacyDataDir(); + // Windows 大小写不敏感:同目录不同写法必须判为相同,否则会自己迁自己 + if (isSameDir(target, legacy)) return { migrated: false, reason: 'same-dir' }; + + // 新目录已经在用了 → 不碰(只迁一次) + if (fs.existsSync(path.join(target, 'config.json')) || fs.existsSync(path.join(target, 'history.db'))) { + return { migrated: false, reason: 'target-in-use' }; + } + + let rawCfg = null; + try { rawCfg = JSON.parse(fs.readFileSync(path.join(legacy, 'config.json'), 'utf8')); } + catch (_) { rawCfg = null; } + const legacyCfg = rawCfg && typeof rawCfg === 'object' && !Array.isArray(rawCfg) + ? sanitize(rawCfg) + : null; + + // 老数据实际在哪:老 config 指到哪就从哪搬(用户可能手动换过目录), + // 没有 config 就按老默认目录找。dataDir 必须取**原始**值:sanitize 会把 + // 缺省字段补成新默认目录(= target),拿它当 src 就成了"从 target 搬到 + // target"—— 什么也没搬却报 migrated: true,老数据被永久跳过。 + let src = legacy; + if (legacyCfg && typeof rawCfg.dataDir === 'string' && rawCfg.dataDir.trim()) { + src = path.resolve(rawCfg.dataDir.trim()); + } + const hasDb = DATA_FILES.some((n) => fs.existsSync(path.join(src, n))); + if (!hasDb && !legacyCfg) return { migrated: false, reason: 'nothing-to-migrate' }; + + fs.mkdirSync(target, { recursive: true }); + const files = []; + for (const name of DATA_FILES) { + const from = path.join(src, name); + if (!fs.existsSync(from)) continue; + try { + fs.copyFileSync(from, path.join(target, name), fs.constants.COPYFILE_EXCL); + files.push(name); + } catch (_) { /* 单个文件搬不动不该拦住启动 */ } + } + + // 设置也跟着搬,但 dataDir 必须落到新目录 —— 否则原样写回去, + // 下次启动又跑回老地方,等于没迁。 + saveConfig({ ...(legacyCfg || {}), dataDir: target }); + return { migrated: true, from: src, to: target, files }; +} + +/** + * 在「跟随系统」开启时,主题按系统明暗二选一;关闭时直接用用户选的 theme。 + * + * 这个判断在主进程(broadcastTheme / theme:get / applyThemeChange)和潜在的 + * 测试里都会用到,集中到这里避免重复实现又跑偏。注意:raw 字段 cfg.theme + * 仍然要保留 —— 用户关掉「跟随系统」后要回到他之前选的那个主题。 + * + * 旧版把这段逻辑直接内联在 main.js 的 broadcastTheme / IPC 处理器里, + * 跟「跟随系统」开关的 IPC 顺序竞争:applyThemeChange 在 broadcast 之后 + * 把 raw theme 写回 res.theme,settings.js 拿到后用 res.theme 设 + * data-theme —— 覆盖了刚刚广播的 effective 主题,开关「跟随系统」后 + * 设置窗口主题卡的高亮和 DOM 不一致。修复方式见 main.js / settings.js。 + * + * @param {{theme: string, followSystem: boolean}} cfg + * @param {boolean} sysDark 当前系统是否处于深色模式 + * @returns {string} + */ +function effectiveTheme(cfg, sysDark) { + if (!cfg || !cfg.followSystem) return cfg && cfg.theme ? cfg.theme : DEFAULTS.theme; + return sysDark ? 'linear' : 'light'; +} + +/** + * 把每个 theme 映射到对应的窗口首帧背景色。 + * 用法:createFramelessWindow({ backgroundColor: themeBgColor(appConfig.theme) }) + * backgroundColor 只影响首帧;切主题后整个渲染进程重绘,无需重新设置。 + * 未知 theme 兜底到 light 背景(与 tokens.css 的 :root --bg-base 一致)。 + */ +function themeBgColor(theme) { + const map = { + linear: '#0E0E12', + midnight: '#08090B', + carbon: '#1A1D23', + solarized: '#002B36', + github: '#0D1117', + light: '#FAFAFA', + }; + return map[theme] || '#FAFAFA'; +} + +module.exports = { + DEFAULTS, ALLOWED_MAX, ALLOWED_DISPLAY, ALLOWED_POLL, ALLOWED_THEMES, ALLOWED_ACCENTS, + ALLOWED_HOTKEYS, + isDirUsable, defaultDataDir, legacyDataDir, getConfigPath, sanitize, loadConfig, saveConfig, + migrateLegacyData, themeBgColor, effectiveTheme, +}; \ No newline at end of file diff --git a/favorites.js b/favorites.js new file mode 100644 index 0000000..7df5d58 --- /dev/null +++ b/favorites.js @@ -0,0 +1,134 @@ +'use strict'; +const Database = require('better-sqlite3'); +const { checkAgainstCap } = require('./lib/favorites-cap'); +const { likePatterns } = require('./lib/search-plan'); + +function open(dbPath) { + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.exec(` + CREATE TABLE IF NOT EXISTS favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + text TEXT, + image BLOB, + preview TEXT, + source_clip_id INTEGER, + created_at REAL NOT NULL, + favorited_at REAL NOT NULL, + UNIQUE(source_clip_id) + ); + CREATE INDEX IF NOT EXISTS idx_fav_favorited_at ON favorites(favorited_at DESC); + `); + return db; +} + +function close(db) { + if (db) db.close(); +} + +// 列表不取 image BLOB —— 图片走 getImage() 按需拉。 +const LIST_COLS = `id, type, text, preview, source_clip_id, created_at, favorited_at, + (image IS NOT NULL) AS has_image`; + +// 每个连接一套预编译语句,避免每次调用都重新 prepare +function stmts(db) { + if (!db._stmt) { + db._stmt = { + insert: db.prepare(` + INSERT INTO favorites(type, text, image, preview, source_clip_id, created_at, favorited_at) + VALUES(?, ?, ?, ?, ?, ?, ?) + `), + getMeta: db.prepare(`SELECT ${LIST_COLS} FROM favorites WHERE id = ?`), + getImage: db.prepare('SELECT image FROM favorites WHERE id = ?'), + getRaw: db.prepare('SELECT * FROM favorites WHERE id = ?'), + bySource: db.prepare('SELECT id FROM favorites WHERE source_clip_id = ?'), + remove: db.prepare('DELETE FROM favorites WHERE id = ?'), + count: db.prepare('SELECT COUNT(*) AS n FROM favorites'), + list: db.prepare(`SELECT ${LIST_COLS} FROM favorites ORDER BY favorited_at DESC LIMIT ?`), + }; + } + return db._stmt; +} + +function rowToClip(row) { + if (!row) return null; + return { + id: row.id, + type: row.type, + text: row.text || '', + hasImage: row.has_image != null ? row.has_image === 1 : row.image != null, + preview: row.preview || '', + sourceClipId: row.source_clip_id, + createdAt: row.created_at, + favoritedAt: row.favorited_at, + }; +} + +function add(db, sourceClip) { + const cap = checkAgainstCap(db); + if (!cap.ok) { + const err = new Error(cap.message); + err.ok = false; err.code = 'FAV_LIMIT'; + throw err; + } + const now = Date.now() / 1000; + const info = stmts(db).insert.run( + sourceClip.type, + sourceClip.type === 'text' ? (sourceClip.text || '') : null, + sourceClip.type === 'image' ? (sourceClip.image || null) : null, + sourceClip.preview || '', + sourceClip.id, + sourceClip.createdAt || now, + now, + ); + return rowToClip(stmts(db).getMeta.get(info.lastInsertRowid)); +} + +function isFavorited(db, clipId) { + const row = stmts(db).bySource.get(clipId); + return row ? row.id : null; +} + +function remove(db, favId) { + return stmts(db).remove.run(favId).changes > 0; +} + +function get(db, favId) { + return rowToClip(stmts(db).getMeta.get(favId)); +} + +function getImage(db, favId) { + const row = stmts(db).getImage.get(favId); + if (!row || !row.image) return null; + return Buffer.from(row.image).toString('base64'); +} + +// 原始行(含 image BLOB),仅供主进程"复制收藏到系统剪贴板"用。 +// 字段形状(type / text / image)与 clips 表一致,可直接喂 lib/clip-payload。 +// 渲染进程不要用它 —— 图片走 getImage() 按需拉。 +function getRaw(db, favId) { + return stmts(db).getRaw.get(favId) || null; +} + +function count(db) { + return stmts(db).count.get().n; +} + +function list(db, query = '', limit = 5000) { + const s = stmts(db); + // 与历史搜索同语义:每个 token 独立子串 AND,% _ \ 已转义(lib/search-plan)。 + // 不转义时搜 "100%" 里的 % 会当通配符命中一片无关记录。 + const patterns = likePatterns(query); + if (!patterns.length) return s.list.all(limit).map(rowToClip); + const where = patterns.map(() => `text LIKE ? ESCAPE '\\'`).join(' AND '); + const rows = db.prepare( + `SELECT ${LIST_COLS} FROM favorites WHERE ${where} ORDER BY favorited_at DESC LIMIT ?` + ).all(...patterns, limit); + return rows.map(rowToClip); +} + +module.exports = { + open, close, rowToClip, add, isFavorited, remove, get, getImage, getRaw, count, list, +}; diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..4bae4d4 Binary files /dev/null and b/icon.ico differ diff --git a/keyhelper.js b/keyhelper.js new file mode 100644 index 0000000..2e8c679 --- /dev/null +++ b/keyhelper.js @@ -0,0 +1,174 @@ +/** + * keyhelper.js — 长驻 PowerShell 子进程。 + * + * ⚠ 非生产代码 —— 当前没有任何主进程路径 require 它或调用它。 + * 历史:早期版本的「点选一条 → 自动粘贴到目标窗口」功能走这条路, + * 需要 PowerShell 用 SendKeys 发 Ctrl+V。在 2026-07-30 那一轮把 + * 自动粘贴整体移除了(用户明确不想要自动粘贴触发的「窗口缩一下」 + * 动画),主进程只剩手动 Ctrl+V 这条路。 + * + * 用途(保留作备用 / bug 复现): + * - 在 Windows 上发 Ctrl+V(避开 robotjs/nut-js 之类的原生模块编译) + * - 通过 stdin/stdout JSON-RPC 与主进程通信,单进程常驻,延迟 ~5ms + * + * 不在 package.json 的 build.files 里 —— 打包后不会进用户机器。 + * 想恢复自动粘贴:把 require('./keyhelper') 加回 main.js,把发键的 + * 逻辑塞到 clips:movetop 处理器的"clipboard.writeText 之后、IPC 返回 + * 之前"那段窗口隐藏逻辑即可(参见 README「不自动粘贴」说明)。 + * + * 协议: + * in : {"id":1,"action":"sendCtrlV"} + * out: {"id":1,"ok":true} + */ + +'use strict'; + +const { spawn } = require('node:child_process'); + +const PS_SCRIPT = String.raw` +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Windows.Forms +while ($true) { + $line = [Console]::In.ReadLine() + if ([string]::IsNullOrEmpty($line)) { continue } + try { + $req = $line | ConvertFrom-Json + $id = $req.id + switch ($req.action) { + 'sendCtrlV' { + [System.Windows.Forms.SendKeys]::SendWait('^v') + [Console]::Out.WriteLine(('{"id":' + $id + ',"ok":true}')) + } + default { + [Console]::Out.WriteLine(('{"id":' + $id + ',"ok":false,"error":"unknown action"}')) + } + } + } catch { + [Console]::Out.WriteLine(('{"id":-1,"ok":false,"error":"' + ($_.Exception.Message -replace '"','\\"') + '"}')) + } +} +`; + +class KeyHelper { + constructor() { + this.id = 0; + this.pending = new Map(); + this.buffer = ''; + this._reconnectTimer = null; + this.disposed = false; + this._spawn(); + } + + _spawn() { + this.ps = spawn( + 'powershell.exe', + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', PS_SCRIPT], + { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } + ); + + this.ps.stdout.on('data', (chunk) => this._onStdout(chunk)); + this.ps.stderr.on('data', (chunk) => { + console.error('[keyhelper stderr]', chunk.toString()); + }); + this.ps.on('exit', (code) => { + console.error('[keyhelper] powershell exited with code', code); + this._rejectAll(new Error('keyhelper exited')); + this._reconnect(); + }); + } + + _reconnect(retryMs = 500) { + if (this.disposed || this._reconnectTimer) return; + const tryOnce = () => { + this._reconnectTimer = null; + if (this.disposed) return; + try { + this._spawn(); + } catch (e) { + console.error('[keyhelper] respawn failed:', e.message); + this._reconnectTimer = setTimeout(tryOnce, Math.min(retryMs * 2, 8000)); + } + }; + this._reconnectTimer = setTimeout(tryOnce, retryMs); + } + + _onStdout(chunk) { + this.buffer += chunk.toString('utf8'); + let nl; + while ((nl = this.buffer.indexOf('\n')) >= 0) { + const line = this.buffer.slice(0, nl).trim(); + this.buffer = this.buffer.slice(nl + 1); + if (!line) continue; + try { + const resp = JSON.parse(line); + if (resp.id === -1) { + console.error('[keyhelper error]', resp.error); + continue; + } + const cb = this.pending.get(resp.id); + if (cb) { + this.pending.delete(resp.id); + cb.resolve(resp); + } + } catch (e) { + // ignore parse error + } + } + } + + _send(action, params = {}, timeoutMs = 10000) { + return new Promise((resolve, reject) => { + if (!this.ps || !this.ps.stdin || this.ps.stdin.destroyed) { + return reject(new Error('keyhelper not running')); + } + const id = ++this.id; + let settled = false; + const t = setTimeout(() => { + if (settled) return; + settled = true; + this.pending.delete(id); + reject(new Error('keyhelper timeout')); + }, timeoutMs); + // 定时器句柄要存下来:dispose() / 进程退出时必须能取消, + // 否则挂起的请求会在 10 秒后抛一个没人接的 rejection, + // 同时这个 timer 还会一直吊着事件循环。 + this.pending.set(id, { + timer: t, + resolve: (v) => { if (settled) return; settled = true; clearTimeout(t); resolve(v); }, + reject: (e) => { if (settled) return; settled = true; clearTimeout(t); reject(e); }, + }); + try { + this.ps.stdin.write(JSON.stringify({ id, action, ...params }) + '\n'); + } catch (e) { + if (settled) return; + settled = true; + clearTimeout(t); + this.pending.delete(id); + reject(e); + } + }); + } + + sendCtrlV() { + return this._send('sendCtrlV'); + } + + _rejectAll(err) { + for (const [, p] of this.pending) p.reject(err); + this.pending.clear(); + } + + dispose() { + this.disposed = true; + if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } + this._rejectAll(new Error('keyhelper disposed')); + if (this.ps) { + // 别再因为我们自己的 kill 触发重连 + this.ps.removeAllListeners('exit'); + try { this.ps.stdin.end(); } catch (e) {} + try { this.ps.kill(); } catch (e) {} + } + } +} + +module.exports = KeyHelper; \ No newline at end of file diff --git a/lib/adaptive-poll.js b/lib/adaptive-poll.js new file mode 100644 index 0000000..081754b --- /dev/null +++ b/lib/adaptive-poll.js @@ -0,0 +1,8 @@ +'use strict'; + +function computePollMs(base, visible, { cap = 4000 } = {}) { + if (visible) return base; + return Math.min(base * 2, cap); +} + +module.exports = { computePollMs }; \ No newline at end of file diff --git a/lib/clip-payload.js b/lib/clip-payload.js new file mode 100644 index 0000000..71e2a7d --- /dev/null +++ b/lib/clip-payload.js @@ -0,0 +1,31 @@ +'use strict'; + +/** + * 把一行 clips 记录转成"要写进系统剪贴板的东西"。 + * + * 单独抽出来是因为这里踩过坑:copyClipToClipboard 里读的是 + * rowToClip() 的返回值上的 .image,而 rowToClip() 只产出 imageBase64, + * 于是 Buffer.from(undefined) 抛错 —— 图片条目的"复制"整个静默失败, + * 而且错误被 catch 吞掉,用户只会觉得"点了没反应"。 + * + * 这一层不碰 electron,可以直接单测。 + * + * @param {object} row 来自 `SELECT * FROM clips`(必须含 image 列) + * @returns {{kind:'text', text:string} | {kind:'image', buffer:Buffer}} + * @throws {Error} row 缺失或图片数据为空时 + */ +function clipPayload(row) { + if (!row) throw new Error('记录不存在'); + if (row.type === 'text') { + return { kind: 'text', text: row.text || '' }; + } + if (row.type === 'image') { + if (row.image == null) throw new Error('图片数据为空'); + const buffer = Buffer.isBuffer(row.image) ? row.image : Buffer.from(row.image); + if (buffer.length === 0) throw new Error('图片数据为空'); + return { kind: 'image', buffer }; + } + throw new Error(`未知的记录类型: ${row.type}`); +} + +module.exports = { clipPayload }; diff --git a/lib/echo-suppress.js b/lib/echo-suppress.js new file mode 100644 index 0000000..576c718 --- /dev/null +++ b/lib/echo-suppress.js @@ -0,0 +1,87 @@ +'use strict'; + +/** + * 剪贴板"回声抑制"状态机 —— 区分"应用自己写进剪贴板的内容"与"外部新复制"。 + * + * 纯逻辑,不依赖 Electron:main.js 的 pollClipboard / writePayloadToClipboard / + * initializeClipboardState 都用它。抽出来之前这份逻辑内联在 main.js 里, + * 测试只能克隆一份状态机自己测自己(main.js 改坏了测试照样绿)—— 而这是 + * 丢数据级的逻辑,必须有真保护。规则都从真实踩坑里固化,改动前先看 + * test/echo-suppress.test.js。 + * + * 规则: + * - 记**内容**而不是记 flag:记下我们写进去的内容,下一次轮询真正读到同样 + * 内容时才吞。占位 flag 会被"两次 tick 之间夹的外部复制"错误消费, + * 把那次外部复制静默吞掉(丢数据)。 + * - 读到不匹配的新内容时顺手清掉 pending,避免它误伤之后内容相同的外部 + * 复制(点复制 A → 外部复制 B → 外部又复制回 A,第二个 A 必须入库)。 + * - 文本被清空('')*不* 消化 pending:外部中途清空剪贴板不该让回声标记 + * 失效,否则用户接着复制同内容会重复入库。 + * - 基线(last)随每次"变化"更新;自己写入时同时预置 pending 和 last, + * 让下一个 tick 走"无变化"分支,连读图/编码的代价都不付。 + * 为此 markOwnWrite 返回一个 undo 闭包:**写入抛错时必须调用它**—— + * 失败时剪贴板上还是旧内容,基线却已预置成新值,不回滚的话下个 tick + * 会把真实的旧内容当成"新变化"再入一次库(旧内容早已在库里 → 重复条目)。 + */ +function createEchoSuppress() { + let last = null; // 上次见到的内容(变化检测基线);null = 还没见过 + let pending = null; // 我们自己写进去、等着"被读到"后吞掉的内容;null = 无 + return { + /** 启动采样:记住当前剪贴板内容当基线,不当新内容、不标记回声。 */ + seed(value) { last = value; }, + + /** + * 应用自己写剪贴板:预置基线(下一 tick 判"无变化")+ 记下回声内容。 + * 返回 undo 闭包:实际写入(clipboard.writeText / writeImage)抛错时 + * 调用它以回滚基线、清掉回声标记(见文件头最后一条规则)。 + */ + markOwnWrite(value) { + const prevLast = last; + pending = value; + last = value; + return () => { pending = null; last = prevLast; }; + }, + + /** 写入失败后调用:清回声标记,别吞掉用户接下来的真实复制。 */ + clearPending() { pending = null; }, + + /** 1×1/空图出现时重置基线(poll 侧按"没东西"处理,下一次任何图都算新)。 */ + resetBaseline() { last = null; }, + + /** + * pollClipboard 入库失败时调用:撤销上一次 observe 对基线/pending 的修改, + * 让下一次 tick 重新探测同一条内容。 + * + * 背景:observe 一旦判定 'new',就已经把 last 设成新值;如果跟着的 + * stmt.insertText / insertImage 抛错(SQLite 临时锁、磁盘满、字段非法……), + * 不回滚的话下一次 tick 读到同一条内容会判 'unchanged',**那条内容被 + * 永久吞掉** —— 直到用户重启(initializeClipboardState 重新 seed 基线) + * 才能恢复。回滚后下次 tick 会再判 'new' 再试一次入库,符合预期。 + * + * 一次 observe 只对应一次入库尝试:last 在入库失败之前已被 observe 改过, + * 但 pending 通常仍是 null(自己写入是 markOwnWrite 走的另一条路)—— + * 不区分地统一置空足够,逻辑简单也避免回退到 observe 之前的精确快照。 + */ + abortLastObserve() { last = null; pending = null; }, + + /** + * 喂入一次轮询读到的内容。 + * @returns {'unchanged'|'empty'|'suppressed'|'new'} + * unchanged —— 与基线相同(文本:调用方继续图片分支;图片:直接 return) + * empty —— 空串,剪贴板被清空(仅文本;不消化 pending) + * suppressed —— 是我们自己写进去的回声,已吞 + * new —— 外部新内容,调用方应入库 + */ + observe(value) { + if (value === last) return 'unchanged'; + last = value; + if (value === '') return 'empty'; + if (pending !== null && value === pending) { pending = null; return 'suppressed'; } + // 不匹配的新内容顺手消化 pending(见文件头规则 2) + if (pending !== null) pending = null; + return 'new'; + }, + }; +} + +module.exports = { createEchoSuppress }; diff --git a/lib/favorites-cap.js b/lib/favorites-cap.js new file mode 100644 index 0000000..3996024 --- /dev/null +++ b/lib/favorites-cap.js @@ -0,0 +1,15 @@ +'use strict'; + +const DEFAULT_MAX = 1000; + +/** + * Returns { ok: true } if current row count < max, else { ok: false, message }. + * Pure: takes a db-like object so tests can mock. + */ +function checkAgainstCap(db, max = DEFAULT_MAX) { + const n = db.prepare('SELECT COUNT(*) AS n FROM favorites').get().n; + if (n < max) return { ok: true }; + return { ok: false, message: `已达收藏上限 ${max} 条` }; +} + +module.exports = { checkAgainstCap, DEFAULT_MAX }; \ No newline at end of file diff --git a/lib/image-cap.js b/lib/image-cap.js new file mode 100644 index 0000000..8e9bc53 --- /dev/null +++ b/lib/image-cap.js @@ -0,0 +1,39 @@ +'use strict'; +const { nativeImage } = require('electron'); + +const PREVIEW_BYTES = 12 * 1024; // >12 KB → needs resizing for inline preview +const SOFT_CAP_BYTES = 2 * 1024 * 1024; // 2 MB +const RESIZE_WIDTH = 800; // 大图压到这个宽度,保证 SOFT_CAP_BYTES 不爆 + +/** + * Returns { buf, preview }. + * - buf: input buffer unchanged. + * - preview: null if the buffer is < PREVIEW_BYTES (12 KB), otherwise the + * literal string '[图片]'. The caller (main.js) is responsible for + * actually resizing when buf.length > SOFT_CAP_BYTES (2 MB); the + * preview marker is purely a signal that the buffer is non-trivial. + */ +function capImage(buf) { + const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf || []); + if (b.length < PREVIEW_BYTES) return { buf: b, preview: null }; + return { buf: b, preview: '[图片]' }; +} + +/** + * 把图片 buffer 按 SOFT_CAP_BYTES 收纳:超出的图压成 RESIZE_WIDTH 宽。 + * 三处共用:pollClipboard / bringFavoriteIntoHistory。 + * + * 返回 { buf, preview },preview 由 capImage 给出(无图/小图 = null), + * 给数据库的 preview 列用。 + */ +function normalizeImageForStorage(buf) { + const cap = capImage(buf); + // 小图/空图:直接原样入库 + if (!cap.preview) return cap; + if (cap.buf.length <= SOFT_CAP_BYTES) return cap; + // 大图:nativeImage 重新编码到 RESIZE_WIDTH 宽,PNG 重编码比原图小一个数量级 + const img = nativeImage.createFromBuffer(cap.buf); + return { buf: img.resize({ width: RESIZE_WIDTH }).toPNG(), preview: cap.preview }; +} + +module.exports = { capImage, normalizeImageForStorage, PREVIEW_BYTES, SOFT_CAP_BYTES, RESIZE_WIDTH }; \ No newline at end of file diff --git a/lib/log.js b/lib/log.js new file mode 100644 index 0000000..72b9b86 --- /dev/null +++ b/lib/log.js @@ -0,0 +1,8 @@ +'use strict'; +const on = process.env.CB_LOG === '1'; + +function devLog(...args) { if (on) console.log(...args); } +function devWarn(...args) { if (on) console.warn(...args); } +function devError(...args) { console.error(...args); } // errors always log + +module.exports = { devLog, devWarn, devError, isDev: on }; \ No newline at end of file diff --git a/lib/migrate.js b/lib/migrate.js new file mode 100644 index 0000000..24a35df --- /dev/null +++ b/lib/migrate.js @@ -0,0 +1,98 @@ +'use strict'; + +const { devWarn } = require('./log'); + +// clips -> clips_fts 的同步触发器。v2 / v3 共用。 +const FTS_TRIGGERS = ` + CREATE TRIGGER IF NOT EXISTS clips_ai AFTER INSERT ON clips BEGIN + INSERT INTO clips_fts(rowid, text) VALUES (new.id, new.text); + END; + CREATE TRIGGER IF NOT EXISTS clips_ad AFTER DELETE ON clips BEGIN + INSERT INTO clips_fts(clips_fts, rowid, text) VALUES('delete', old.id, old.text); + END; + -- 只在 text 真的变了才重建索引。没有这个 WHEN 的话,每次 + -- "UPDATE clips SET top_at = ?"(也就是每次复制/置顶)都会白白 + -- 把该行从 FTS 删掉再插回去。 + CREATE TRIGGER IF NOT EXISTS clips_au AFTER UPDATE ON clips + WHEN old.text IS NOT new.text BEGIN + INSERT INTO clips_fts(clips_fts, rowid, text) VALUES('delete', old.id, old.text); + INSERT INTO clips_fts(rowid, text) VALUES (new.id, new.text); + END; +`; + +// STEPS[i] upgrades the schema from version (i + 1) to (i + 2). +// STEPS[0] therefore is the v1 -> v2 step. +const STEPS = [ + // v1 -> v2: add FTS5 table + triggers (idempotent). + function v1_to_v2(db) { + db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS clips_fts USING fts5(text, content='clips', content_rowid='id'); + ${FTS_TRIGGERS} + INSERT INTO clips_fts(rowid, text) SELECT id, text FROM clips WHERE text IS NOT NULL; + `); + }, + + // v2 -> v3: 换成 trigram 分词器。 + // + // v2 用的是默认的 unicode61 分词器,它把一整串连续的中文当成**一个** token, + // 而 FTS5 的 MATCH 是整 token 匹配、不做子串匹配。结果:搜"公园"在 + // "今天去公园散步"里一条都搜不到 —— 对一个中文剪贴板工具来说等于搜索全废。 + // (v2 这一步之前因为索引下标算错从没真正执行过,所以线上一直悄悄退回 LIKE, + // 反而"正好"是对的;把 v2 修好之后这个坑才会暴露出来。) + // + // trigram 分词器按 3 字符滑窗切分,支持子串匹配,中文可用。 + // 代价:≤2 字符的查询它匹配不到,这部分由调用方退回 LIKE 处理。 + function v2_to_v3(db) { + db.exec(` + DROP TRIGGER IF EXISTS clips_ai; + DROP TRIGGER IF EXISTS clips_ad; + DROP TRIGGER IF EXISTS clips_au; + DROP TABLE IF EXISTS clips_fts; + CREATE VIRTUAL TABLE clips_fts USING fts5( + text, content='clips', content_rowid='id', tokenize='trigram' + ); + ${FTS_TRIGGERS} + INSERT INTO clips_fts(rowid, text) SELECT id, text FROM clips WHERE text IS NOT NULL; + `); + }, +]; + +/** + * Applies each pending schema step in order. + * + * The DB's own `PRAGMA user_version` is the source of truth for what has already + * been applied — not the config file, which only records the version the app + * *wants*. Passing a config-derived `fromVersion` would re-run every step on + * every launch (re-indexing the whole FTS table each time the app starts). + * + * Returns the version actually reached, so callers can tell whether a step was + * skipped because of an error. + */ +function migrate(db, fromVersion, toVersion) { + let current = fromVersion; + try { + const row = db.pragma('user_version', { simple: true }); + if (typeof row === 'number' && row > 0) current = row; + } catch (e) { + // Non-sqlite stub (tests) — fall back to the caller-supplied version. + } + + for (let v = current + 1; v <= toVersion; v++) { + const step = STEPS[v - 2]; + if (!step) continue; + try { + step(db); + current = v; + try { db.pragma(`user_version = ${v}`); } catch (e) {} + } catch (e) { + // Most likely FTS5 is not compiled into this SQLite build. Leave the + // version marker where it is so a later launch can retry, and let the + // caller fall back to LIKE-based search. + devWarn('[migrate] step', v, 'failed, leaving schema at v' + current + ':', e.message); + break; + } + } + return current; +} + +module.exports = { migrate, STEPS }; diff --git a/lib/notify-fanout.js b/lib/notify-fanout.js new file mode 100644 index 0000000..c27724b --- /dev/null +++ b/lib/notify-fanout.js @@ -0,0 +1,21 @@ +'use strict'; + +function createSubscribers() { + const clip = new Set(); + const fav = new Set(); + const theme = new Set(); + return { + addClip: (wc) => clip.add(wc), + addFav: (wc) => fav.add(wc), + addTheme: (wc) => theme.add(wc), + drop: (wc) => { clip.delete(wc); fav.delete(wc); theme.delete(wc); }, + notifyClip: (ch, payload) => { for (const wc of clip) try { wc.send(ch, payload); } catch {} }, + notifyFav: (ch, payload) => { for (const wc of fav) try { wc.send(ch, payload); } catch {} }, + notifyTheme: (ch, payload) => { for (const wc of theme) try { wc.send(ch, payload); } catch {} }, + get clip() { return clip; }, + get fav() { return fav; }, + get theme() { return theme; }, + }; +} + +module.exports = { createSubscribers }; \ No newline at end of file diff --git a/lib/poll-guard.js b/lib/poll-guard.js new file mode 100644 index 0000000..02721c5 --- /dev/null +++ b/lib/poll-guard.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * 轮询守卫:决定本 tick 是否进入图片分支。 + * + * 背景:默认 pollClipboard 每 tick 都跑 `clipboard.readImage().getBitmap()` + * + sha1,剪贴板上有 4K 截图时约 33 MB memcpy + ~40ms。先用 availableFormats() + * 探一下,文本剪贴板直接跳过图片分支,零开销。 + * + * 已知残留:当剪贴板确实有大图时仍然每 tick 重哈希。彻底解决需要启发式 + * 风险(尺寸相同可能漏抓变更)或订阅 clipboard-sequence-number(Electron + * 不暴露)。本次只做最稳妥的格式守卫。 + * + * @param {unknown} formats 来自 clipboard.availableFormats() 的列表 + * @returns {boolean} 是否需要读图片 + */ +function shouldReadImage(formats) { + if (!Array.isArray(formats)) return false; + for (const f of formats) { + if (typeof f === 'string' && f.startsWith('image/')) return true; + } + return false; +} + +module.exports = { shouldReadImage }; diff --git a/lib/same-dir.js b/lib/same-dir.js new file mode 100644 index 0000000..2add97b --- /dev/null +++ b/lib/same-dir.js @@ -0,0 +1,44 @@ +'use strict'; +const path = require('node:path'); +const fs = require('node:fs'); + +/** + * 把路径规整到 OS 真实指向(symlink / 8.3 短名 / 盘符大小写都消除)。 + * 失败时退回到 path.resolve —— 比如路径根本不存在,realpath 抛 ENOENT, + * 用 resolve 至少能让 path.resolve(a) === path.resolve(b) 这条兜底成立。 + * + * @param {string} p + * @returns {string} + */ +function real(p) { + try { return fs.realpathSync.native(p); } catch (_) { return path.resolve(p); } +} + +/** + * 判断两个路径是否指向同一个目录。 + * + * Windows 文件系统大小写不敏感:`D:\Clipboard Data` 与 `d:\clipboard data` + * 是同一个目录,必须按小写比较。原始 `path.resolve(a) === path.resolve(b)` + * 在大小写不一致时会误判为不同目录 —— settings:choose-dir 里这会让用户 + * 重选当前目录时绕过「已经是当前数据位置」提示,弹出「目标已有数据」 + * 迁移对话框自己迁自己。 + * + * 此外 NTFS 还做 Unicode-normalization 不敏感:`café` 的 NFC / NFD 两种写法 + * 指向同一文件。光是 toLowerCase 比较会把两种写法判成不同,触发自我迁移。 + * 走 realpathSync.native 让 OS 自己解析到内部规范形式再比较。 + * + * POSIX 大小写敏感、symlink 敏感:严格比较。 + * + * @param {string} a + * @param {string} b + * @returns {boolean} + */ +function isSameDir(a, b) { + const ra = real(a); + const rb = real(b); + return process.platform === 'win32' + ? ra.toLowerCase() === rb.toLowerCase() + : ra === rb; +} + +module.exports = { isSameDir }; diff --git a/lib/search-plan.js b/lib/search-plan.js new file mode 100644 index 0000000..cb08107 --- /dev/null +++ b/lib/search-plan.js @@ -0,0 +1,58 @@ +'use strict'; + +// trigram 分词器按 3 字符滑窗建索引,因此**任何**短于 3 字符的词都匹配不到。 +const TRIGRAM_MIN = 3; + +/** + * 转义 LIKE 元字符(% _ \),供 `LIKE ? ESCAPE '\'` 使用。 + * 用户输入里的 % / _ 不转义会被当通配符:搜 "100%" 变成匹配一切以 100 开头的串。 + */ +function escapeLike(s) { + return String(s).replace(/[\\%_]/g, (c) => '\\' + c); +} + +/** + * 把查询拆成「每个 token 一个 LIKE 模式」的数组(已转义)。 + * 多 token 是 AND 关系 —— 与 FTS MATCH 多短语的语义对齐, + * 且各 token 独立匹配,不要求原文里连空格一起逐字出现。 + * + * 空查询返回空数组(调用方按"列出全部"处理)。 + */ +function likePatterns(query) { + const trimmed = String(query == null ? '' : query).trim(); + if (!trimmed) return []; + return trimmed.split(/\s+/).filter(Boolean).map((tok) => `%${escapeLike(tok)}%`); +} + +/** + * 决定一个搜索词该走 FTS 还是退回 LIKE,并给出安全转义后的 FTS 查询串。 + * + * 背景:clips_fts 用的是 trigram 分词器(见 lib/migrate.js 的 v2->v3)。 + * 选它是因为默认的 unicode61 会把一整串中文当成一个 token,导致 + * "公园" 搜不到 "今天去公园散步" —— 中文场景下搜索基本全废。 + * trigram 解决了子串匹配,但代价是 ≤2 字符的词一条也匹配不到, + * 所以这类查询必须退回 LIKE("公园""会议""复制" 这种两字词非常常见)。 + * + * 另外,用户输入直接塞进 MATCH 会因 FTS5 保留字符(- " ( ) * ^ :)报语法错误, + * 所以每个 token 都要包成 FTS5 字符串字面量。 + * + * @param {string} query 用户原始输入 + * @returns {{mode:'all'} | {mode:'like', patterns:string[]} | {mode:'fts', match:string}} + */ +function planSearch(query) { + const patterns = likePatterns(query); + if (patterns.length === 0) return { mode: 'all' }; + + // 任何一个 token 太短 → 整个 MATCH 都会返回空(多 token 是 AND 关系), + // 所以只要有一个短 token 就整体退回 LIKE。 + // 注意用 [...tok] 按码点算长度,别用 .length(代理对会算成 2)。 + const trimmed = String(query == null ? '' : query).trim(); + const tokens = trimmed.split(/\s+/).filter(Boolean); + const tooShort = tokens.some((tok) => [...tok].length < TRIGRAM_MIN); + if (tooShort) return { mode: 'like', patterns }; + + const match = tokens.map((tok) => `"${tok.replace(/"/g, '""')}"`).join(' '); + return { mode: 'fts', match }; +} + +module.exports = { planSearch, likePatterns, escapeLike, TRIGRAM_MIN }; diff --git a/lib/settings-handlers.js b/lib/settings-handlers.js new file mode 100644 index 0000000..389e73a --- /dev/null +++ b/lib/settings-handlers.js @@ -0,0 +1,37 @@ +'use strict'; +const cfg = require('../config.js'); + +const SETTINGS_HANDLERS = { + autoLaunch: { + // 宽松强制转换:布尔、真值字符串('true' / 'yes')、1 都视为开启 + apply: (cur, v) => cfg.sanitize({ ...cur, autoLaunch: Boolean(v) }), + }, + maxItems: { + apply: (cur, v) => cfg.sanitize({ ...cur, maxItems: v }), + }, + displayLimit: { + // 界面显示条数 —— 只影响渲染层,不动数据库。运行时改完要通知所有窗口 + // 重拉,preload 的默认值才会跟着变(见 main.js settings:set 分支)。 + apply: (cur, v) => cfg.sanitize({ ...cur, displayLimit: v }), + }, + pollMs: { + apply: (cur, v) => cfg.sanitize({ ...cur, pollMs: v }), + }, + hotkey: { + // 预设白名单校验在 cfg.sanitize 里(非法值回落默认 Ctrl+Alt+V)。 + // 运行时的注册/解绑在 main.js settings:set 的 hotkey 分支, + // 那里才拿得到 globalShortcut。 + apply: (cur, v) => cfg.sanitize({ ...cur, hotkey: v }), + }, + // dataDir 刻意没有 handler:更换目录必须走 settings:choose-dir + // (关库 → 搬文件 → 校验 → 改配置 → 重启)。只在这里改 config.dataDir, + // 运行中的数据库连接仍指向旧目录 —— 内存与落盘分叉,重启后数据"消失"。 +}; + +function applyPatch(current, { key, value }) { + const h = SETTINGS_HANDLERS[key]; + if (!h) return { ...current }; + return h.apply(current, value); +} + +module.exports = { SETTINGS_HANDLERS, applyPatch }; \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..31fdbe5 --- /dev/null +++ b/main.js @@ -0,0 +1,1454 @@ +/** + * Electron 主进程 + * + * 职责: + * - 应用生命周期 + 单实例锁 + * - 系统托盘(含右键菜单,单击打开历史窗口) + * - 主窗口(无边框、置顶、Frameless) + * - 轮询剪贴板(默认 1000ms,可在设置里改)→ 去重 → 写 SQLite + * - IPC:list / search / movetop / delete / clear / favorites / theme / settings + * - 点选一条:写系统剪贴板 + 置顶(不自动粘贴,用户自己按 Ctrl+V) + */ + +'use strict'; + +const { + app, BrowserWindow, Tray, Menu, ipcMain, + clipboard, nativeImage, screen, dialog, shell, + nativeTheme, globalShortcut, +} = require('electron'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); + +const Database = require('better-sqlite3'); + +const configMod = require('./config'); +const favorites = require('./favorites'); +const { buildTrayMenuTemplate } = require('./tray-menu.js'); +const { devLog, devWarn } = require('./lib/log'); +const { normalizeImageForStorage } = require('./lib/image-cap'); +const { applyPatch } = require('./lib/settings-handlers'); +const { createSubscribers } = require('./lib/notify-fanout'); +const { migrate } = require('./lib/migrate'); +const { computePollMs } = require('./lib/adaptive-poll'); +const { shouldReadImage } = require('./lib/poll-guard'); +const { clipPayload } = require('./lib/clip-payload'); +const { planSearch, likePatterns } = require('./lib/search-plan'); +const { isSameDir } = require('./lib/same-dir'); +const { createEchoSuppress } = require('./lib/echo-suppress'); + + +// ---------- 配置 ---------- + +const APP_NAME = '剪贴板'; + +let appConfig = configMod.loadConfig(); +let MAX_ITEMS = appConfig.maxItems; // 运行时可变 +let POLL_MS = appConfig.pollMs; // 运行时可变 + +// 数据目录默认固定在 D:\Clipboard Data(见 config.js),可在设置里迁移, +// 或用环境变量 CLIPBOARD_APP_DATA_DIR 临时覆盖(测试也用它隔离)。 +// 一律走 appConfig.dataDir 现取,不缓存到常量 —— 迁移后旧值就是错的。 +// +// sessionDataDir:本会话内的临时目录。仅当用户配置的 dataDir 在本次启动 +// 时不可用(盘符缺失 / 权限不足)才被赋值;不影响磁盘上的 config.json。 +// 这样盘符恢复后下次启动会自动回到用户原本选的目录,不会因为一次"临 +// 时坏掉"把用户的偏好覆盖掉。 +let sessionDataDir = null; +function effectiveDataDir() { return sessionDataDir || appConfig.dataDir; } +function getDbPath() { return path.join(effectiveDataDir(), 'history.db'); } +function getFavDbPath() { return path.join(effectiveDataDir(), 'favorites.db'); } + + +// ---------- 存储 ---------- + +let db; +let favDb = null; +const stmt = {}; +// favorites 的动态 IN 语句按占位符个数缓存 +const favStmtCache = new Map(); + +let ftsAvailable = null; +function hasFts5(d) { + if (!d) return false; + if (ftsAvailable != null) return ftsAvailable; + try { d.prepare('SELECT 1 FROM clips_fts LIMIT 0').all(); ftsAvailable = true; } + catch { ftsAvailable = false; } + return ftsAvailable; +} + +// 列表/搜索不需要 image BLOB —— 单条最大 2MB,200 条能到几百 MB。 +// 只取渲染需要的列,图片走 clips:get-image 按需拉取。 +const listCols = (p = '') => + `${p}id, ${p}type, ${p}text, ${p}preview, ${p}top_at, ${p}created_at, (${p}image IS NOT NULL) AS has_image`; + +/** + * 排序:按最近活跃时间倒序 —— 无论是外部新复制进来(created_at = now), + * 还是用户点过「复制」(movetop 把 created_at 也置为 now),最新活跃的 + * 一律排在最上面。这是「第一个就是当前剪贴板内容」契约的根基: + * + * 1. 刚被复制进来的内容 → created_at = now → 排到顶 + * 2. 用户点过复制的项 → created_at = now(被 movetop 同步更新)→ 排到顶 + * 3. 其它项按 created_at DESC 自然排列 + * + * 用 id DESC 作 tiebreaker:created_at 用 ms 精度,同 ms 内多次变更 + * (理论上几乎不可能,但 poll tick + 用户点击落在同一 tick 上的极端 + * 场景下能撞上)给一个稳定次序。 + * + * top_at 仍然保留 —— 它现在只用于「trim / 清空历史」时保护被用户点过 + * 复制的项(见 trimOldest / clearNonTop),不再参与排序。 + */ +const orderBy = (p = '') => `${p}created_at DESC, ${p}id DESC`; + +function openDb() { + fs.mkdirSync(appConfig.dataDir, { recursive: true }); + ftsAvailable = null; // 换库后能力可能不同,重新探测 + clipCount = null; // 行数缓存绑定旧库,换库后必须回源重查 + db = new Database(getDbPath()); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); // WAL 下足够安全,省掉每次写的 fsync + db.exec(` + CREATE TABLE IF NOT EXISTS clips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, -- 'text' | 'image' + text TEXT, + image BLOB, + preview TEXT, + top_at REAL, -- NULL = 自然位置;非空 = 用户点过「复制」(trim / 清空历史 跳过) + created_at REAL NOT NULL -- 最近活跃时间:外部新复制 = 入库时间;点过「复制」= 同步更新到 now + ); + CREATE INDEX IF NOT EXISTS idx_clips_created_id ON clips(created_at DESC, id DESC); + `); + // 兼容不带 top_at 列的旧 DB(早期版本无置顶状态):补上 + try { db.exec(`ALTER TABLE clips ADD COLUMN top_at REAL`); } catch (e) {} + // 列表排序是 created_at DESC, id DESC(见 orderBy 的注释)—— 复合索引 + // idx_clips_created_id 正好覆盖 ORDER BY 的两列,扫到 LIMIT 就停,不会 + // 因为「id DESC 兜底」触发 TEMP B-TREE FOR LAST TERM OF ORDER BY。 + // 早期版本用过的 idx_clips_created_at(created_at DESC)(单列,不够覆盖) + // 和 idx_clips_top_created(top_at DESC, created_at DESC) 都已不再使用, + // 启动时主动 drop 一次,避免无意义的索引拖慢写入。 + try { db.exec(`DROP INDEX IF EXISTS idx_clips_top_created`); } catch (_) {} + try { db.exec(`DROP INDEX IF EXISTS idx_clips_created_at`); } catch (_) {} + // 启动时清掉所有 top_at —— "不是长期固定",重启就归零 + db.prepare(`UPDATE clips SET top_at = NULL WHERE top_at IS NOT NULL`).run(); + + migrate(db, 1, appConfig.schemaVersion); + + stmt.insertText = db.prepare(`INSERT INTO clips(type, text, preview, top_at, created_at) VALUES('text', ?, ?, ?, ?)`); + stmt.insertImage = db.prepare(`INSERT INTO clips(type, image, preview, top_at, created_at) VALUES('image', ?, ?, ?, ?)`); + stmt.count = db.prepare(`SELECT COUNT(*) AS n FROM clips`); + // 排序:created_at DESC, id DESC(最近活跃优先,见 orderBy 注释) + stmt.listAll = db.prepare(`SELECT ${listCols()} FROM clips ORDER BY ${orderBy()} LIMIT ?`); + stmt.get = db.prepare(`SELECT * FROM clips WHERE id = ?`); + stmt.getMeta = db.prepare(`SELECT ${listCols()} FROM clips WHERE id = ?`); + stmt.getImage = db.prepare(`SELECT image FROM clips WHERE id = ?`); + stmt.moveToTop = db.prepare(`UPDATE clips SET top_at = ?, created_at = ? WHERE id = ?`); + stmt.clearTop = db.prepare(`UPDATE clips SET top_at = NULL WHERE id = ?`); + stmt.delete = db.prepare(`DELETE FROM clips WHERE id = ?`); + stmt.clearNonTop = db.prepare(`DELETE FROM clips WHERE top_at IS NULL`); + // 之前每次入库都现场 prepare 一遍,热路径上白白编译 + stmt.trimOldest = db.prepare(` + DELETE FROM clips + WHERE id IN ( + SELECT id FROM clips WHERE top_at IS NULL + ORDER BY created_at ASC LIMIT ? + ) + `); + if (hasFts5(db)) { + stmt.searchFts = db.prepare(` + SELECT ${listCols('c.')} + FROM clips_fts f JOIN clips c ON c.id = f.rowid + WHERE clips_fts MATCH ? + ORDER BY ${orderBy('c.')} + LIMIT ? + `); + } +} + +function openFavDb() { + fs.mkdirSync(appConfig.dataDir, { recursive: true }); + favStmtCache.clear(); // 语句绑定在旧连接上,换库后必须丢掉 + favDb = favorites.open(getFavDbPath()); +} + +// 行数在内存里跟踪,避免每次入库都跑一次 COUNT(*) 全表扫描。 +// null = 尚未初始化(下次用到时回源查一次)。 +let clipCount = null; + +function getClipCount() { + if (clipCount == null) clipCount = stmt.count.get().n; + return clipCount; +} + +function enforceLimit() { + if (MAX_ITEMS <= 0) return; // 0 = 不限制 + const total = getClipCount(); + if (total <= MAX_ITEMS) return; + // 优先删除最旧的"自然位置"项(top_at IS NULL),不动置顶中的 + const removed = stmt.trimOldest.run(total - MAX_ITEMS).changes; + clipCount = total - removed; +} + +// 多 token 的 LIKE 搜索:每个 token 独立子串、AND 组合(与 FTS MATCH 多短语的 +// 语义对齐),不要求原文连空格一起逐字出现。patterns 已由 lib/search-plan +// 转义好(% _ \),这里一律带 ESCAPE '\'。语句形状随 token 数变化没法预编译 +// 缓存,现 prepare —— SQLite 编译这种简单查询是 µs 级,热路径可接受。 +function searchLikeRows(patterns, limit) { + if (!patterns.length) return stmt.listAll.all(limit); + const where = patterns.map(() => `text LIKE ? ESCAPE '\\'`).join(' AND '); + return db.prepare( + `SELECT ${listCols()} FROM clips WHERE ${where} ORDER BY ${orderBy()} LIMIT ?` + ).all(...patterns, limit); +} + +function makePreview(text, maxLen = 120) { + const flat = String(text || '').replace(/\s+/g, ' ').trim(); + return flat.length > maxLen ? flat.slice(0, maxLen - 1) + '…' : flat; +} + +// row 来自 listCols()(无 image 列,带 has_image)或 SELECT *(有 image 列)。 +// 两种都不在这里做 base64 —— 图片由渲染进程按需调 clips:get-image 拉。 +function rowToClip(row) { + if (!row) return null; + return { + id: row.id, + type: row.type, + text: row.text || '', + hasImage: row.has_image != null ? row.has_image === 1 : row.image != null, + preview: row.preview || '', + topAt: row.top_at, // null = 未被用户点过「复制」;非空 = 用户点过(trim / 清空历史 跳过) + createdAt: row.created_at, + }; +} + +function listWithFavorited(rows) { + if (!favDb || rows.length === 0) return rows; + const ids = rows.map(r => r.id); + // 按占位符个数缓存语句:列表长度只有少数几种取值(200 / 命中数), + // 之前每次列表都重新 prepare 一条动态 IN 语句。 + let st = favStmtCache.get(ids.length); + if (!st) { + st = favDb.prepare( + `SELECT source_clip_id, id FROM favorites WHERE source_clip_id IN (${ids.map(() => '?').join(',')})` + ); + if (favStmtCache.size < 64) favStmtCache.set(ids.length, st); + } + const favs = st.all(...ids); + if (favs.length === 0) return rows; + const favMap = new Map(favs.map(f => [f.source_clip_id, f.id])); + return rows.map(r => ({ + ...r, + favorited: favMap.has(r.id), + favId: favMap.get(r.id), + })); +} + + +// ---------- 剪贴板轮询 ---------- + +let pollTimer = null; +// 回声抑制:区分"我们自己写进剪贴板的"和"外部新复制"。两份独立状态机 +// (文本 / 图片指纹),规则与踩坑记录见 lib/echo-suppress.js。 +const textSuppress = createEchoSuppress(); +const imageSuppress = createEchoSuppress(); +let pollTick = 0; // 累计轮询次数 + +/** + * 图片指纹。走 getBitmap()(原始 BGRA,一次 memcpy)而不是 toPNG()。 + * + * toPNG() 要做完整的 PNG 压缩 —— 剪贴板上放一张 5MB 截图时,每个 tick 都要 + * 花 ~50ms 重新编码一遍,哪怕图根本没变。改成 hash 原始位图后,同样的判断 + * 快了将近一个数量级,而且仍然是精确比较(不会把两张同尺寸的不同图判成一样)。 + * toPNG() 现在只在确认图变了之后跑一次。 + */ +function imageFingerprint(img) { + const size = img.getSize(); + return crypto.createHash('sha1') + .update(`${size.width}x${size.height}:`) + .update(img.getBitmap()) + .digest('hex'); +} + +function pollClipboard() { + pollTick++; + + // 读文本 + let text = null; // null = 读文本失败(被其他进程独占 / 系统 API 抽风) + try { + text = clipboard.readText() || ''; + } catch (e) { + console.error('[poll] readText error:', e.message); + } + + // 每 20 次打一次心跳,方便确认轮询是否在跑(间隔 = 20 × pollMs) + if (pollTick % 20 === 1) { + devLog(`[poll] tick=${pollTick} textLen=${text == null ? -1 : text.length}`); + } + + // 文本变化检测(规则见 lib/echo-suppress.js)。 + // readText 抛错时(text === null)**不调 observe** —— 否则 last 会被改成 + // '' 触发 'empty' 直接退出,但剪贴板上可能有图片;图片分支被一并跳过, + // 用户刚复制的那张图就静默丢失。等下次 tick readText 恢复时再正常观察。 + if (text !== null) { + const verdict = textSuppress.observe(text); + if (verdict === 'empty') { + // 剪贴板被清空或原本就空,跳过(不消化回声标记) + return; + } + if (verdict === 'suppressed') { + devLog('[poll] suppressed (was our own paste)'); + return; + } + if (verdict === 'new') { + devLog(`[poll] NEW TEXT len=${text.length} preview=${JSON.stringify(text.slice(0, 60))}`); + try { + const now = Date.now() / 1000; + const info = stmt.insertText.run(text, makePreview(text), null, now); + if (clipCount != null) clipCount++; + enforceLimit(); + notifyNew(rowToClip(stmt.getMeta.get(info.lastInsertRowid))); + devLog(`[poll] inserted id=${info.lastInsertRowid}`); + } catch (e) { + // 入库失败:observe 已经把 last 改成新内容,不回滚下次 tick 会判 + // 'unchanged' 把这条内容**永久吞掉**(直到重启重新 seed)。abort + // 后下次 tick 再判 'new' 重试一次,符合预期。 + console.error('[poll] DB insert error:', e.message, e.stack); + textSuppress.abortLastObserve(); + } + return; + } + // verdict === 'unchanged' —— 文本没变,继续看图片分支 + } else { + devLog('[poll] readText skipped (last read failed), still probing image branch'); + } + + // 守卫:剪贴板若没有图片格式,跳过读图避免 ~33MB memcpy + sha1 + // (剪贴板只有文本时这是 100% 的常见情况)。 + // availableFormats 也用 try 裹住:它只是守卫,抛了(剪贴板被别的进程 + // 独占 / 系统 API 抽风)就跳过本 tick 的图片分支 —— 漏一张图的代价 + // 远小于未捕获异常打挂主进程(Node 默认 uncaughtException = 退出)。 + let formats; + try { + formats = clipboard.availableFormats(); + } catch (e) { + console.error('[poll] availableFormats error:', e.message); + return; + } + if (!shouldReadImage(formats)) return; + + // 读图片 + try { + const img = clipboard.readImage(); + if (!img || img.isEmpty()) return; + const size = img.getSize(); + if (size.width < 2 || size.height < 2) { + // 1×1 透明图,按"没东西"处理:重置基线,下一次任何真图都算"新" + imageSuppress.resetBaseline(); + return; + } + const hash = imageFingerprint(img); + // unchanged(没变,不编码)/ suppressed(自己的回声)都不入库 + if (imageSuppress.observe(hash) !== 'new') return; + + // 确认是新图,才付 PNG 编码的代价 + const buf = img.toPNG(); + devLog(`[poll] NEW IMAGE ${size.width}x${size.height} bytes=${buf.length}`); + try { + const now = Date.now() / 1000; + const { buf: resized, preview: capPreview } = normalizeImageForStorage(buf); + const preview = capPreview || '[图片]'; + const info = stmt.insertImage.run(resized, preview, null, now); + if (clipCount != null) clipCount++; + enforceLimit(); + notifyNew(rowToClip(stmt.getMeta.get(info.lastInsertRowid))); + devLog(`[poll] inserted id=${info.lastInsertRowid}`); + } catch (e) { + // 入库失败:跟文本分支对称地回滚基线,避免 PNG 编码的代价被白付 + + // 下次 tick 把同样的图当 unchanged 静默吞掉。 + console.error('[poll] DB insert error:', e.message, e.stack); + imageSuppress.abortLastObserve(); + } + } catch (e) { + console.error('[poll] readImage error:', e.message); + } +} + +function startPolling() { + stopPolling(); + // 立即跑一次,再开定时器 + pollClipboard(); + const visible = !mainWindow || mainWindow.isDestroyed() ? true : mainWindow.isVisible(); + const ms = computePollMs(POLL_MS, visible); + pollTimer = setInterval(pollClipboard, ms); + devLog(`[poll] started, interval=${ms}ms (visible=${visible})`); +} + +// 启动时给文本/图片基线喂一次当前剪贴板,**不**入库。 +// 之前这里只有 null;要是用户开了第二次(比如上午开过、下午又开), +// 第一次 tick 就会把剪贴板里"上次会话就在的"那条内容当作新内容重入一次。 +// seed 只设基线、不标记回声,后面用户的真实外部复制照常入库。 +function initializeClipboardState() { + let textLen = 0; + let hasImage = false; + try { + const text = clipboard.readText() || ''; + textSuppress.seed(text); + textLen = text.length; + } catch (e) { + devWarn('[poll] init readText failed:', e.message); + } + try { + const img = clipboard.readImage(); + if (img && !img.isEmpty()) { + const size = img.getSize(); + if (size.width >= 2 && size.height >= 2) { + // 1×1 透明图不值得记指纹(前面 poll 也会按"没东西"处理) + imageSuppress.seed(imageFingerprint(img)); + hasImage = true; + } + } + } catch (e) { + devWarn('[poll] init readImage failed:', e.message); + } + devLog(`[poll] init clipboard state seeded: textLen=${textLen} hasImage=${hasImage}`); +} + +function attachAdaptivePoll(window) { + if (!window) return; + window.on('show', () => startPolling()); + window.on('hide', () => startPolling()); +} +function stopPolling() { + if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } +} + +// ---------- 主题广播 ---------- + +function broadcastTheme() { + const sysDark = nativeTheme.shouldUseDarkColors; + const effective = configMod.effectiveTheme(appConfig, sysDark); + const payload = { + // 旧字段保留:渲染端旧版本只读 payload.theme。新代码应优先用 effectiveTheme。 + theme: effective, + effectiveTheme: effective, + rawTheme: appConfig.theme, + accent: appConfig.accent, + followSystem: appConfig.followSystem, + systemDark: sysDark, + }; + subscribers.notifyTheme('theme:system-changed', payload); +} + +nativeTheme.on('updated', () => { + devLog('[theme] system theme changed, broadcasting'); + broadcastTheme(); +}); + +function applyMaxItems(n) { + MAX_ITEMS = n; + try { enforceLimit(); } catch (e) { console.error('[cfg] enforceLimit failed:', e.message); } +} + +function applyPollMs(ms) { + POLL_MS = ms; + startPolling(); // startPolling 内部会先 stopPolling +} + +// 窗口置顶:不落盘,重启回到默认(开启)——与本功能开发前的行为一致 +let alwaysOnTop = true; + +function applyAlwaysOnTop(on) { + alwaysOnTop = on === true; + if (mainWindow && !mainWindow.isDestroyed()) { + try { mainWindow.setAlwaysOnTop(alwaysOnTop); } + catch (e) { devWarn('[window] setAlwaysOnTop failed:', e.message); } + } + return alwaysOnTop; +} + + +// ---------- 窗口 ---------- + +let mainWindow = null; + +function createFramelessWindow({ width, height, file, onClose, minimizable = false }) { + const w = new BrowserWindow({ + width, height, + show: false, + frame: false, + resizable: true, + minimizable, + maximizable: false, + skipTaskbar: true, + // 跟随当前的置顶开关。写死 true 会让用户关掉置顶、关窗、再开窗之后 + // 窗口又变回置顶,而按钮状态仍显示"已关闭"。 + alwaysOnTop, + // 背景色跟随主题:之前写死 #FFFFFF,5/6 的主题首帧会闪白。 + backgroundColor: configMod.themeBgColor(appConfig.theme), + // OS 级窗口图标:alt-tab、任务管理器进程图标、Win+Tab 任务视图。 + // 工厂被主窗口和设置窗口共用,一行设置两窗生效。 + icon: path.join(__dirname, 'icon.ico'), + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + }); + // 外链一律不许在应用内开窗 / 导航:关于页的链接由渲染进程拦下走 + // shell.openExternal(scheme 白名单在主进程),这里是纵深防御 —— 万一 + // 将来有链接漏过了点击拦截(中键点击 / 拖拽 / JS 直跳),也不能把 + // frameless 窗口顶成外部网页,或弹一个没有我们 preload 的裸窗口。 + w.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); + w.webContents.on('will-navigate', (e, url) => { + e.preventDefault(); + devWarn('[window] blocked in-page navigation to', url); + }); + w.loadFile(path.join(__dirname, 'renderer', file)); + forwardRendererLog(w.webContents); + if (onClose) w.on('closed', onClose); + return w; +} + +function createMainWindow() { + mainWindow = createFramelessWindow({ + // 默认高度按"刚好显示 5 条单行内容"。每个部件的实测高度(用 headless + // chromium 跑过,subpixel 取整后): + // 拖动区 42 #drag-bar = padding 8+7 + icon 26 + border 1 + // Tab 条 33 #tabs = padding 0 + .tab 27 + border 2 + .tab margin-bottom -1 + container border 1 + // (.tab 里 16px 高的 .tab-count 把行盒顶到 33) + // 搜索区 50 #search-row = padding 8+10 + input 32 + // 状态栏 33 #status = padding 7+7 + 18px kbd + border 1 + // ( 18px 高,比 11px×1.4 文字高,把容器撑到 33) + // 列表内边距 16 #list-wrap padding 8+8 + // 5×卡片 ~69.8 .card = padding 9+10 + head 24 + margin 4 + body 20.8 + border 2 + // (.action.fav 里 14px SVG 比 11px 文字高,把 head 撑到 24) + // 4×间距 24 #list gap: 6px + // 根容器边框 2 #root border-top+border-bottom + // 总和 42+33+50+33+16+5×69.8+4×6+2 = 549.4 → 550 正好够,第 5 + // 条底部贴底但不溢出,list-wrap 不出现滚动条(549 时 5 条全可见, + // 540 时第 5 条已被裁掉)。 + // 之前的注释算成 470 / 520 都没考虑到 .tab-count / kbd / SVG 把 + // 容器顶高的几个隐式 max-height,所以差了几十 px;用户反馈"可以 + // 高一些,刚好 5 条"就是要 5 条稳稳占满、无需滚动。 + // 默认尺寸: + // 宽度 500 —— 460 时 text-preview 一行只能显示 ~28 个汉字,长一点的 URL/ + // 路径/代码片段经常被截到第二行;500 让单行可显示 ~38 字,多数短文/ + // URL 一行内看完。设置窗口是 420,这里再宽 80 形成"主宽 / 设窄"的 + // 层级,仍能在 1080p 屏幕上居中不显拥挤。 + // 高度 550 —— 刚好显示 5 条单行卡片(详见上方注释的逐项实测)。 + // 用户拖窗可自由调整,跟旧行为一致。 + width: 500, height: 550, + file: 'index.html', + onClose: () => { mainWindow = null; }, + // 自动粘贴移除后没有代码会主动最小化它;保留 true 只是不拦截 + // Win+M 之类的系统行为,toggleWindow() 里有 isMinimized → restore 兜底。 + minimizable: true, + }); + mainWindow.webContents.on('before-input-event', (_e, input) => { + if (input.key === 'F12' || + ((input.control || input.meta) && input.shift && input.key.toLowerCase() === 'i')) { + mainWindow.webContents.toggleDevTools(); + } + }); + // 必须在这里挂,不能只在启动时挂一次 —— 用户关掉窗口后 toggleWindow() + // 会重建一个新的 BrowserWindow,旧窗口上的监听随之失效,自适应轮询就哑了。 + attachAdaptivePoll(mainWindow); + return mainWindow; +} + +function toggleWindow() { + // 窗口已销毁(用户主动关闭过)→ 重新创建一个并显示 + if (!mainWindow || mainWindow.isDestroyed()) { + createMainWindow(); + mainWindow.once('ready-to-show', () => { + positionAtTopCenter(); + mainWindow.show(); + mainWindow.focus(); + }); + return; + } + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + if (mainWindow.isMinimized()) mainWindow.restore(); + positionAtTopCenter(); + mainWindow.show(); + mainWindow.focus(); + // 之前这里是 mainWindow.reload() —— 每次打开都重新解析全部 CSS、重跑 + // preload 和 renderer.js,100~300ms 的可感延迟。渲染进程本来就订阅了 + // clips:refresh,推一条通知让它自己重拉列表就够了。 + notifyRefresh(); + } +} + +let settingsWindow = null; + +function openSettingsWindow() { + if (settingsWindow && !settingsWindow.isDestroyed()) { settingsWindow.show(); settingsWindow.focus(); return; } + settingsWindow = createFramelessWindow({ + // 480 是初版只有三四个开关时的高度;加了 Tab 页、「更改目录」「清空历史」 + // 和快捷键之后,「通用」页内容已有 ~550px,而 #settings-body 是 + // overflow:hidden(设计上设置窗口就该一屏放下、不滚动)—— 旧高度下 + // 底部整块被裁掉且没有滚动条,「清空历史」按钮根本点不到。 + // 690 = 内容 ~545 + 标题栏/Tab 条/状态栏 ~125 + 余量(见 settings-tabs 测试)。 + width: 420, height: 690, + file: 'settings.html', + onClose: () => { settingsWindow = null; }, + }); + settingsWindow.webContents.on('before-input-event', (_e, input) => { if (input.key === 'F12') settingsWindow.webContents.toggleDevTools(); }); + settingsWindow.show(); +} + +function positionAtTopCenter() { + if (!mainWindow) return; + const display = screen.getPrimaryDisplay(); + const wa = display.workArea; + const w = mainWindow.getBounds().width; + const x = Math.round(wa.x + (wa.width - w) / 2); + const y = Math.round(wa.y + wa.height * 0.18); + mainWindow.setPosition(x, y); +} + + +// ---------- 全局快捷键 ---------- + +// 当前实际注册成功的 accelerator。改键 / 退出时用它精确解绑。 +// '' = 此刻没注册任何东西(配置为禁用,或注册失败)。 +let registeredHotkey = ''; + +/** + * 按 appConfig.hotkey 重新注册全局唤起快捷键 → toggleWindow()。 + * + * 幂等:先解绑上一次注册成功的键,再注册新键。启动、设置里改键两条路 + * 都走这里。返回 true = 生效(注册成功,或配置就是禁用);false = 想注册 + * 但没成(accelerator 被别的程序占用 / 无效),设置界面据此提示用户。 + * + * 必须在 app.whenReady() 之后调用 —— globalShortcut 在这之前不可用。 + */ +function applyHotkey() { + // 记下旧的(成功注册过、且这次想换)—— 失败时把它注册回去。 + // 不留这个快照的话:从能用的 'A' 改成被占用的 'B' 会让用户 + // 暂时完全没有全局快捷键(unregister A 已成事实,B 又注册不上)。 + const prev = registeredHotkey; + if (prev) { + try { globalShortcut.unregister(prev); } catch (_) {} + registeredHotkey = ''; + } + const acc = appConfig.hotkey; + if (!acc) return true; // 配置为禁用 —— 不是失败 + const tryRegister = (target) => { + try { + // register 同步返回 boolean:false = 组合键已被占用,回调永远不会触发 + return globalShortcut.register(target, () => toggleWindow()); + } catch (_) { + // accelerator 格式非法时 register 会抛异常(而不是返回 false) + return false; + } + }; + if (tryRegister(acc)) { + registeredHotkey = acc; + devLog(`[hotkey] registered ${acc}`); + return true; + } + devWarn(`[hotkey] register failed (in use or invalid): ${acc}`); + if (prev && tryRegister(prev)) { + registeredHotkey = prev; + devLog(`[hotkey] recovered previous ${prev} after failed re-register`); + return false; // 仍然告诉设置界面「这次没注册上」 + } + return false; +} + + +// ---------- 托盘 ---------- + +let tray = null; +let trayIconCache = null; + +function getTrayIcon() { + if (trayIconCache) return trayIconCache; + // 优先用随包发布的 icon.ico(多尺寸 16~256),缺失或解析失败时回退程序化绘制 + try { + const iconPath = path.join(__dirname, 'icon.ico'); + if (fs.existsSync(iconPath)) { + const img = nativeImage.createFromPath(iconPath); + if (!img.isEmpty()) { + trayIconCache = img; + return trayIconCache; + } + devWarn('[tray] icon.ico loaded as empty image, falling back to drawn icon'); + } else { + devWarn('[tray] icon.ico not found at', iconPath, ', falling back to drawn icon'); + } + } catch (e) { + devWarn('[tray] load icon.ico failed:', e.message, ', falling back to drawn icon'); + } + trayIconCache = buildTrayIcon(); + return trayIconCache; +} + +function buildTrayIcon() { + // 64×64 程序化绘制 + const size = 64; + const img = nativeImage.createEmpty(); + const buf = Buffer.alloc(size * size * 4); + const setPx = (x, y, r, g, b, a = 255) => { + const i = (y * size + x) * 4; + buf[i] = b; buf[i + 1] = g; buf[i + 2] = r; buf[i + 3] = a; + }; + + // 圆角蓝底 + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dx = Math.max(0, Math.max(4 - x, x - (size - 5))); + const dy = Math.max(0, Math.max(4 - y, y - (size - 5))); + const r2 = dx * dx + dy * dy; + if (r2 <= 14 * 14) setPx(x, y, 59, 130, 246); // #3B82F6 + else setPx(x, y, 0, 0, 0, 0); + } + } + // 顶部夹子 + for (let y = 8; y < 16; y++) + for (let x = 22; x < 42; x++) setPx(x, y, 255, 255, 255); + // 下方白板 + for (let y = 20; y < 52; y++) + for (let x = 14; x < 50; x++) setPx(x, y, 255, 255, 255); + // 内层蓝线 + for (let x = 20; x < 44; x++) { setPx(x, 30, 59, 130, 246); setPx(x, 36, 59, 130, 246); } + for (let x = 20; x < 38; x++) setPx(x, 42, 59, 130, 246); + + return nativeImage.createFromBuffer(buf, { width: size, height: size }); +} + +// ---------- 应用图标(标题栏左上角 + OS 窗口图标) ---------- + +let appIconDataUrlCache = null; + +/** + * 把 icon.ico 转成 32×32 PNG data URL,给渲染进程当 `` 用。 + * + * 与 getTrayIcon 独立缓存:托盘要的是 nativeImage(喂给 Tray.setImage), + * 标题栏要的是 data URL(浏览器渲染),且目标尺寸也不同。 + * + * 缓存命中空字符串表示"曾经加载失败",避免每个窗口启动时都重新尝试。 + */ +function getAppIconDataUrl() { + if (appIconDataUrlCache !== null) return appIconDataUrlCache; + try { + const iconPath = path.join(__dirname, 'icon.ico'); + if (!fs.existsSync(iconPath)) throw new Error('icon.ico not found'); + const img = nativeImage.createFromPath(iconPath); + if (img.isEmpty()) throw new Error('empty image'); + // 32×32:标题栏 CSS 显示 16px,2x HiDPI 下仍锐利。 + // .ico 内置最大 256×256,resize 是高质量降采样。 + const resized = img.resize({ width: 32, height: 32, quality: 'best' }); + appIconDataUrlCache = resized.toDataURL(); + return appIconDataUrlCache; + } catch (e) { + devWarn('[app-icon] failed to load icon.ico:', e.message); + appIconDataUrlCache = ''; + return ''; + } +} + +// ---------- 主题元数据 ---------- + +function applyThemeChange(patch) { + appConfig = configMod.sanitize({ ...appConfig, ...patch }); + // 落盘失败要带给 UI(同 settings:set):本次会话生效、重启还原,得让用户知道。 + let saved = true; + try { configMod.saveConfig(appConfig); } + catch (e) { saved = false; console.error('[theme] saveConfig failed:', e.message); } + broadcastTheme(); + // 同步返回 effectiveTheme —— settings.js 拿到后要用它设 data-theme, + // 不用 raw 主题。返回的 rawTheme 字段给 localStorage / 测试 / 调试用。 + const sysDark = nativeTheme.shouldUseDarkColors; + const effective = configMod.effectiveTheme(appConfig, sysDark); + return { + theme: effective, + effectiveTheme: effective, + rawTheme: appConfig.theme, + accent: appConfig.accent, + followSystem: appConfig.followSystem, + systemDark: sysDark, + saved, + }; +} + +function buildTray() { + if (tray) { try { tray.destroy(); } catch (_) {} tray = null; } + tray = new Tray(getTrayIcon()); + tray.setToolTip(APP_NAME); + tray.setContextMenu(Menu.buildFromTemplate(buildTrayMenuTemplate({ + onShow: () => toggleWindow(), + onSettings: () => openSettingsWindow(), + onQuit: () => app.quit(), + }))); + tray.on('click', () => toggleWindow()); + // 不要同时挂 double-click:单击触发 click → 再过 ~500ms 又触发 double-click, + // 两次 toggleWindow 让用户「双击托盘」变「开一下立刻关掉」。 + // 单击是主流用法,需要双击唤起的用户极少;硬要可托 tray.setIgnoreDoubleClickEvents(true)。 +} + + +// ---------- 复制到剪贴板 ---------- + +// 共用的"载荷 → 系统剪贴板"一步,带回声抑制:记下写进去的内容, +// 让轮询在真正读到它时吞掉,避免自己的写入被当成新内容入库。 +function writePayloadToClipboard(payload) { + // markOwnWrite 预置基线 + 回声标记,返回的 undo 供写入失败时回滚: + // 失败时剪贴板上还是旧内容,基线却已是新值,不回滚下个 tick 会把真实 + // 旧内容当"新变化"重复入库。写入成功则基线保持预置,下一 tick 直接走 + // "无变化"分支,连读图/编码的代价都不付。 + if (payload.kind === 'text') { + // 记下"我们写进去的内容",poll 真正读到这串内容时才吞。 + // 比占位 flag 更严:中间夹了外部复制也不会误吞。 + const undo = textSuppress.markOwnWrite(payload.text); + try { clipboard.writeText(payload.text); } + catch (e) { undo(); throw e; } + } else { + const img = nativeImage.createFromBuffer(payload.buffer); + if (img.isEmpty()) throw new Error('图片解码失败'); + // 必须和 pollClipboard 用同一套指纹(原始位图,不是 PNG 字节), + // 否则下一个 tick 会把我们自己写进去的图当成新图。 + const undo = imageSuppress.markOwnWrite(imageFingerprint(img)); + try { clipboard.writeImage(img); } + catch (e) { undo(); throw e; } + } +} + +// 把某条历史记录写入系统剪贴板(不触发重复入库) +function copyClipToClipboard(clipId) { + const row = stmt.get.get(clipId); + if (!row) return false; + try { + writePayloadToClipboard(clipPayload(row)); + devLog(`[clipboard] copied id=${clipId} type=${row.type}`); + return true; + } catch (e) { + // 失败时清掉刚记下的 echo 标记,否则会吞掉用户接下来的一次真实复制 + textSuppress.clearPending(); + imageSuppress.clearPending(); + console.error('[clipboard] copy failed:', e.message); + return false; + } +} + +// 把收藏自身存下的内容写入系统剪贴板。 +// 收藏与历史分库,favId 和历史 id 是两套独立自增数 —— 绝不能拿 favId +// 走 copyClipToClipboard,那会按同号去 clips 表查到一条无关的历史记录 +// (两个库都有 id=1 时必中),把错误内容静默写进剪贴板。 +function copyFavToClipboard(favId) { + const row = favorites.getRaw(favDb, favId); + if (!row) return false; + try { + writePayloadToClipboard(clipPayload(row)); + devLog(`[clipboard] copied fav id=${favId} type=${row.type}`); + } catch (e) { + textSuppress.clearPending(); + imageSuppress.clearPending(); + console.error('[clipboard] fav copy failed:', e.message); + return false; + } + // 收藏 Tab 复制也要在历史 Tab 留一份 —— 收藏与历史分库,源记录可能已 + // 被清理(那才是用户去收藏 Tab 点复制的原因)。源还在 → 置顶(与历史 + // Tab 复制行为对齐),源不在 / 无源 → 当作新内容入库。 + try { + bringFavoriteIntoHistory(row); + } catch (e) { + // 入库失败不能把整个复制操作搞砸 —— 剪贴板已经写好了, + // 用户最差就是看到「历史 Tab 没冒新条目」,比「复制失败」轻得多。 + console.error('[clipboard] fav copy → history bridge failed:', e.message); + } + return true; +} + +// 把收藏的内容带到历史 Tab。源记录还在 → 置顶;不在 → 当新条目入库。 +// 入库路径与 pollClipboard 一致:clipCount++ / enforceLimit / notifyNew, +// 让 poll 缓存、上限清理、横切通知这些只剩一份实现。 +function bringFavoriteIntoHistory(row) { + const sourceId = row.source_clip_id; + if (sourceId != null) { + const existing = stmt.get.get(sourceId); + if (existing) { + const now = Date.now() / 1000; + stmt.moveToTop.run(now, now, sourceId); + notifyRefresh(); + return; + } + } + const now = Date.now() / 1000; + let info; + if (row.type === 'text') { + const text = row.text || ''; + const preview = row.preview || makePreview(text); + info = stmt.insertText.run(text, preview, null, now); + } else { + // 图片:复用 poll 的尺寸/预览判定,超大图压成 800px 宽保证 SOFT_CAP 不爆 + const buf = Buffer.isBuffer(row.image) ? row.image : Buffer.from(row.image || []); + const { buf: resized, preview: capPreview } = normalizeImageForStorage(buf); + const preview = capPreview || '[图片]'; + info = stmt.insertImage.run(resized, preview, null, now); + } + if (clipCount != null) clipCount++; + enforceLimit(); + notifyNew(rowToClip(stmt.getMeta.get(info.lastInsertRowid))); +} + +// 通知所有订阅者刷新(用于 clear-top 等场景) +function notifyRefresh() { + subscribers.notifyClip('clips:refresh'); +} + +// 通知所有订阅者收藏列表已变更 +function notifyFavoritesChanged() { + subscribers.notifyFav('favorites:changed'); +} + + +// ---------- IPC ---------- + +const subscribers = createSubscribers(); + +function notifyNew(clip) { + subscribers.notifyClip('clips:added', clip); +} +// 把渲染进程的 console 转发到主进程终端(方便调试) +function forwardRendererLog(webContents) { + if (process.env.CB_LOG !== '1') return; + webContents.on('console-message', (_e, level, message) => { + const tag = ['DBG', 'INF', 'WRN', 'ERR'][level] || 'LOG'; + devLog(`[renderer ${tag}] ${message}`); + }); +} + +function registerIpc() { + ipcMain.handle('clips:list', (_e, { query = '', limit = 0 } = {}) => { + // limit = 0 → 用配置的界面显示条数;设置改了之后渲染进程重新拉就生效 + const eff = limit > 0 ? limit : (appConfig.displayLimit || 300); + const plan = planSearch(query); + let rows; + if (plan.mode === 'fts' && stmt.searchFts) { + try { + rows = stmt.searchFts.all(plan.match, eff); + } catch (e) { + // FTS 出问题就退回 LIKE,不要把搜索整个打死 + devWarn('[clips:list] FTS search failed, falling back to LIKE:', e.message); + rows = searchLikeRows(likePatterns(query), eff); + } + } else if (plan.mode === 'fts') { + // 没有 FTS 索引(老库 / 这个 SQLite 不带 FTS5) + rows = searchLikeRows(likePatterns(query), eff); + } else if (plan.mode === 'like') { + rows = searchLikeRows(plan.patterns, eff); + } else { + rows = stmt.listAll.all(eff); + } + return listWithFavorited(rows.map(rowToClip)); + }); + + // 图片按需拉取:列表只带 hasImage 标记,渲染进程滚到哪张才取哪张。 + // 之前 clips:list 会把 200 条的 image BLOB 全部 base64 后过 IPC, + // 几十条图片就是几百 MB 的字符串。 + ipcMain.handle('clips:get-image', (_e, { id }) => { + const row = stmt.getImage.get(id); + if (!row || !row.image) return null; + // row.image 是 better-sqlite3 BLOB 列,原生就是 Buffer; + // 之前 Buffer.from(row.image) 是一次额外拷贝(2MB 图 → 2MB 浪费)。 + // Buffer.prototype.toString('base64') 不需要再 wrap一层。 + return row.image.toString('base64'); + }); + + ipcMain.handle('clips:count', () => getClipCount()); + + ipcMain.handle('clips:movetop', async (_e, { id }) => { + const now = Date.now() / 1000; + stmt.moveToTop.run(now, now, id); + // 把该内容写入系统剪贴板,用户切到目标窗口自己按 Ctrl+V 即可。 + const copied = copyClipToClipboard(id); + notifyRefresh(); + // 渲染到 IPC 之间可能已被自动清理(enforceLimit)—— row 可能不存在。 + // 但剪贴板已经写成功了,渲染端必须能正确报"已写入", + // 而不是把它当失败。让 row 缺失只影响回显字段,copied 不动。 + const row = stmt.getMeta.get(id); + if (!row) return { copied, pasted: false, clip: null }; + + // 不做自动粘贴、不隐藏/最小化窗口 —— 界面任何时候都保持可见。 + // (自动粘贴需要交出焦点必然要藏窗口,会产生"缩一下"的动画,用户明确不要。) + return { ...listWithFavorited([rowToClip(row)])[0], copied, pasted: false }; + }); + + ipcMain.handle('clips:delete', (_e, { id }) => { + const changes = stmt.delete.run(id).changes; + if (clipCount != null) clipCount -= changes; + if (changes) notifyRefresh(); + return true; + }); + + // clips:clear-natural 已彻底移除 —— 历史 Tab 从不调用它, + // 唯一在用的清空历史入口是 settings:clear-history(设置窗口)。 + // 两份路径完全是同实现(clearNonTop + clipCount = null + notifyRefresh), + // 留一条就够了 —— ipc-wiring 守的是双向一致,一删都删。 + + ipcMain.handle('clips:subscribe', (e) => { + subscribers.addClip(e.sender); + e.sender.on('destroyed', () => subscribers.drop(e.sender)); + return true; + }); + + ipcMain.handle('settings:open', () => { + openSettingsWindow(); + return true; + }); + + function favGuard() { + if (!favDb) throw new Error('favorites 不可用'); + } + + ipcMain.handle('favorites:add', (_e, { clipId }) => { + favGuard(); + const row = stmt.get.get(clipId); + if (!row) throw new Error('源记录不存在'); + // 幂等:重复收藏同一条(双击竞态 / 快速连点)不抛 UNIQUE 约束错, + // 直接返回已有收藏行。 + const existFavId = favorites.isFavorited(favDb, row.id); + if (existFavId) return favorites.get(favDb, existFavId); + const result = favorites.add(favDb, { + id: row.id, + type: row.type, + text: row.text, + image: row.image, + preview: row.preview, + createdAt: row.created_at, + }); + notifyFavoritesChanged(); + return result; + }); + + ipcMain.handle('favorites:remove', (_e, { favId }) => { + favGuard(); + const ok = favorites.remove(favDb, favId); + notifyFavoritesChanged(); + return ok; + }); + + ipcMain.handle('favorites:list', (_e, { query = '', limit = 0 } = {}) => { + favGuard(); + // 收藏列表也走 displayLimit —— 渲染层 limit=0 → 用配置档位 + const eff = limit > 0 ? limit : (appConfig.displayLimit || 300); + return favorites.list(favDb, query, eff); + }); + + // 只要一个数字。之前渲染进程为了显示收藏计数会拉全部 5000 条(含图片 base64), + // 收藏里有几百张图时就是几百 MB 的无谓开销。 + ipcMain.handle('favorites:count', () => (favDb ? favorites.count(favDb) : 0)); + + ipcMain.handle('favorites:get-image', (_e, { favId }) => { + if (!favDb) return null; + return favorites.getImage(favDb, favId); + }); + + // 收藏 Tab 的"复制":写收藏自己存的内容(源记录可能早已被清理)。 + // 返回形状与 clips:movetop 对齐:copied / pasted(不自动粘贴,恒 false)。 + ipcMain.handle('favorites:copy', (_e, { favId }) => { + favGuard(); + const copied = copyFavToClipboard(favId); + return copied ? { copied: true, pasted: false } : null; + }); + + ipcMain.handle('favorites:subscribe', (e) => { + subscribers.addFav(e.sender); + e.sender.on('destroyed', () => subscribers.drop(e.sender)); + return true; + }); + + ipcMain.handle('window:hide', () => { + if (mainWindow) mainWindow.hide(); + }); + + // on 省略(undefined)→ 翻转;传 boolean → 直接设置 + ipcMain.handle('window:toggle-always-on-top', (_e, { on } = {}) => + applyAlwaysOnTop(typeof on === 'boolean' ? on : !alwaysOnTop)); + + ipcMain.handle('window:get-always-on-top', () => alwaysOnTop); + + // 标题栏左上角小图标:返回 PNG data URL,渲染进程直接喂给 。 + // 命中缓存后基本零成本(一次 IPC 往返 + 一个字符串赋值)。 + ipcMain.handle('app:get-icon', () => getAppIconDataUrl()); + + ipcMain.handle('theme:get', () => { + // 跟 broadcastTheme / applyThemeChange 同形 —— 渲染端拿到时直接用 effectiveTheme + // 设 data-theme,不需要自己再判 followSystem(之前由渲染端各自判,反而因为 + // IPC 顺序竞争让设置窗口 / 主窗口在「跟随系统」开着的状态下显示 raw 主题)。 + const sysDark = nativeTheme.shouldUseDarkColors; + const effective = configMod.effectiveTheme(appConfig, sysDark); + return { + theme: effective, + effectiveTheme: effective, + rawTheme: appConfig.theme, + accent: appConfig.accent, + followSystem: appConfig.followSystem, + systemDark: sysDark, + }; + }); + + // 没有这个订阅,broadcastTheme() 就是在往一个永远空的集合里发 —— + // "跟随系统"改了主题后,已经打开的窗口收不到任何通知。 + ipcMain.handle('theme:subscribe', (e) => { + subscribers.addTheme(e.sender); + e.sender.on('destroyed', () => subscribers.drop(e.sender)); + return true; + }); + + ipcMain.handle('theme:set', (_e, { theme }) => { + if (!configMod.ALLOWED_THEMES.includes(theme)) return null; + return applyThemeChange({ theme, followSystem: false }); + }); + + ipcMain.handle('theme:set-accent', (_e, { accent }) => { + if (!configMod.ALLOWED_ACCENTS.includes(accent)) return null; + return applyThemeChange({ accent }); + }); + + ipcMain.handle('theme:set-follow-system', (_e, { followSystem }) => { + return applyThemeChange({ followSystem: followSystem === true }); + }); + + ipcMain.handle('settings:get', () => ({ ...appConfig })); + + ipcMain.handle('settings:set', (_e, { key, value }) => { + appConfig = applyPatch(appConfig, { key, value }); + let extra = {}; + if (key === 'autoLaunch') { + try { app.setLoginItemSettings({ openAtLogin: appConfig.autoLaunch }); } + catch (e) { console.error('[settings] setLoginItem failed:', e.message); } + } else if (key === 'maxItems') { + applyMaxItems(appConfig.maxItems); + } else if (key === 'pollMs') { + applyPollMs(appConfig.pollMs); + } else if (key === 'hotkey') { + // 立刻换键生效。注册失败(被占用)也照存配置 —— 占用是暂时的, + // 重启后可能就空出来了;同时把失败状态带给设置界面提示用户。 + extra = { hotkeyRegistered: applyHotkey() }; + } else if (key === 'displayLimit') { + // 推到所有 BrowserWindow,渲染层订阅后自己 reload()(用新 limit 重拉)。 + // 不直接 reload:跨窗口(设置窗口改、主窗口收)需要统一走 IPC 通知。 + for (const w of BrowserWindow.getAllWindows()) { + try { w.webContents.send('settings:display-limit-changed', { displayLimit: appConfig.displayLimit }); } + catch (_) {} + } + } + // 落盘失败不能只打日志:运行时状态(轮询间隔/快捷键/登录项)已经改了, + // 但重启会按磁盘旧值还原 —— 必须如实告诉渲染端,由 UI 提示"未保存"。 + try { configMod.saveConfig(appConfig); } + catch (e) { + console.error('[settings] saveConfig failed:', e.message); + extra = { ...extra, saved: false, saveError: e.message }; + } + return { ...appConfig, ...extra }; + }); + + ipcMain.handle('settings:clear-history', () => { + // 与 clips:clear-natural 同构:行数缓存必须作废,否则下次入库 + // enforceLimit 按过期行数算差值,会把刚插入的新记录(乃至所有 + // 非置顶项)误删;开着的主窗口也要推一条刷新,不然列表停在已删除的项上。 + try { + const changes = stmt.clearNonTop.run().changes; + clipCount = null; // 重新回源计数 + if (changes) notifyRefresh(); + return changes; + } catch (e) { + console.error('[settings] clear-history failed:', e.message); + return 0; + } + }); + + ipcMain.handle('settings:open-dir', () => { + // openPath 返回 Promise:失败原因是 resolve 出来的错误串(不是 reject), + // 同步 try/catch 接不到任何东西,失败时用户点了没反应、日志里也没痕迹。 + // 走 effectiveDataDir() —— 启动兜底后用户看的是真正在用的目录,而不是坏的盘符。 + shell.openPath(effectiveDataDir()).then( + (err) => { if (err) console.error('[settings] openPath failed:', err); }, + (e) => { console.error('[settings] openPath failed:', e.message); } + ); + }); + + // 外链一律交给系统浏览器:设置窗口直接导航出去会把整个页面拖走。 + // scheme 必须卡死在 http/https —— shell.openExternal 接受任意 scheme + // (file:、自定义协议),渲染进程递来的字符串不校验就是 RCE 面。 + // openExternal 返回 Promise:OS 拒绝时 reject,必须 await —— 不 await 的话 + // try/catch 接不住,Node 20 默认把 unhandled rejection 升级成主进程崩溃。 + ipcMain.handle('settings:open-external', async (_e, { url } = {}) => { + if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) return { ok: false }; + try { + await shell.openExternal(url); + return { ok: true }; + } catch (e) { + console.error('[settings] openExternal failed:', e.message); + return { ok: false }; + } + }); + + function copyIfExists(src, dst) { + if (fs.existsSync(src)) fs.copyFileSync(src, dst); + } + + function dbFileSet(dir) { + return [ + path.join(dir, 'history.db'), + path.join(dir, 'history.db-wal'), + path.join(dir, 'history.db-shm'), + path.join(dir, 'favorites.db'), + path.join(dir, 'favorites.db-wal'), + path.join(dir, 'favorites.db-shm'), + ]; + } + + // 迁移复制只搬 db + wal。-shm 是 WAL 的临时共享内存索引,SQLite 打开时 + // 自己重建,搬一个可能与 wal 不同步的快照过去反而更糟(与 config.js + // migrateLegacyData 的处理一致)。dbFileSet 保留 -shm 是给「删旧文件 / + // 清理失败目标」用的 —— 删的时候要删干净。 + const COPY_NAMES = ['history.db', 'history.db-wal', 'favorites.db', 'favorites.db-wal']; + + ipcMain.handle('settings:choose-dir', async () => { + // 1. 选目录 + const pick = await dialog.showOpenDialog({ + title: '选择数据保存位置', + properties: ['openDirectory', 'createDirectory'], + }); + if (pick.canceled || !pick.filePaths[0]) return { ok: false }; + const newDir = pick.filePaths[0]; + // Windows 大小写不敏感:D:\X 与 d:\x 是同一目录,必须按平台语义比较, + // 否则重选当前目录会绕过这个提示、走到下面"目标已有数据"自己迁自己。 + // 比较 effectiveDataDir()(含 session 降级)—— 用户在兜底状态下重选 + // 同一个 session 目录时不弹迁移对话框自己迁自己。 + if (isSameDir(newDir, effectiveDataDir())) { + return { ok: false, message: '已经是当前数据位置' }; + } + + // 2. 可写校验 + try { + fs.mkdirSync(newDir, { recursive: true }); + const probe = path.join(newDir, '.write-test'); + fs.writeFileSync(probe, 'ok'); fs.unlinkSync(probe); + } catch (e) { + return { ok: false, message: '目标目录不可写:' + e.message }; + } + + const targetDb = path.join(newDir, 'history.db'); + const targetExists = fs.existsSync(targetDb); + + // 3. 目标已有 db → 二选一 + if (targetExists) { + const r = dialog.showMessageBoxSync({ + type: 'question', + buttons: ['使用该目录的现有数据', '取消'], + defaultId: 1, + cancelId: 1, + title: '目标已有数据', + message: '该目录已存在剪贴板数据。是否直接使用它?', + detail: '选择「使用现有数据」将切换到该目录的数据库(不会合并当前数据)。', + }); + if (r !== 0) return { ok: false, message: '已取消' }; + const oldDir = effectiveDataDir(); + stopPolling(); // 马上退出;别让轮询在退出前的间隙再写旧库 + appConfig.dataDir = newDir; + sessionDataDir = null; // 用户已正式选定新目录,session 兜底退出 + try { configMod.saveConfig(appConfig); } + catch (e) { + // 失败后应用留在旧目录继续运行:回滚内存配置 + 恢复轮询, + // 否则剪贴板监听静默死掉,且后续任何 settings:set 会把错误的 + // newDir 落盘(内存/落盘分叉,同 choose-dir 迁移分支修过的坑)。 + appConfig.dataDir = oldDir; + startPolling(); + return { ok: false, message: '保存配置失败:' + e.message }; + } + app.relaunch(); + app.quit(); + return { ok: true }; + } + + // 4. 迁移:停轮询 → 关闭 db → 复制 → 校验 → 删旧 → 写 config → 重启 + // 轮询的 prepared statements 绑在旧连接上:必须先停轮询再关库,否则迁移 + // 期间(复制+校验有几十~几百 ms 窗口)tick 触发会抛 "database is closed", + // 这一两秒里的外部复制也会被静默丢掉。失败恢复路径里会重新 startPolling()。 + stopPolling(); + // 旧目录可能是 sessionDataDir 兜底出来的 —— 用 effectiveDataDir(), + // 不要再用裸 appConfig.dataDir,否则 session 状态下会从用户原本选的 + // (不可用的)目录复制,而当前在用的兜底目录里的数据被丢在原地。 + const oldDir = effectiveDataDir(); + try { + if (db) { const d = db; db = null; d.close(); } + if (favDb) { const f = favDb; favDb = null; favorites.close(f); } + } catch (e) { + // db.close 抛错时把句柄置空再恢复轮询:留着旧句柄会让下一个 tick + // 在已关闭的连接上 prepare,抛 "database is closed" 打到控制台。 + db = null; favDb = null; + startPolling(); // 关库失败也要恢复轮询,应用还留在旧目录运行 + return { ok: false, message: '关闭数据库失败:' + e.message }; + } + + try { + const oldFavDbExists = fs.existsSync(path.join(oldDir, 'favorites.db')); + for (const name of COPY_NAMES) { + copyIfExists(path.join(oldDir, name), path.join(newDir, name)); + } + + const testH = new Database(path.join(newDir, 'history.db')); + testH.prepare('SELECT COUNT(*) AS n FROM clips').get(); + testH.close(); + + // 旧目录没有 favorites.db(升级自旧版本)→ 用 favorites.open() 初始化空库 + if (!oldFavDbExists) { + const initFav = favorites.open(path.join(newDir, 'favorites.db')); + favorites.close(initFav); + } + + const testF = new Database(path.join(newDir, 'favorites.db')); + testF.prepare('SELECT COUNT(*) AS n FROM favorites').get(); + testF.close(); + } catch (e) { + try { for (const f of dbFileSet(newDir)) if (fs.existsSync(f)) fs.unlinkSync(f); } catch (_) {} + try { openDb(); } catch (_) {} + try { openFavDb(); } catch (_) {} + startPolling(); // 迁移失败、留在旧目录:必须恢复剪贴板监听 + return { ok: false, message: '迁移失败,已保留原数据:' + e.message }; + } + + // 先写 config + 重开新库(指向新位置),再删旧文件 —— 避免崩溃窗口遗留孤儿数据。 + // 失败分支语义:新文件已复制成功但配置未落盘 → 不删旧文件(两边都在,下次启动 + // 仍能从旧目录恢复),内存里切到新目录让会话内可用,重启后由 config 决定位置。 + appConfig.dataDir = newDir; + sessionDataDir = null; // 用户已正式选定新目录,session 兜底退出 + try { configMod.saveConfig(appConfig); } + catch (e) { + try { openDb(); } catch (_) {} + try { openFavDb(); } catch (_) {} + startPolling(); // 应用继续运行(已切到新库):恢复监听 + return { ok: false, message: '数据已迁移但配置未保存:' + e.message }; + } + try { for (const f of dbFileSet(oldDir)) if (fs.existsSync(f)) fs.unlinkSync(f); } catch (_) {} + + app.relaunch(); + app.quit(); + return { ok: true }; + }); +} + + +// ---------- 单实例 + 启动 ---------- + +const gotLock = app.requestSingleInstanceLock(); +if (!gotLock) { + app.quit(); +} else { + // 老版本把数据放在 %USERPROFILE%\.clipboard-app,现在默认固定在 + // D:\Clipboard Data。第一次跑到新目录时把老数据搬过来(复制,老文件保留)。 + // 放在单实例锁之后:否则被忽略的第二个实例会去复制正在被写的 WAL。 + try { + const mig = configMod.migrateLegacyData(); + if (mig.migrated) { + devLog(`[main] 数据已从 ${mig.from} 迁移到 ${mig.to}:${mig.files.join(', ') || '(仅配置)'}`); + appConfig = configMod.loadConfig(); + MAX_ITEMS = appConfig.maxItems; + POLL_MS = appConfig.pollMs; + } + } catch (e) { + console.error('[main] 老数据迁移失败(继续用新目录启动):', e.message); + } + + app.on('second-instance', () => toggleWindow()); + + app.whenReady().then(() => { + try { + devLog('[main] whenReady, opening db...'); + + // 用户在设置里改过 dataDir、之后那块盘符断电 / 被拔 / 改权限了, + // openDb 的 mkdirSync 会抛 ENOENT —— 之前会一路冒到外层 catch + // 弹「启动失败」对话框。这里主动兜一次:本会话切到 defaultDataDir() + // (它本身已经做了 D 盘 → home 的二级降级),配置页也能打开。 + // + // 注意:这里只动 sessionDataDir,不改 appConfig.dataDir,也不写盘 — + // 用户原本选的目录可能只是临时坏掉(外接硬盘拔了、临时网盘断连), + // 盘符恢复后下次启动会回到原配置;现在覆盖掉的话用户得手动重选。 + if (!configMod.isDirUsable(appConfig.dataDir)) { + const fallback = configMod.defaultDataDir(); + console.warn(`[main] dataDir ${appConfig.dataDir} 不可用,本会话切到 ${fallback}`); + sessionDataDir = fallback; + } + + openDb(); + try { + openFavDb(); + devLog('[main] favorites db opened'); + } catch (e) { + // favorites DB 损坏/不可用 —— 不让整个应用挂掉;收藏功能降级为不可用 + console.error('[main] openFavDb failed (favorites disabled):', e.message, e.stack); + favDb = null; + } + devLog('[main] db opened'); + + registerIpc(); + devLog('[main] ipc registered'); + + // 启动时把模块级 alwaysOnTop 同步到磁盘配置 —— 之前硬编码 true, + // 用户在主窗口点了图钉关闭置顶之后重启,配置里若有 ever-set 的 + // false 状态也会被无视。应用层只暴露主窗口图钉一个入口, + // 所以这个字段大多停留在默认值;只是把"读 config、推模块变量" + // 的链路补上,避免和磁盘分叉。 + // 必须在 createMainWindow 之前调用:模块级 alwaysOnTop 默认 true, + // 窗口构造时会读它,晚了就要先建后改(多一次 setAlwaysOnTop 调用)。 + applyAlwaysOnTop(appConfig.alwaysOnTop); + + createMainWindow(); // 内部已经挂好自适应轮询的 show/hide 监听 + devLog('[main] window created'); + + // 启动时窗口默认隐藏 —— 靠托盘图标唤出。createFramelessWindow 已经 + // 用 show:false 创建,这里不再触发 ready-to-show 弹窗,否则每次开机 + // 都会抢焦点,和"剪贴板工具"的预期行为不一致。 + // + // 不在此时广播主题:渲染窗口还没起来、没人订阅,setImmediate 这条只 + // 是对着空 Set 发了一次消息;初始状态由 theme:get 主动拉。 + + buildTray(); + devLog('[main] tray built'); + + // 全局快捷键必须在 whenReady 之后注册。被占用时不致命: + // 托盘唤出仍然可用,只是少了键盘入口(applyHotkey 内部有日志)。 + applyHotkey(); + + try { + app.setLoginItemSettings({ openAtLogin: appConfig.autoLaunch }); + } catch (e) { console.error('[main] setLoginItem failed:', e.message); } + + // 先采样当前剪贴板作为"上次状态",再开 poll。否则第一个 tick + // 就会把当前剪贴板里的内容当成"新内容"再入一次库。 + initializeClipboardState(); + startPolling(); + devLog(`[main] ready, polling every ${POLL_MS}ms; db = ${getDbPath()}`); + } catch (e) { + console.error('[main] startup FAILED:', e.message, e.stack); + // ENOENT/EACCES/EPERM:数据目录出问题最常见的三种 —— 引导用户去设置里改。 + const code = e && e.code; + const hint = (code === 'ENOENT' || code === 'EACCES' || code === 'EPERM') + ? '\n\n可能是数据目录不可用(盘符缺失、权限不足等)。\n请到「设置 → 通用 → 数据保存位置」换一个位置,或检查该盘符/文件夹是否可访问。' + : ''; + dialog.showErrorBox(APP_NAME, '启动失败:' + e.message + hint); + } + }); + + app.on('window-all-closed', () => { + // 不退出,靠托盘 + }); + + app.on('will-quit', () => { + stopPolling(); + // 退出时释放全局快捷键。进程正常退出时系统本来也会回收,但崩溃窗口 + // 里残留注册会影响下次启动抢占同名组合键 —— 显式清掉最干净。 + try { globalShortcut.unregisterAll(); } catch (_) {} + if (db) db.close(); + if (favDb) favorites.close(favDb); + }); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bc1c6bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4419 @@ +{ + "name": "clipboard-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clipboard-app", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "better-sqlite3": "^11.10.0" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^31.7.7", + "electron-builder": "^24.13.3" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "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/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "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": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "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.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "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.0.5", + "resolved": "https://registry.npmmirror.com/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "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.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "dev": true, + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "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" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "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/universal/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@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" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "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.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "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/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "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.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "peer": true, + "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/archiver-utils/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "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/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/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, + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmmirror.com/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmmirror.com/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "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.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "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.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "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.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "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.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "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.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "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.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmmirror.com/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "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": "24.13.3", + "resolved": "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmmirror.com/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "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.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "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-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "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.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "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.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "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.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "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.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmmirror.com/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "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.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "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.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "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.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/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, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "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/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmmirror.com/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "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.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "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.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "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.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "peer": true, + "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/zip-stream/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b1dd22 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "clipboard-app", + "version": "1.0.0", + "description": "剪贴板 — 简洁的剪贴板管理器(Electron 实现)", + "main": "main.js", + "scripts": { + "start": "set \"ELECTRON_RUN_AS_NODE=\" && electron .", + "test": "node test/run.js", + "test:ui": "set \"ELECTRON_RUN_AS_NODE=\" && electron test/settings-tabs.test.js && electron test/settings-render.test.js", + "test:all": "npm test && npm run test:ui", + "rebuild": "electron-rebuild -f -w better-sqlite3", + "postinstall": "electron-builder install-app-deps", + "build": "electron-builder", + "build:dir": "electron-builder --dir" + }, + "dependencies": { + "better-sqlite3": "^11.10.0" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^31.7.7", + "electron-builder": "^24.13.3" + }, + "build": { + "appId": "com.clipboard.app", + "productName": "剪贴板", + "files": [ + "main.js", + "config.js", + "tray-menu.js", + "preload.js", + "favorites.js", + "lib/**/*", + "renderer/**/*", + "icon.ico", + "package.json" + ], + "win": { + "target": "nsis", + "icon": "icon.ico" + } + } +} diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..4feb230 --- /dev/null +++ b/preload.js @@ -0,0 +1,87 @@ +/** + * preload.js — contextBridge 暴露给渲染进程的安全 IPC API + */ +'use strict'; + +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('api', { + // 数据查询(列表不含图片数据,图片走 getImage 按需拉)。 + // limit 留空 → 主进程用配置里的 displayLimit(默认 300),改设置后下次拉就生效。 + list: (query, limit) => ipcRenderer.invoke('clips:list', { query: query || '', limit: limit || 0 }), + getImage: (id) => ipcRenderer.invoke('clips:get-image', { id }), + count: () => ipcRenderer.invoke('clips:count'), + + // 操作 + movetop: (id) => ipcRenderer.invoke('clips:movetop', { id }), + remove: (id) => ipcRenderer.invoke('clips:delete', { id }), + hide: () => ipcRenderer.invoke('window:hide'), + openSettings: () => ipcRenderer.invoke('settings:open'), + + // 窗口置顶 + toggleAlwaysOnTop: (on) => ipcRenderer.invoke('window:toggle-always-on-top', { on }), + getAlwaysOnTop: () => ipcRenderer.invoke('window:get-always-on-top'), + + // 应用图标(标题栏左上角小图标):返回 PNG data URL,喂给 。 + // 主进程会缓存 .ico→PNG 的转换结果,每个窗口首次调用一次即可。 + getAppIcon: () => ipcRenderer.invoke('app:get-icon'), + + // 订阅 + subscribe: () => ipcRenderer.invoke('clips:subscribe'), + onAdded: (cb) => { + const handler = (_e, clip) => cb(clip); + ipcRenderer.on('clips:added', handler); + return () => ipcRenderer.removeListener('clips:added', handler); + }, + onRefresh: (cb) => { + const handler = () => cb(); + ipcRenderer.on('clips:refresh', handler); + return () => ipcRenderer.removeListener('clips:refresh', handler); + }, +}); + +contextBridge.exposeInMainWorld('favoritesApi', { + add: (clipId) => ipcRenderer.invoke('favorites:add', { clipId }), + remove: (favId) => ipcRenderer.invoke('favorites:remove', { favId }), + // 收藏 Tab 的复制:写收藏自身存的内容(favId 与历史 id 不同库,不能走 movetop) + copy: (favId) => ipcRenderer.invoke('favorites:copy', { favId }), + list: (query, limit) => ipcRenderer.invoke('favorites:list', { query: query || '', limit: limit || 0 }), + getImage: (favId) => ipcRenderer.invoke('favorites:get-image', { favId }), + count: () => ipcRenderer.invoke('favorites:count'), + subscribe: () => ipcRenderer.invoke('favorites:subscribe'), + onChanged: (cb) => { + const handler = () => cb(); + ipcRenderer.on('favorites:changed', handler); + return () => ipcRenderer.removeListener('favorites:changed', handler); + }, +}); + +contextBridge.exposeInMainWorld('settingsApi', { + get: () => ipcRenderer.invoke('settings:get'), + set: (key, value) => ipcRenderer.invoke('settings:set', { key, value }), + clearHistory: () => ipcRenderer.invoke('settings:clear-history'), + openDir: () => ipcRenderer.invoke('settings:open-dir'), + chooseDir: () => ipcRenderer.invoke('settings:choose-dir'), + // 外链交给系统浏览器(关于页)。scheme 白名单在主进程卡,渲染进程只管递 URL + openExternal: (url) => ipcRenderer.invoke('settings:open-external', { url }), + // 设置窗口订阅 displayLimit 变更,主进程设置被改后给所有窗口推一条刷新通知 + onDisplayLimitChanged: (cb) => { + const handler = (_e, payload) => cb(payload); + ipcRenderer.on('settings:display-limit-changed', handler); + return () => ipcRenderer.removeListener('settings:display-limit-changed', handler); + }, +}); + +contextBridge.exposeInMainWorld('themeApi', { + get: () => ipcRenderer.invoke('theme:get'), + set: (theme) => ipcRenderer.invoke('theme:set', { theme }), + setAccent: (accent) => ipcRenderer.invoke('theme:set-accent', { accent }), + setFollowSystem: (followSystem)=> ipcRenderer.invoke('theme:set-follow-system', { followSystem }), + // 必须先订阅,主进程才会把 theme:system-changed 发过来 + subscribe: () => ipcRenderer.invoke('theme:subscribe'), + onSystemChanged: (cb) => { + const handler = (_e, payload) => cb(payload); + ipcRenderer.on('theme:system-changed', handler); + return () => ipcRenderer.removeListener('theme:system-changed', handler); + }, +}); \ No newline at end of file diff --git a/renderer/index.html b/renderer/index.html new file mode 100644 index 0000000..a674029 --- /dev/null +++ b/renderer/index.html @@ -0,0 +1,134 @@ + + + + + + 剪贴板历史 + + + + + + + + +
+ +
+ +

剪贴板历史

+ + + + + + +
+ + + + + +
+
+ + + +
+
+ + +
+
    + +
    + + +
    + 就绪 + + 选择 + · + 19 / Enter 复制 + · + Esc 关闭 + +
    +
    + + + + + + + \ No newline at end of file diff --git a/renderer/renderer.js b/renderer/renderer.js new file mode 100644 index 0000000..90ac079 --- /dev/null +++ b/renderer/renderer.js @@ -0,0 +1,713 @@ +/** + * renderer.js — 历史窗口 UI 逻辑 + */ + +'use strict'; + +// 直接从 window.api 解构,避免 'api' 这个名字与 contextBridge 暴露的属性冲突 +const { + list, count, movetop, remove, hide, + subscribe, onAdded, onRefresh, openSettings, + toggleAlwaysOnTop, getAlwaysOnTop, getImage, +} = window.api; + +const { + add: favAdd, + remove: favRemove, + copy: favCopy, + list: favList, + count: favCountOf, + getImage: favGetImage, + subscribe: favSubscribe, + onChanged: onFavoritesChanged, +} = window.favoritesApi; + +const { onDisplayLimitChanged, get: settingsGet } = window.settingsApi; + +// ---------- DOM ---------- + +const $list = document.getElementById('list'); +const $listWrap = document.getElementById('list-wrap'); +const $empty = document.getElementById('empty'); +const $statusL = document.getElementById('status-left'); +const $search = document.getElementById('search'); +const $searchClear = document.getElementById('search-clear'); +const $tpl = document.getElementById('card-tpl'); +const $closeBtn = document.getElementById('close-btn'); +const $refreshBtn = document.getElementById('refresh-btn'); +const $settingsBtn = document.getElementById('settings-btn'); +const $pinBtn = document.getElementById('pin-btn'); +const $ipcDot = document.getElementById('ipc-status'); +const $tabs = document.getElementById('tabs'); +const $historyCount = document.getElementById('history-count'); +const $favCount = document.getElementById('fav-count'); + +// ---------- 状态 ---------- + +let clips = []; +let selectedIdx = -1; +let currentTab = 'history'; // 'history' | 'favorites' +let displayLimit = 300; // 来自 config.displayLimit,预加载时拉一次,IPC 变更时刷新 +let $cards = []; + +// ---------- 时间格式化 ---------- +// todayKey 由 render() / prependClip() 入口处刷新一次,避免每张卡片都 +// new Date() 一次(displayLimit=1000 时就是 1000 次 Date 构造 + toDateString)。 +let _todayKey = ''; +function refreshTodayKey() { + _todayKey = new Date().toDateString(); +} + +function fmtTime(ts) { + if (!ts) return ''; + const d = new Date(ts * 1000); + const sameDay = d.toDateString() === _todayKey; + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + if (sameDay) return `今天 ${hh}:${mm}`; + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day} ${hh}:${mm}`; +} + +function setTab(name) { + if (name !== 'history' && name !== 'favorites') return; + currentTab = name; + for (const t of $tabs.querySelectorAll('.tab')) { + t.classList.toggle('active', t.dataset.tab === name); + } + selectedIdx = -1; + // 切到 favorites 时先取真实总数(favorites:count IPC),列表本身只取 200 条, + // 徽标反映真实值而不被截断。 + if (name === 'favorites') updateFavCount(); + reload(); +} + +$tabs.addEventListener('click', (e) => { + const btn = e.target.closest('.tab'); + if (btn && btn.dataset.tab) setTab(btn.dataset.tab); +}); + +// ---------- 图片懒加载 ---------- +// 列表只带 hasImage 标记,真正的图片数据滚到视口里才拉。 +// 这样打开窗口不再需要把几百 MB 的 base64 从主进程搬过来。 + +// FIFO 缓存 —— 满 60 条时删最早插入的那条。Map.keys() 按插入顺序排列, +// 所以 delete(keys().next().value) 就够了;不要命名成「LRU」,那是按访问 +// 顺序淘汰,不是这里的行为。 +const fifoCache = new Map(); // id -> base64(上限 60 条,避免无限增长) + +function cacheImage(key, data) { + if (fifoCache.size >= 60) fifoCache.delete(fifoCache.keys().next().value); + fifoCache.set(key, data); +} + +const imgObserver = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + const el = entry.target; + imgObserver.unobserve(el); + loadImageInto(el); + } +}, { root: document.getElementById('list-wrap'), rootMargin: '200px' }); + +async function loadImageInto(el) { + const id = Number(el.dataset.imgId); + const fromFav = el.dataset.imgFav === '1'; + const key = (fromFav ? 'f' : 'c') + id; + if (fifoCache.has(key)) { + const cached = fifoCache.get(key); + if (cached) el.src = `data:image/png;base64,${cached}`; + return; + } + try { + const b64 = await (fromFav ? favGetImage(id) : getImage(id)); + if (!b64) return; // 没有数据就保持占位,不要设一个坏 src + cacheImage(key, b64); + el.src = `data:image/png;base64,${b64}`; + } catch (e) { + console.error('[renderer] getImage failed:', e); + } +} + +// ---------- 渲染 ---------- + +// 单张卡片:纯 DOM 构造,任何状态都不在这里改。 +function buildCard(c, i) { + const node = $tpl.content.firstElementChild.cloneNode(true); + node.dataset.id = String(c.id); + + node.querySelector('.time').textContent = fmtTime(c.createdAt); + const typeBadge = node.querySelector('.badge.type'); + if (c.type === 'text') { + typeBadge.innerHTML = '文本'; + typeBadge.classList.add('badge-text'); + } else { + typeBadge.innerHTML = '图片'; + typeBadge.classList.add('badge-image'); + } + + const slotEl = node.querySelector('.slot'); + if (i < 9) { + slotEl.textContent = String(i + 1); + slotEl.classList.remove('hidden'); + } else { + slotEl.classList.add('hidden'); + } + + const favBtn = node.querySelector('.action.fav'); + if (currentTab === 'favorites' || c.favorited) { + favBtn.classList.add('favorited'); + favBtn.title = currentTab === 'favorites' ? '取消收藏' : '已收藏(点击取消)'; + } else { + favBtn.classList.remove('favorited'); + favBtn.title = '收藏'; + } + favBtn.onclick = (e) => { + e.stopPropagation(); + // 捕获点击时刻的 Tab:回调进串行队列延迟执行,期间用户切了 Tab, + // 若在执行时读全局 currentTab,历史 id 会被当 favId 发往 + // favorites:remove,静默删掉一条同号的无关收藏(两库自增撞号几乎必然)。 + toggleFavorite(c, currentTab); + }; + + const pinBtn = node.querySelector('.action.copy'); + const delBtn = node.querySelector('.action.delete'); + // 「复制」按钮在两个 Tab 都用:历史 Tab 走 movetop(写剪贴板 + 置顶), + // 收藏 Tab 走 favorites:copy(写收藏自己存的内容,favId 与历史 id 不同库)。 + pinBtn.classList.remove('hidden'); + if (currentTab === 'favorites') { + // 收藏 Tab 没有"置顶"语义,也没"删除"按钮(收藏 Tab 用单独的取消收藏) + delBtn.classList.add('hidden'); + pinBtn.title = '复制到剪贴板'; + pinBtn.onclick = (e) => { e.stopPropagation(); copyEntry(c); }; + } else { + delBtn.classList.remove('hidden'); + pinBtn.title = '复制(按 Enter)'; + pinBtn.onclick = (e) => { e.stopPropagation(); moveToTop(c.id); }; + delBtn.onclick = (e) => { e.stopPropagation(); removeClip(c.id); }; + } + + const textEl = node.querySelector('.text-preview'); + const imgEl = node.querySelector('.image-preview'); + + if (c.type === 'text') { + textEl.textContent = c.text || c.preview || ''; + imgEl.classList.add('hidden'); + } else { + textEl.classList.add('hidden'); + imgEl.classList.remove('hidden'); + imgEl.removeAttribute('src'); + if (c.hasImage) { + imgEl.dataset.imgId = String(c.id); + imgEl.dataset.imgFav = currentTab === 'favorites' ? '1' : '0'; + imgObserver.observe(imgEl); + } + } + + // 点击选中按下标 —— 但必须点击时再从 DOM 现取:prependClip 增量插入 + // 会把已有卡片整体后移,构建时捕获的 i 会过期,选中错误的项。 + node.onclick = () => select($cards.indexOf(node)); + node.ondblclick = () => copyEntry(c); + + return node; +} + +function render() { + refreshTodayKey(); + imgObserver.disconnect(); + $list.innerHTML = ''; + if (clips.length === 0) { + $empty.classList.remove('hidden'); + $statusL.textContent = '无内容'; + selectedIdx = -1; + $cards = []; + return; + } + $empty.classList.add('hidden'); + + const frag = document.createDocumentFragment(); + for (let i = 0; i < clips.length; i++) { + frag.appendChild(buildCard(clips[i], i)); + } + $list.appendChild(frag); + $cards = Array.from($list.querySelectorAll('.card')); + + // 选中态:reload() 现在统一走 select(0),所以这里 selectedIdx = -1,条件不成立。 + // 留下这段是为了万一将来 render() 被别处复用时还能保持选中态可视化。 + if (selectedIdx >= 0 && selectedIdx < $cards.length) { + $cards[selectedIdx].classList.add('selected'); + } + + $statusL.textContent = `显示 ${clips.length} 条`; +} + +// 滚到列表顶部($listWrap.scrollTop = 0)。两处调用: +// - reload() 末尾:覆盖「窗口被隐藏又重新打开」路径 —— 此时 list-wrap 的 +// scrollTop 是上次关闭前的位置,innerHTML='' 重建 DOM 不会自动归零, +// select(0) 配套的 scrollIntoView({block:'nearest'}) 又依赖可见性判定, +// 不写死 0 可能在边界情况下漏滚。 +// - prependClip() 末尾:新复制的卡片插到 DOM 顶端,但 list-wrap.scrollTop +// 还停在用户之前滚到的位置,新条目不可见。 +// 直接写 scrollTop 比依赖 scrollIntoView 更可控(后者行为依赖浏览器对 +// 可见性的判定,且会同时滚所有祖先滚动容器)。 +function scrollListToTop() { + if ($listWrap) $listWrap.scrollTop = 0; +} + +// 把新记录增量插入列表。失败时由调用方 fallback 到 reload()。 +// 插入位 = 0:主进程顺序就是 created_at DESC(最新活跃在最顶),新插入的 +// 这条记录入库时 created_at = now,天然就是最顶上 —— 直接 unshift 即可。 +// 选中态:不再保留旧选中,统一在末尾用 select(0) 把新条目点亮并滚到顶部。 +function prependClip(clip) { + refreshTodayKey(); + // 空列表时 render() 亮起了空状态;第一条增量进来必须熄掉, + // 否则卡片和"暂无内容"会同时显示。 + $empty.classList.add('hidden'); + clips.unshift(clip); + // 渲染端的二次裁剪 —— 主进程 SQL LIMIT 是配置里的 displayLimit,这里再守一道 + // 防止 IPC 之间收到 N 条新增时把数组无限撑大;超过上限的尾部从数组+DOM 一起裁掉。 + const cap = displayLimit; + if (clips.length > cap) { + clips.pop(); // 刚 unshift 的在最顶,被裁的永远是尾部的旧项 + const trimmedEl = $cards[$cards.length - 1]; + $cards.pop(); + // IntersectionObserver 持有已观察元素的强引用:不 unobserve, + // 被裁的 和 buildCard 闭包会滞留到下一次整表 render 才能 GC。 + const img = trimmedEl.querySelector('.image-preview'); + if (img) imgObserver.unobserve(img); + // FIFO 缓存里若有这个 id 的 base64,主动清掉;不依赖自然淘汰, + // 否则会占着一个 slot 直到被新插入挤掉。 + if (img && img.dataset.imgId) { + const fromFav = img.dataset.imgFav === '1'; + fifoCache.delete((fromFav ? 'f' : 'c') + img.dataset.imgId); + } + trimmedEl.remove(); + } + // 选中态:所有已有卡片整体后移一位(之前的 +1 / clamp 是为了"保留旧选中"), + // 现在策略改为"新复制 = 跳到第一条",旧选中下标没有意义了,直接交给下面的 select(0) 接管。 + const newCard = buildCard(clip, 0); + $list.insertBefore(newCard, $cards[0] || null); // null = 追加到末尾 + $cards.unshift(newCard); + // Slot badge 重排:每次 prepend 之后,只有原来第 8 位(现在第 9 位)的卡需要 + // 从「显示 9」变成「隐藏」,其它卡(0-8 已可见、9+ 已隐藏)都不动。 + // 全表 1000 张卡都跑一遍是浪费。 + const boundary = $cards[9]; // 第 10 位(下标 9):需要被隐藏 + if (boundary) { + const slotEl = boundary.querySelector('.slot'); + slotEl.classList.add('hidden'); + } + // 选中态:强制选中第一条(最新复制的条目),并滚到顶部让 Enter / 数字键 1 复制它。 + // select() 内部会遍历 $cards 把旧 'selected' 清掉、把 $cards[0] 点亮, + // 然后 scrollIntoView({block:'nearest'}) —— 这里再调 scrollListToTop() 兜底, + // 避免 nearest 把"已可见"误判导致不滚(参见 scrollListToTop 注释)。 + if (clips.length > 0) select(0); + scrollListToTop(); + $statusL.textContent = `显示 ${clips.length} 条`; +} + +// ---------- 选择 ---------- + +function select(idx) { + if (idx < 0 || idx >= clips.length) return; + selectedIdx = idx; + if (!$cards.length) return; + $cards.forEach((el, i) => el.classList.toggle('selected', i === idx)); + const sel = $cards[idx]; + if (sel) sel.scrollIntoView({ block: 'nearest' }); +} + +function moveSelection(delta) { + if (clips.length === 0) return; + if (selectedIdx < 0) { select(0); return; } + select(Math.max(0, Math.min(clips.length - 1, selectedIdx + delta))); +} + +// ---------- 操作 ---------- + +// 收藏 Tab 的复制:c.id 是 favId —— 与历史 id 分属两个库、自增数会撞号, +// 绝不能走 moveToTop(clips:movetop 会按同号查到一条无关的历史记录)。 +// 走 favorites:copy,写收藏行自身存下的内容(源记录被清了也照样能复制)。 +async function copyFavorite(favId) { + try { + const result = await favCopy(favId); + $statusL.textContent = result && result.copied + ? '已写入剪贴板(按 Ctrl+V 粘贴)' + : '复制失败'; + } catch (e) { + console.error('[renderer] favCopy failed:', e); + $statusL.textContent = '复制失败:' + (e.message || e); + } +} + +// 「复制一条」的统一入口:历史 Tab 走 movetop(写剪贴板 + 置顶), +// 收藏 Tab 走 favorites:copy。双击 / Enter / 数字键都收口到这里。 +function copyEntry(c) { + if (currentTab === 'favorites') return copyFavorite(c.id); + return moveToTop(c.id); +} + +async function moveToTop(id) { + try { + const result = await movetop(id); + await reload(); + // 点选只写系统剪贴板(不自动粘贴):成功 → 提示按 Ctrl+V;失败 → 复制失败 + $statusL.textContent = result && result.pasted + ? '已粘贴' + : (result && result.copied ? '已写入剪贴板(按 Ctrl+V 粘贴)' : '复制失败'); + } catch (e) { + console.error('[renderer] movetop failed:', e); + $statusL.textContent = '复制失败:' + (e.message || e); + } +} + +async function removeClip(id) { + if (!confirm('确定删除此条?此操作不可撤销。')) return; + await remove(id); + await reload(); + updateHistoryCount(); +} + +// 并发守卫:reload 的触发方很多(搜索防抖 / IPC 推送 / 切 Tab / 复制回显), +// 两次重叠时渲染结果必须由"最后发起的"决定,而不是"最后返回的" —— +// 否则慢查询的旧结果会盖掉新查询(搜索框显示 ab、列表停在 a 的结果上)。 +let reloadSeq = 0; +async function reload() { + const seq = ++reloadSeq; + const query = $search.value.trim(); + $refreshBtn.classList.add('spinning'); + // 先落局部变量:过期响应(seq 失配)在下面被丢弃时碰不到全局 clips, + // 否则慢响应乱序到达会把 clips 数组污染成旧查询的结果(DOM 新、数组旧 → 分叉)。 + let rows; + try { + if (currentTab === 'history') { + rows = await list(query); + } else { + // 收藏列表走配置里的 displayLimit(默认 300),避免一次建几千个 DOM 节点。 + // 徽标计数不能用 clips.length(被 displayLimit 截断会失真)——交给 + // updateFavCount()(favorites:count IPC)在每条 reload 路径上保证真实值。 + rows = await favList(query); + updateFavCount(); + } + } catch (e) { + if (seq !== reloadSeq) return; // 已有更新的请求:错误状态交给它处理 + console.error('[renderer] reload failed:', e); + $statusL.textContent = '加载失败:' + (e.message || e); + $refreshBtn.classList.remove('spinning'); + return; + } + if (seq !== reloadSeq) return; // 过期响应:丢弃,别拿旧查询的结果渲染 + clips = rows; + selectedIdx = -1; + render(); + // 窗口刚被唤起 / 切 Tab / 刷新列表 / 搜索结果更新:都强制选中第一条 + // 并滚到顶部,让最新内容成为焦点(按 1 / Enter 复制即拷贝最新条目)。 + // 之前用 prevId → restored 保留旧选中,但用户的旧选中此时通常已不在 + // 视口(scrollTop 仍是关闭前的位置),体验不连贯。 + if (clips.length > 0) select(0); + scrollListToTop(); + $refreshBtn.classList.remove('spinning'); +} + +let favPromise = Promise.resolve(); +async function toggleFavorite(c, tab) { + favPromise = favPromise.then(async () => { + try { + // 全程用点击时捕获的 tab 分发 —— 不在执行时读全局 currentTab(见调用处注释) + let willFavorite; + if (tab === 'favorites') { + // 收藏 Tab 的卡片 —— c.id 是 favId + await favRemove(c.id); + willFavorite = false; + } else if (c.favorited) { + await favRemove(c.favId); + willFavorite = false; + } else { + // favAdd 通过 IPC 返回新 fav row,row.id 是 favId —— 必须存到 c 上,否则下次 ★ 无法 remove + const row = await favAdd(c.id); + if (row && row.id) c.favId = row.id; + willFavorite = true; + } + if (tab === 'favorites') { + // 收藏 Tab 取消后,重新加载列表(被取消的卡片需从列表消失) + await reload(); + } else { + // 历史 Tab:只更新当前卡片 DOM + const node = $list.querySelector(`.card[data-id="${c.id}"]`); + if (node) { + const btn = node.querySelector('.action.fav'); + btn.classList.toggle('favorited', willFavorite); + btn.title = willFavorite ? '已收藏(点击取消)' : '收藏'; + } + // 同步状态对象 + c.favorited = willFavorite; + if (!willFavorite) delete c.favId; + } + // 触发收藏徽标更新 + updateFavCount(); + } catch (e) { + $statusL.textContent = '收藏操作失败:' + (e.message || e); + } + }); + return favPromise; +} + +// ---------- 键盘 ---------- + +document.addEventListener('keydown', (e) => { + // 全局快捷键:不看焦点(在搜索框里也能 F5 / Ctrl+L)。 + if (e.key === 'F5') { + e.preventDefault(); + reload(); + return; + } + if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'l') { + // Ctrl+L 聚焦搜索框。组合键完整匹配避免吃掉 Ctrl+Shift+L 之类。 + e.preventDefault(); + $search.focus(); + $search.select(); + return; + } + + // 搜索框聚焦时不抢键盘 + const inSearch = document.activeElement === $search; + + if (e.key === 'Escape') { + // 搜索框里按 Esc 先退出搜索,不要直接把窗口关掉。 + // (这个分支必须在无条件关闭之前,否则永远走不到。) + if (inSearch) { + clearSearch(); + $search.blur(); + return; + } + // 用户主动关闭:隐藏窗口而不是销毁它(应用留在托盘)。 + // 销毁后再唤出要走 BrowserWindow 冷路径,100~300ms 不可感延迟。 + hide(); + return; + } + + if (!inSearch) { + if (e.key === 'ArrowDown') { e.preventDefault(); moveSelection(1); return; } + if (e.key === 'ArrowUp') { e.preventDefault(); moveSelection(-1); return; } + if (e.key === 'PageDown') { e.preventDefault(); moveSelection(5); return; } + if (e.key === 'PageUp') { e.preventDefault(); moveSelection(-5); return; } + if (e.key === 'Home') { e.preventDefault(); select(0); return; } + if (e.key === 'End') { e.preventDefault(); select(clips.length - 1); return; } + // 数字键 1..9 按当前位置复制(最顶上 = 1);历史 Tab 同时置顶 + if (/^[1-9]$/.test(e.key)) { + e.preventDefault(); + const slot = parseInt(e.key, 10) - 1; + if (slot < clips.length) copyEntry(clips[slot]); + return; + } + // Enter:与"复制"按钮功能一致 —— 复制选中卡片(历史 Tab 同时置顶) + // 当焦点在卡片上的某个按钮时,由按钮自身默认行为处理,避免重复触发 + if (e.key === 'Enter') { + const active = document.activeElement; + if (active && active.tagName === 'BUTTON') return; + e.preventDefault(); + if (selectedIdx >= 0 && selectedIdx < clips.length) { + copyEntry(clips[selectedIdx]); + } + return; + } + } +}); + +// ---------- 搜索 ---------- + +let searchTimer = null; + +function syncSearchClear() { + // 输入框有任何字符就显示清空按钮;空了就藏起来。trim 前判断, + // 让用户敲一两个空格也能用按钮清掉(reload 走 trim,不会真的去搜)。 + $searchClear.classList.toggle('visible', $search.value.length > 0); +} + +function clearSearch() { + if (!$search.value) return; + $search.value = ''; + syncSearchClear(); + reload(); +} + +$search.addEventListener('input', () => { + syncSearchClear(); + clearTimeout(searchTimer); + searchTimer = setTimeout(reload, 80); +}); +$search.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + // Just select the first result; don't paste (Enter semantic conflict). + if (clips.length > 0) select(0); + e.preventDefault(); + } +}); +$searchClear.addEventListener('click', () => { + clearSearch(); + // 点完按钮焦点会落在按钮上 —— 拉回输入框,让用户能继续敲而不是按 Tab + $search.focus(); +}); + +// 启动时同步一次(兜底:将来若有"恢复上次搜索"逻辑也能直接复用) +syncSearchClear(); + +// ---------- 实时更新 ---------- + +subscribe() + .then(() => { + $ipcDot.classList.add('connected'); + $ipcDot.title = 'IPC 已连接'; + }) + .catch((e) => { + console.error('[renderer] subscribe failed:', e); + $ipcDot.classList.add('error'); + $ipcDot.title = 'IPC 失败: ' + (e.message || e); + }); +onAdded((clip) => { + // 收藏 Tab 走自己的 refresh;搜索时新项可能不匹配结果集,也走 reload。 + // 只在 history Tab + 搜索框为空时走增量插入,避免全表 innerHTML 重建。 + if (currentTab !== 'history' || $search.value.trim() !== '') { + reload(); + updateHistoryCount(); + return; + } + try { + prependClip(clip); + updateHistoryCount(); + } catch (e) { + console.error('[renderer] prepend failed, falling back to reload:', e); + reload(); + updateHistoryCount(); + } +}); +onRefresh(() => { reload(); updateHistoryCount(); }); + +favSubscribe().catch((e) => console.error('[renderer] favSubscribe failed:', e)); +onFavoritesChanged(() => { + if (currentTab === 'favorites') reload(); + else updateFavCount(); +}); + +async function updateFavCount() { + try { + // 只要一个数字:以前这里拉的是全部收藏(含图片 base64),只为读 .length + $favCount.textContent = String(await favCountOf()); + } catch (e) {} +} + +async function updateHistoryCount() { + try { + // clips:count 走主进程内存里的行数缓存(stmt.count 命中时不再 COUNT(*) 全表), + // 每条新增 / 删除 / 清空历史都会实时同步,徽标总能反映真实总数。 + $historyCount.textContent = String(await count()); + } catch (e) {} +} + +// ---------- 主题 ---------- + +const { get: themeGet, subscribe: themeSubscribe, onSystemChanged } = window.themeApi; + +async function applyTheme() { + try { + const cfg = await themeGet(); + // 优先用 effectiveTheme:「跟随系统」开时 cfg.theme 已经是 effective, + // 但要兼容旧主进程(旧版只发 raw theme),用 || 兜底。 + const theme = cfg.effectiveTheme || cfg.theme; + document.documentElement.setAttribute('data-theme', theme); + document.documentElement.setAttribute('data-accent', cfg.accent); + try { + // 同时缓存 raw(用于下次启动 theme-boot 兜底)和 effective(用于 + // 「跟随系统」开时直接跳过 flash)。如果主进程旧版没给 rawTheme, + // 就退回到 cfg.theme 字段本身。 + localStorage.setItem('clipboard-theme', JSON.stringify({ + theme: cfg.rawTheme || cfg.theme, + effectiveTheme: cfg.effectiveTheme || cfg.theme, + accent: cfg.accent, + followSystem: cfg.followSystem, + })); + } catch (_) {} + } catch (e) { + console.error('[theme] applyTheme failed:', e.message); + } +} + +onSystemChanged((payload) => { + // 系统主题切换只更新 DOM,不写 localStorage:payload 里没有 rawTheme, + // 写下去会让后续启动在「跟随系统」关掉时拿不到用户真正选的主题。 + // localStorage 由 applyTheme()(持有完整 rawTheme)统一维护。 + const theme = payload.effectiveTheme || payload.theme; + document.documentElement.setAttribute('data-theme', theme); + document.documentElement.setAttribute('data-accent', payload.accent); +}); + +// 不订阅的话,主进程的 theme:system-changed 根本不会发到这个窗口 +themeSubscribe().catch((e) => console.error('[renderer] themeSubscribe failed:', e)); + +applyTheme(); + +// ---------- 启动 ---------- + +// 关闭按钮:用户主动关闭走 hide(),不销毁窗口(理由见 Esc 分支)。 +$closeBtn.addEventListener('click', () => hide()); + +// 刷新按钮 +$refreshBtn.addEventListener('click', () => reload()); + +// 设置按钮 +$settingsBtn.addEventListener('click', () => openSettings()); + +// 置顶按钮:图标状态由 aria-pressed 表达(开启=true) +function syncPinBtn(on) { + $pinBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + $pinBtn.title = on ? '取消窗口置顶' : '窗口置顶'; + // 图标走 SVG,开/关靠 CSS 上色区分(accent vs muted) +} + +$pinBtn.addEventListener('click', async () => { + try { + const next = await toggleAlwaysOnTop(); // 不传 on → 翻转 + syncPinBtn(next === true); + $statusL.textContent = next ? '已开启窗口置顶' : '已关闭窗口置顶'; + } catch (e) { + console.error('[renderer] toggleAlwaysOnTop failed:', e); + $statusL.textContent = '切换置顶失败:' + (e.message || e); + } +}); + +// 启动时同步主进程的初始状态(默认开启) +getAlwaysOnTop() + .then((on) => syncPinBtn(on === true)) + .catch((e) => console.error('[renderer] getAlwaysOnTop failed:', e)); + +// 启动时直接刷新两个 tab 的计数——不依赖当前 Tab +updateHistoryCount(); +updateFavCount(); +reload(); + +// ---------- 界面显示条数(displayLimit) ---------- +// 启动时拉一次配置,让前端 displayLimit 跟主进程保持一致; +// 设置窗口改了之后主进程会推 settings:display-limit-changed,所有窗口重拉。 +async function syncDisplayLimit() { + try { + const c = await settingsGet(); + if (c && c.displayLimit) displayLimit = c.displayLimit; + } catch (_) {} +} +syncDisplayLimit(); +onDisplayLimitChanged(({ displayLimit: next }) => { + if (typeof next === 'number' && next > 0) { + displayLimit = next; + reload(); + } +}); + +// ---------- 应用图标(标题栏左上角) ---------- +// 一次性从主进程拿 PNG data URL 喂给 。主进程已缓存 .ico→PNG 的转换, +// IPC 命中后基本零成本;图标缺失静默失败,不影响功能。 +window.api.getAppIcon().then((dataUrl) => { + if (!dataUrl) return; + const el = document.getElementById('app-icon'); + if (el) el.src = dataUrl; +}).catch(() => { /* 图标缺失不影响功能 */ }); \ No newline at end of file diff --git a/renderer/settings.html b/renderer/settings.html new file mode 100644 index 0000000..29c2626 --- /dev/null +++ b/renderer/settings.html @@ -0,0 +1,80 @@ + + + + + + 设置 + + + + + + + + +
    +

    设置

    + +
    +
    +
    +
    主题
    +
    +
    +
    + + +
    +
    +
    强调色
    +
    +
    +
    + + + + +
    +
    就绪
    +
    + + + diff --git a/renderer/settings.js b/renderer/settings.js new file mode 100644 index 0000000..2f1880a --- /dev/null +++ b/renderer/settings.js @@ -0,0 +1,227 @@ +'use strict'; + +// ---------- 标签页 ---------- +// 放在 IPC 解构之前:即使 preload 没注入,标签页也要能切换。 + +(() => { + const bar = document.getElementById('tab-bar'); + if (!bar) return; + const tabs = Array.from(bar.querySelectorAll('[role="tab"]')); + if (!tabs.length) return; + + function select(tab) { + for (const t of tabs) { + const on = t === tab; + t.setAttribute('aria-selected', on ? 'true' : 'false'); + t.tabIndex = on ? 0 : -1; + const panel = document.getElementById(t.getAttribute('aria-controls')); + if (panel) panel.hidden = !on; + } + } + + for (const t of tabs) { + t.addEventListener('click', () => { select(t); t.focus(); }); + } + + bar.addEventListener('keydown', (e) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + e.preventDefault(); + // 按「当前选中」而不是 document.activeElement 找位置: + // 窗口未获焦点时 activeElement 不可靠,测试里也无法聚焦隐藏窗口。 + const i = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true'); + const step = e.key === 'ArrowRight' ? 1 : tabs.length - 1; + const next = tabs[((i < 0 ? 0 : i) + step) % tabs.length]; + select(next); + next.focus(); + }); + + select(tabs[0]); +})(); + +const { get, set, clearHistory, openDir, chooseDir, openExternal } = window.settingsApi; +const $auto=document.getElementById('opt-autolaunch'),$hotkey=document.getElementById('opt-hotkey'),$max=document.getElementById('opt-maxitems'),$display=document.getElementById('opt-displaylimit'),$poll=document.getElementById('opt-pollms'),$dir=document.getElementById('opt-datadir'),$change=document.getElementById('btn-change-dir'),$open=document.getElementById('btn-open-dir'),$clear=document.getElementById('btn-clear'),$close=document.getElementById('close-btn'),$status=document.getElementById('status-left'); +function flash(msg){$status.textContent=msg;setTimeout(()=>$status.textContent='就绪',2000)} +// settings:set 在主进程先改运行时状态、后写盘;写盘失败(磁盘满/只读/杀软锁文件) +// 会带 saved:false 回来 —— 本次会话已生效但重启还原,必须明确提示, +// 不能让用户以为设置已经保存。 +function flashIfNotSaved(res){ + if(res&&res.saved===false){flash('⚠ 设置未保存到磁盘,重启后还原');return true} + return false +} +async function refresh(){const c=await get();$auto.checked=!!c.autoLaunch;$hotkey.value=c.hotkey||'';$max.value=String(c.maxItems);$display.value=String(c.displayLimit);$poll.value=String(c.pollMs);$dir.textContent=c.dataDir} +$auto.addEventListener('change',async()=>{const res=await set('autoLaunch',$auto.checked);if(!flashIfNotSaved(res))flash('已'+($auto.checked?'开启':'关闭')+'开机自启动')}); +$max.addEventListener('change',async()=>{const res=await set('maxItems',parseInt($max.value,10));if(!flashIfNotSaved(res))flash('最大条数已设为 '+$max.value)}); +$display.addEventListener('change',async()=>{const res=await set('displayLimit',parseInt($display.value,10));if(!flashIfNotSaved(res))flash('界面显示条数已设为 '+$display.value)}); +$poll.addEventListener('change',async()=>{const res=await set('pollMs',parseInt($poll.value,10));if(!flashIfNotSaved(res))flash('轮询间隔已设为 '+$poll.value+' ms')}); +$hotkey.addEventListener('change',async()=>{ + const res=await set('hotkey',$hotkey.value); + if(flashIfNotSaved(res))return; + if($hotkey.value&&res&&res.hotkeyRegistered===false){ + // 主进程 register 返回 false:组合键被别的程序占用。配置照存, + // 但必须告诉用户,否则他会以为快捷键坏了。 + flash('快捷键可能被其他程序占用,未生效'); + }else{ + flash($hotkey.value?'唤起快捷键已设为 '+$hotkey.value:'已禁用唤起快捷键'); + } +}); +$open.addEventListener('click',()=>openDir()); +$clear.addEventListener('click',async()=>{if(!confirm('确定清空所有非置顶历史记录?此操作不可撤销。'))return;flash('已清除 '+await clearHistory()+' 条')}); +$change.addEventListener('click',async()=>{if(typeof chooseDir!=='function')return;const res=await chooseDir();if(res&&res.ok===false&&res.message)flash(res.message)}); +$close.addEventListener('click',()=>window.close());document.addEventListener('keydown',e=>{if(e.key==='Escape')window.close()});refresh(); + +// ---------- 应用图标(标题栏左上角)---------- +// 一次性从主进程拿 PNG data URL 喂给 。与主窗口共用同一个 IPC, +// 主进程缓存命中后零成本;图标缺失静默失败,不影响设置面板功能。 +window.api.getAppIcon().then((dataUrl) => { + if (!dataUrl) return; + const el = document.getElementById('app-icon'); + if (el) el.src = dataUrl; +}).catch(() => { /* 图标缺失不影响功能 */ }); + +// ---------- 外链(关于页)---------- +// 窗口不能直接导航到外部 URL(会把设置页整个顶掉),点击统一拦下来 +// 走主进程 shell.openExternal 交给系统浏览器;scheme 白名单在主进程卡。 +for (const a of document.querySelectorAll('a.ext-link')) { + a.addEventListener('click', (e) => { e.preventDefault(); if (typeof openExternal === 'function') openExternal(a.href); }); +} + +// ---------- 主题 ---------- + +// 不要用 `const themeApi`:contextBridge 暴露的 window.themeApi 是不可配置属性, +// 顶层同名 const 会让整个脚本在实例化阶段抛 SyntaxError(一行都不执行)。 +const themeIpc = window.themeApi; +const { set: themeSet, setAccent, setFollowSystem, onSystemChanged } = themeIpc; + +const THEMES = [ + { id: 'linear', name: 'Linear Dark', desc: '极简暗色专业', bg: '#0E0E12', text: '#E6E6E9', accent: '#5E6AD2' }, + { id: 'midnight', name: 'Midnight', desc: '极致暗色', bg: '#08090B', text: '#E8E8EA', accent: '#7C3AED' }, + { id: 'carbon', name: 'Carbon', desc: 'VS Code 灰', bg: '#1A1D23', text: '#D4D4D4', accent: '#3B82F6' }, + { id: 'solarized', name: 'Solarized', desc: '经典 Sola', bg: '#002B36', text: '#93A1A1', accent: '#268BD2' }, + { id: 'github', name: 'GitHub Dark', desc: 'GitHub 风', bg: '#0D1117', text: '#C9D1D9', accent: '#8957E5' }, + { id: 'light', name: 'Light', desc: '白天模式', bg: '#FAFAFA', text: '#111827', accent: '#5E6AD2' }, +]; + +const ACCENTS = [ + { id: 'purple', color: '#5E6AD2' }, + { id: 'blue', color: '#3B82F6' }, + { id: 'green', color: '#10B981' }, + { id: 'orange', color: '#F59E0B' }, + { id: 'red', color: '#EF4444' }, +]; + +function renderThemes(currentTheme) { + const grid = document.getElementById('theme-grid'); + if (!grid) return; + grid.innerHTML = ''; + for (const t of THEMES) { + const card = document.createElement('div'); + card.className = 'theme-card' + (t.id === currentTheme ? ' active' : ''); + card.dataset.theme = t.id; + card.innerHTML = ` +
    + Aa + +
    +
    +
    ${t.name}
    +
    ${t.desc}
    +
    + `; + card.addEventListener('click', async () => { + const res = await themeSet(t.id); + if (res) { + const eff = res.effectiveTheme || res.theme; + document.documentElement.setAttribute('data-theme', eff); + document.documentElement.setAttribute('data-accent', res.accent); + try { + // rawTheme 在 themeSet 路径上 = eff(顺手把 followSystem 关了), + // 但旧主进程没发 rawTheme 字段,这里用 eff 兜底。 + localStorage.setItem('clipboard-theme', JSON.stringify({ + theme: res.rawTheme || eff, + effectiveTheme: eff, + accent: res.accent, + followSystem: res.followSystem, + })); + } catch (_) {} + document.getElementById('opt-follow-system').checked = false; + renderThemes(eff); + flashIfNotSaved(res); + } + }); + grid.appendChild(card); + } +} + +function renderAccents(currentAccent) { + const grid = document.getElementById('accent-grid'); + if (!grid) return; + grid.innerHTML = ''; + for (const a of ACCENTS) { + const sw = document.createElement('button'); + sw.className = 'accent-swatch' + (a.id === currentAccent ? ' active' : ''); + sw.dataset.accent = a.id; + sw.style.background = a.color; + sw.title = a.id; + sw.addEventListener('click', async () => { + const res = await setAccent(a.id); + if (res) { + document.documentElement.setAttribute('data-accent', res.accent); + try { + localStorage.setItem('clipboard-theme', JSON.stringify({ + theme: res.rawTheme || res.theme, + effectiveTheme: res.effectiveTheme || res.theme, + accent: res.accent, + followSystem: res.followSystem, + })); + } catch (_) {} + renderAccents(res.accent); + flashIfNotSaved(res); + } + }); + grid.appendChild(sw); + } +} + +const $follow = document.getElementById('opt-follow-system'); +if ($follow) { + $follow.addEventListener('change', async () => { + const res = await setFollowSystem($follow.checked); + if (res) { + try { + localStorage.setItem('clipboard-theme', JSON.stringify({ + theme: res.rawTheme || res.theme, + effectiveTheme: res.effectiveTheme || res.theme, + accent: res.accent, + followSystem: res.followSystem, + })); + } catch (_) {} + if (!flashIfNotSaved(res)) flash($follow.checked ? '已开启跟随系统' : '已关闭跟随系统'); + } + }); +} + +onSystemChanged((payload) => { + // 同 renderer:只更新 DOM,不写 localStorage(payload 里没有 rawTheme)。 + const eff = payload.effectiveTheme || payload.theme; + document.documentElement.setAttribute('data-theme', eff); + document.documentElement.setAttribute('data-accent', payload.accent); + renderThemes(eff); +}); + +async function initTheme() { + try { + // 先订阅再拉当前值,否则跟随系统改主题时这个窗口收不到广播 + try { await themeIpc.subscribe(); } + catch (e) { console.error('[theme] subscribe failed:', e.message); } + const cfg = await themeIpc.get(); + const eff = cfg.effectiveTheme || cfg.theme; + document.documentElement.setAttribute('data-theme', eff); + document.documentElement.setAttribute('data-accent', cfg.accent); + if ($follow) $follow.checked = cfg.followSystem; + renderThemes(eff); + renderAccents(cfg.accent); + } catch (e) { + console.error('[theme] initTheme failed:', e.message); + } +} +initTheme(); diff --git a/renderer/styles.css b/renderer/styles.css new file mode 100644 index 0000000..9a13f62 --- /dev/null +++ b/renderer/styles.css @@ -0,0 +1,952 @@ +/* 现代简约样式 —— 通过 token 系统接入 6 主题 / 5 强调色 */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + background: var(--bg-base); + color: var(--text); + font-family: var(--font-ui); + font-size: var(--fs-base); + line-height: var(--lh-base); + letter-spacing: var(--tracking-cn); + /* Windows + CJK 小字清晰度关键设置: + * - 默认 subpixel-antialiased 保留 RGB 子像素渲染,对中文笔画描边至关重要; + * 之前强制 antialiased/grayscale 会让 12px 以下的汉字变成"雾蒙蒙"的灰块。 + * - text-rendering 用 auto,让浏览器按字号自适应:中文小字走 subpixel, + * 英文大标题再走 optimizeLegibility 拿连字。 + * - font-feature 只在 ≥14px 的西文场景开启,避免给小字号增加渲染负担。 */ + -webkit-font-smoothing: subpixel-antialiased; + -moz-osx-font-smoothing: auto; + text-rendering: auto; + font-feature-settings: normal; + user-select: none; +} + +/* 大字号西文(标题、关于页)单独开启连字,小字号不参与 */ +#title, .about-name, .theme-preview-text { + -webkit-font-smoothing: subpixel-antialiased; + text-rendering: optimizeLegibility; + font-feature-settings: "kern" 1, "liga" 1, "calt" 1; +} + +#root { + display: flex; + flex-direction: column; + height: 100vh; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +/* ---------- 拖动区 ---------- */ + +#drag-bar { + -webkit-app-region: drag; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px 7px; + border-bottom: 1px solid var(--border-soft); + background: var(--bg-base); +} + +#title { + font-size: var(--fs-md); + font-weight: var(--fw-semibold); + color: var(--text); + letter-spacing: 0.2px; + /* 原本 margin-left: 4px 用来撑开标题与左边框的距离; + * 现在 #app-icon 在更左侧,由 #app-icon 的 margin-right 控制间隙更合理。 */ + margin-left: 0; + /* 标题单行:body 的 line-height=1.6 在这里会撑出多余空白, + * 关掉让标题跟按钮在同一基线。 */ + line-height: 1; +} + +/* 标题栏左上角小图标:16×16 显示位,源是 32×32 PNG(主进程从 .ico resize + * 而来),对常见 1.0/1.25/1.5/2x 缩放都锐利。 + * 父级 #drag-bar 已设 -webkit-app-region: drag,按住图标仍可拖动窗口。 */ +#app-icon { + width: 16px; + height: 16px; + display: block; + flex-shrink: 0; + margin-right: 6px; + border-radius: 3px; +} + +.spacer { flex: 1; } + +.icon-btn { + -webkit-app-region: no-drag; + width: 26px; + height: 26px; + padding: 0; + background: transparent; + border: none; + border-radius: 6px; + color: var(--text-muted); + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background var(--transition-fast), color var(--transition-fast); +} + +/* SVG 图标:颜色继承 currentColor,让 aria-pressed 状态色直接生效 */ +.icon-btn svg { + display: block; + color: inherit; +} + +.icon-btn:hover { + background: var(--bg-hover); + color: var(--text); +} + +#close-btn:hover { + background: var(--danger-bg); + color: var(--danger); +} + +/* 置顶开启:图标上色 + 轻微背景,关闭时回到 muted */ +#pin-btn[aria-pressed="true"] { + color: var(--accent); + background: var(--accent-soft); +} + +#pin-btn[aria-pressed="true"]:hover { + /* 开启态下 hover 不要洗掉上色,只把背景加深一点 */ + background: var(--accent-hover); + color: var(--accent); +} + +#refresh-btn.spinning svg { + animation: spin 0.8s linear infinite; + transform-origin: 50% 50%; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.ipc-dot { + display: inline-block; + width: 6px; + height: 6px; + margin: 0 4px 0 2px; + border-radius: 50%; + background: var(--text-muted); + transition: background var(--transition-base), box-shadow var(--transition-base); +} + +.ipc-dot.connected { + background: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15); +} + +.ipc-dot.error { + background: var(--danger); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15); +} + +/* ---------- Tab 栏 ---------- */ + +#tabs { + -webkit-app-region: no-drag; + display: flex; + gap: 2px; + padding: 0 12px; + background: var(--bg-base); + border-bottom: 1px solid var(--border-soft); +} + +/* 历史窗口的 Tab —— 作用域限定到 #tabs,避免被下面 settings 面板的 + * 通用 .tab 规则覆盖(同一文件后写的同优先级规则会赢,导致 padding / + * transition 跟设计稿不一致)。 */ +#tabs .tab { + padding: 8px 10px 7px; + border: none; + border-radius: 6px 6px 0 0; + background: transparent; + color: var(--text-soft); + font-size: var(--fs-sm); + font-weight: var(--fw-medium); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); + border-bottom: 2px solid transparent; + margin-bottom: -1px; + /* 与 #title 一致:行高压成 1,文字基线跟标题对齐;不要靠 padding 顶。 */ + line-height: 1; +} + +#tabs .tab:hover { + color: var(--text); +} + +#tabs .tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.tab-count { + display: inline-block; + margin-left: 5px; + padding: 0 6px; + background: var(--bg-hover); + border-radius: 8px; + font-size: 10.5px; + color: var(--text-muted); + line-height: 16px; + height: 16px; + font-weight: var(--fw-medium); + vertical-align: 1px; + font-variant-numeric: tabular-nums; + transition: background var(--transition-fast), color var(--transition-fast); +} + +#tabs .tab.active .tab-count { + background: var(--accent-soft); + color: var(--accent); +} + +/* ---------- 搜索 ---------- */ + +#search-row { + padding: 8px 12px 10px; + background: var(--bg-base); +} + +#search-wrap { + position: relative; + display: flex; + align-items: center; +} + +#search-icon { + position: absolute; + left: 10px; + color: var(--text-muted); + pointer-events: none; + transition: color var(--transition-fast); +} + +#search { + -webkit-app-region: no-drag; + width: 100%; + height: 32px; + padding: 0 32px 0 32px; + background: var(--bg-soft); + border: 1px solid transparent; + border-radius: 8px; + color: var(--text); + font-size: var(--fs-base); + line-height: 1.4; + outline: none; + transition: background var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base); +} + +#search:focus { + background: var(--bg-overlay); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +#search:focus + #search-clear, +#search-wrap:focus-within #search-icon { + color: var(--accent); +} + +#search::placeholder { + color: var(--text-muted); +} + +#search-clear { + -webkit-app-region: no-drag; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + padding: 0; + background: transparent; + border: none; + border-radius: 50%; + color: var(--text-muted); + line-height: 1; + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + transition: background var(--transition-fast), color var(--transition-fast); +} + +#search-clear.visible { + display: inline-flex; +} + +#search-clear:hover { + background: var(--bg-hover); + color: var(--text); +} + +/* ---------- 列表 ---------- */ + +#list-wrap { + flex: 1; + overflow-y: auto; + padding: 8px 12px 8px; + background: var(--bg-base); + scroll-behavior: smooth; +} + +#list { + list-style: none; + display: flex; + flex-direction: column; + gap: 6px; +} + +/* 自定义滚动条 */ +#list-wrap::-webkit-scrollbar { + width: 8px; +} +#list-wrap::-webkit-scrollbar-track { + background: transparent; +} +#list-wrap::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + border: none; +} +#list-wrap::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* ---------- 卡片 ---------- */ + +.card { + background: var(--bg-base); + border: 1px solid var(--border-soft); + border-radius: var(--radius-md); + padding: 9px 12px 10px; + cursor: pointer; + transition: background var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base), transform var(--transition-fast); + outline: none; + position: relative; +} + +.card:hover { + background: var(--bg-soft); + border-color: var(--border); + box-shadow: var(--shadow-sm); +} + +.card.selected { + background: var(--bg-overlay); + border-color: var(--border-strong); + border-left: 2px solid var(--accent); + padding-left: 11px; +} + +.card.selected:hover { + border-color: var(--accent); +} + +.card-head { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.time { + font-size: var(--fs-xs); + color: var(--text-soft); + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 10.5px; + color: var(--text-soft); + background: var(--bg-hover); + border-radius: 4px; + padding: 1px 6px 1px 5px; + font-weight: var(--fw-medium); + line-height: 1.5; + letter-spacing: 0; +} + +.badge svg { + display: block; + flex-shrink: 0; + opacity: 0.75; +} + +.badge-image { + color: var(--accent); +} + +.badge-image svg { + color: var(--accent); + opacity: 0.85; +} + +.slot { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 5px; + font-size: 11px; + font-weight: var(--fw-semibold); + color: var(--accent-text); + background: var(--accent); + border-radius: 5px; + letter-spacing: 0; + font-family: var(--font-mono); + line-height: 1; +} + +.action { + -webkit-app-region: no-drag; + display: inline-flex; + align-items: center; + gap: 4px; + font-size: var(--fs-xs); + color: var(--text-muted); + background: transparent; + border: none; + padding: 4px 8px; + border-radius: 6px; + cursor: pointer; + opacity: 0; + line-height: 1; + transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast); +} + +.action svg { + display: block; + flex-shrink: 0; +} + +.card:hover .action, +.card.selected .action { + opacity: 1; +} + +.action:hover { + color: var(--text); + background: var(--bg-hover); +} + +.action.copy { + color: var(--accent); + padding-right: 10px; +} + +.action.copy:hover { + color: var(--accent); + background: var(--accent-hover); +} + +.action.fav { + padding: 5px 7px; + color: var(--text-muted); +} + +.action.fav svg { + fill: none; + transition: fill var(--transition-fast), color var(--transition-fast); +} + +.action.fav.favorited { + color: var(--warn); +} + +.action.fav.favorited svg { + fill: var(--warn); +} + +.action.fav:hover { + color: var(--warn); + background: var(--warn-bg); +} + +.action.delete { + padding: 5px 7px; +} + +.action.delete:hover { + color: var(--danger); + background: var(--danger-bg); +} + +.card-body { + display: flex; + align-items: center; + gap: 10px; +} + +.text-preview { + flex: 1; + min-width: 0; + font-size: var(--fs-base); + color: var(--text); + /* 中文正文行高 ≥1.55 才不会出现笔画"黏连",这里设 1.6 让 13px 中文清爽 */ + line-height: 1.6; + word-break: break-all; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + user-select: text; + cursor: text; +} + +.image-preview { + width: 88px; + height: 88px; + border-radius: 8px; + border: 1px solid var(--border-soft); + object-fit: contain; + background: + linear-gradient(45deg, var(--bg-soft) 25%, transparent 25%), + linear-gradient(-45deg, var(--bg-soft) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--bg-soft) 75%), + linear-gradient(-45deg, transparent 75%, var(--bg-soft) 75%); + background-size: 12px 12px; + background-position: 0 0, 0 6px, 6px -6px, -6px 0; +} + +/* ---------- 空状态 ---------- */ + +#empty { + text-align: center; + padding: 64px 24px 56px; + color: var(--text-soft); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.empty-icon { + color: var(--text-muted); + opacity: 0.55; + margin-bottom: 14px; +} + +.empty-title { + font-size: var(--fs-md); + color: var(--text-soft); + font-weight: var(--fw-medium); + margin-bottom: 6px; +} + +#empty .muted { + font-size: var(--fs-sm); + color: var(--text-muted); + line-height: 1.5; +} + +.hidden { display: none !important; } + +/* ---------- 状态栏 ---------- */ + +#status { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 14px; + border-top: 1px solid var(--border-soft); + background: var(--bg-base); + font-size: var(--fs-xs); + line-height: 1.4; + color: var(--text-muted); + user-select: none; +} + +#status-left { + color: var(--text-soft); +} + +.shortcuts { + display: inline-flex; + align-items: center; + gap: 4px; + flex-wrap: nowrap; + white-space: nowrap; + overflow: hidden; +} + +.shortcuts .dot { + color: var(--text-muted); + opacity: 0.5; + margin: 0 1px; +} + +kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: var(--fw-medium); + color: var(--text-soft); + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 4px; + line-height: 1; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04); +} + +/* ---------- 设置面板 ---------- */ +#settings-body { + flex: 1; + min-height: 0; + overflow: hidden; + padding: 0; + background: var(--bg-base); + display: flex; + flex-direction: column; +} + +/* ---------- 设置面板:标签栏 ---------- */ +/* 用 #tab-bar 前缀隔离:上面「历史/收藏」Tab 也用 .tab,但 padding/transition + * 跟设置页 Tab 不一样。两份样式同特异性时位置靠后的赢,会把主窗口 Tab + * 的 padding 从 5px 12px 改成 6px 10px 5px —— 显式框死作用域避免串味。 */ + +#tab-bar { + display: flex; + gap: 2px; + padding: 0 12px; + background: var(--bg-base); + border-bottom: 1px solid var(--border-soft); +} + +#tab-bar .tab { + appearance: none; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + padding: 8px 10px 7px; + font-family: inherit; + font-size: var(--fs-sm); + color: var(--text-soft); + cursor: pointer; + line-height: 1; + margin-bottom: -1px; + transition: color 0.12s ease, border-color 0.12s ease; +} + +#tab-bar .tab:hover { color: var(--text); } + +#tab-bar .tab[aria-selected="true"] { + color: var(--text); + border-bottom-color: var(--accent); + font-weight: var(--fw-semibold); +} + +#tab-bar .tab:focus-visible { outline: 1px solid var(--accent); outline-offset: -2px; } + +.tab-panel { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* 面板滚动条 —— 跟主列表统一 */ +.tab-panel::-webkit-scrollbar { width: 8px; } +.tab-panel::-webkit-scrollbar-track { background: transparent; } +.tab-panel::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +.tab-panel::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +.tab-panel[hidden] { display: none !important; } +.setting-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 14px; + border: 1px solid var(--border-soft); + border-radius: var(--radius-md); + background: var(--bg-base); + line-height: 1.4; + transition: border-color var(--transition-fast); +} + +.setting-row:hover { + border-color: var(--border); +} + +.setting-row.column { + flex-direction: column; + align-items: stretch; + gap: 8px; +} +.setting-label { font-size: var(--fs-base); color: var(--text); font-weight: var(--fw-medium); } +.setting-row select { + height: 30px; + padding: 0 28px 0 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-soft); + color: var(--text); + font-size: var(--fs-base); + line-height: 1.4; + outline: none; + cursor: pointer; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + appearance: none; + -webkit-appearance: none; + background-image: + linear-gradient(45deg, transparent 50%, var(--text-muted) 50%), + linear-gradient(-45deg, transparent 50%, var(--text-muted) 50%); + background-position: calc(100% - 13px) 12px, calc(100% - 8px) 12px; + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; +} +.setting-row select:hover { border-color: var(--border-strong); } +.setting-row select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.path-box { + margin-top: 4px; + padding: 9px 11px; + background: var(--bg-soft); + border-radius: 6px; + border: 1px solid var(--border-soft); +} +.path-text { font-size: var(--fs-sm); color: var(--text-soft); word-break: break-all; user-select: text; font-family: var(--font-mono); } +.btn-row { display: flex; gap: 8px; margin-top: 4px; } +.btn { + height: 30px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-base); + color: var(--text); + font-size: var(--fs-sm); + line-height: 1; + cursor: pointer; + transition: background var(--transition-fast), border-color var(--transition-fast); + font-weight: var(--fw-medium); +} +.btn:hover { background: var(--bg-hover); border-color: var(--border-strong); } +.btn:active { background: var(--bg-soft); } +.btn-danger { color: var(--danger); border-color: var(--danger-bg); background: var(--danger-bg); } +.btn-danger:hover { background: var(--danger); color: var(--accent-text); border-color: var(--danger); } +.danger-zone { border-color: var(--danger-bg); } +.danger-zone:hover { border-color: rgba(239, 68, 68, 0.4); } + +/* 开关 */ +.switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; } +.switch input { opacity: 0; width: 0; height: 0; } +.slider-track { + position: absolute; inset: 0; cursor: pointer; + background: var(--border-strong); border-radius: 20px; + transition: background var(--transition-base); +} +.slider-track::before { + content: ""; position: absolute; height: 16px; width: 16px; left: 2px; top: 2px; + background: var(--bg-overlay); border-radius: 50%; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); + transition: transform var(--transition-base); +} +.switch input:checked + .slider-track { background: var(--accent); } +.switch input:checked + .slider-track::before { transform: translateX(16px); } +.switch input:focus-visible + .slider-track { box-shadow: 0 0 0 3px var(--accent-soft); } + +/* 主题网格 */ +.theme-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-top: 4px; +} + +.theme-card { + cursor: pointer; + border: 2px solid var(--border-soft); + border-radius: var(--radius-md); + padding: 6px; + background: var(--bg-base); + transition: border-color var(--transition-base), transform var(--transition-fast), box-shadow var(--transition-base); + text-align: left; +} + +.theme-card:hover { + border-color: var(--border-strong); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.theme-card.active { + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent) inset; +} + +.theme-preview { + position: relative; + height: 56px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: flex-start; + padding: 8px 12px; + border: 1px solid transparent; + overflow: hidden; +} + +.theme-preview-text { + font-family: var(--font-ui); + font-weight: var(--fw-semibold); + font-size: 22px; + letter-spacing: -0.5px; +} + +.theme-preview-accent { + position: absolute; + right: 10px; + top: 10px; + width: 12px; + height: 12px; + border-radius: 50%; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); +} + +.theme-info { + margin-top: 7px; +} + +.theme-name { + font-size: var(--fs-sm); + color: var(--text); + font-weight: var(--fw-medium); +} + +.theme-desc { + font-size: 11px; + color: var(--text-muted); + margin-top: 1px; +} + +/* 强调色 */ +.accent-grid { + display: flex; + gap: 10px; + margin-top: 4px; +} + +.accent-swatch { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid var(--border-soft); + cursor: pointer; + padding: 0; + position: relative; + transition: transform var(--transition-fast), box-shadow var(--transition-base); +} + +.accent-swatch:hover { + transform: scale(1.08); +} + +.accent-swatch.active { + box-shadow: 0 0 0 2px var(--bg-base), 0 0 0 4px var(--text); +} + +.accent-swatch.active::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 50%; + background-image: url("data:image/svg+xml;utf8,"); + background-size: 16px 16px; + background-position: center; + background-repeat: no-repeat; +} + +/* ---------- 设置面板:关于页 ---------- */ + +.about-head { + text-align: center; + padding: 18px 0 8px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.about-icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: linear-gradient(135deg, var(--accent), var(--accent-hover)); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-text); + margin-bottom: 6px; + box-shadow: 0 2px 8px var(--accent-soft); +} + +.about-icon svg { + display: block; +} + +.about-name { + font-size: var(--fs-lg); + font-weight: var(--fw-semibold); + color: var(--text); + letter-spacing: 0.2px; +} + +.about-tagline { + font-size: var(--fs-xs); + color: var(--text-muted); + margin-top: 2px; +} + +.about-devline { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 10px; + margin-top: 4px; +} +.about-dev { font-size: var(--fs-base); color: var(--text); } +.ext-link { + font-size: var(--fs-sm); + color: var(--accent); + text-decoration: none; + word-break: break-all; + cursor: pointer; + transition: opacity var(--transition-fast); +} +.ext-link:hover { text-decoration: underline; opacity: 0.85; } diff --git a/renderer/theme-boot.js b/renderer/theme-boot.js new file mode 100644 index 0000000..75cde9a --- /dev/null +++ b/renderer/theme-boot.js @@ -0,0 +1,21 @@ +'use strict'; +/** + * 首屏主题预应用:在首次绘制前从 localStorage 读取上次的主题/强调色, + * 避免等 IPC 返回期间的白屏闪烁。 + * + * 必须是「外部脚本」——页面 CSP 为 default-src 'self',未开 script-src 'unsafe-inline', + * 内联