From 514d62644e53d34ba94f38b105e188ebae416a34 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Wed, 22 Apr 2026 11:15:41 +0800 Subject: [PATCH] =?UTF-8?q?v2.0.0:=20=E4=B8=BB=E9=A2=98=E7=B3=BB=E7=BB=9F+?= =?UTF-8?q?SSH=E7=A7=81=E9=92=A5=E8=A7=A3=E6=9E=90=E4=BF=AE=E5=A4=8D+?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=8C=81=E4=B9=85=E5=8C=96+=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=B1=A0=E4=BC=98=E5=8C=96+UI=E6=89=93=E7=A3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心修复: - ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2, 解决 'Cannot parse privateKey: Unsupported key format' 错误 - useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey - sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题 - database-service.ts: updateSession() 补充 private_key_path 字段持久化 - SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa' UI/UX: - 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计 - macOS 标题栏支持双击最大化(window:toggleMaximize IPC) - macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,) - WelcomeView 4卡片布局,移除 Settings 按钮 - TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线 架构优化: - SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard) - 连接池竞态修复,支持快速多标签连接 - IPC 接口规范化,安全错误处理 - Vite 代码分割优化 质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass --- .editorconfig | 12 + .eslintrc.cjs | 4 + .github/workflows/ci.yml | 81 ++ .lintstagedrc.json | 4 + .prettierrc | 8 + AGENTS.md | 5 +- electron/ipc/app.ipc.ts | 2 +- electron/ipc/file.ipc.ts | 163 ++- electron/ipc/index.ts | 17 +- electron/ipc/session.ipc.ts | 23 +- electron/ipc/settings.ipc.ts | 8 +- electron/ipc/sftp.ipc.ts | 67 +- electron/ipc/ssh.ipc.ts | 124 +- electron/ipc/zmodem.ipc.ts | 12 +- electron/main.ts | 31 +- electron/preload.ts | 104 +- electron/services/crypto.ts | 115 +- electron/services/database-service.ts | 100 +- electron/services/key-service.ts | 66 +- electron/services/ssh-service.ts | 95 +- electron/services/zmodem-service.ts | 66 +- electron/tsconfig.json | 1 - index.html | 8 +- package-lock.json | 1242 ++++++++++++++--- package.json | 22 +- postcss.config.js | 2 +- shared/types/ipc.types.ts | 2 +- shared/types/session.types.ts | 1 + src/App.tsx | 123 +- src/components/ConnectionPanel.tsx | 6 +- src/components/PortForwardView.tsx | 6 +- src/components/QuickConnectDialog.tsx | 4 +- src/components/SFTPView.tsx | 20 +- src/components/SessionDialog.tsx | 12 +- src/components/SettingsView.tsx | 54 +- src/components/Sidebar.tsx | 23 +- src/components/StatusBar.tsx | 2 +- src/components/TabBar.tsx | 9 +- src/components/TabContent.tsx | 9 +- src/components/TerminalView.tsx | 12 +- src/components/WelcomeView.tsx | 31 +- src/components/common/DefaultFallback.tsx | 54 + src/components/common/ErrorBoundary.tsx | 53 +- src/components/common/Skeleton.tsx | 2 +- src/components/common/Toast.tsx | 14 +- src/components/sftp/ChmodDialog.tsx | 2 +- src/components/sftp/ConnectionPanel.tsx | 2 +- src/components/sftp/ConnectionSelector.tsx | 2 +- src/components/sftp/ContextMenu.tsx | 2 +- src/components/sftp/FileListPanel.tsx | 8 +- src/components/sftp/FilePropertiesDialog.tsx | 2 +- src/components/sftp/FileRow.tsx | 2 +- src/components/sftp/SortHeader.tsx | 2 +- src/components/sftp/Toolbar.tsx | 2 +- src/components/sftp/TransferPanel.tsx | 4 +- src/components/sidebar/SessionTree.tsx | 2 +- src/components/snippets/SnippetPanel.tsx | 6 +- src/components/tabbar/TabItem.tsx | 6 +- src/components/terminal/TerminalOverlay.tsx | 5 +- src/components/terminal/TerminalSearchBar.tsx | 6 +- src/electron.d.ts | 126 +- src/hooks/useKeyboardShortcuts.ts | 2 +- src/hooks/useSFTP.ts | 95 +- src/hooks/useTerminalConnection.ts | 28 +- src/hooks/useTheme.ts | 25 +- src/index.css | 60 +- src/stores/connection/poolStore.ts | 4 +- src/stores/connection/sessionHelper.ts | 1 + src/stores/sessionStore.ts | 17 +- src/stores/tabStore.ts | 13 +- src/themes/app-theme.ts | 46 + tailwind.config.js | 71 +- test/services/crypto.test.ts | 95 ++ test/setup.ts | 4 +- test/stores/connectionPool.test.ts | 2 +- test/stores/tabStore.test.ts | 8 +- vite.config.ts | 25 +- 77 files changed, 2563 insertions(+), 931 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .lintstagedrc.json create mode 100644 .prettierrc create mode 100644 src/components/common/DefaultFallback.tsx create mode 100644 test/services/crypto.test.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4039ff1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d6c9537..2f12460 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -14,5 +14,9 @@ module.exports = { 'warn', { allowConstantExport: true }, ], + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], }, } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e1167f4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint-and-typecheck: + name: Lint & TypeCheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: TypeCheck + run: npm run typecheck + + - name: Format Check + run: npm run format:check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test + + - name: Upload coverage + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage/ + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..b6b9051 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*.{ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{css,json,md}": ["prettier --write"] +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..37ae408 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "es5", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/AGENTS.md b/AGENTS.md index e0aaaae..dfd8d05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,10 @@ ### 当前版本状态 -- **版本**: v1.0.0 -- **状态**: 已发布 (Release) +- **版本**: v2.0.0 +- **状态**: 已发布 (Release) - 安全增强版 - **已完成**: 核心 SSH/SFTP 功能、加密存储、多标签页系统、Zmodem 传输 +- **v2.0 优化**: safeStorage 主密钥加密、路径遍历防护、PBKDF2 600K 迭代、类型安全、工程化建设、性能优化 - **技术债务**: 部分组件仍在迁移到新的连接池架构 --- diff --git a/electron/ipc/app.ipc.ts b/electron/ipc/app.ipc.ts index 8863136..52bda03 100644 --- a/electron/ipc/app.ipc.ts +++ b/electron/ipc/app.ipc.ts @@ -3,7 +3,7 @@ * 处理应用级别信息 */ -import { ipcMain, IpcMainInvokeEvent, app } from 'electron' +import { ipcMain, app } from 'electron' // IPC channel names export const APP_CHANNELS = { diff --git a/electron/ipc/file.ipc.ts b/electron/ipc/file.ipc.ts index 43c94d5..2896a14 100644 --- a/electron/ipc/file.ipc.ts +++ b/electron/ipc/file.ipc.ts @@ -1,6 +1,7 @@ /** * File IPC Handlers * 处理本地文件操作和远程文件读写 + * 安全增强:防止路径遍历攻击 */ import { ipcMain, IpcMainInvokeEvent, app } from 'electron' @@ -21,25 +22,106 @@ export const FILE_CHANNELS = { LOCAL_MKDIR: 'local:mkdir', } as const +// 允许的文件操作基础目录白名单 +const ALLOWED_BASE_PATHS: string[] = [] + +/** + * 初始化允许的路径白名单 + * 防止路径遍历攻击 + */ +export function initializeAllowedPaths(): void { + ALLOWED_BASE_PATHS.push( + app.getPath('home'), + app.getPath('downloads'), + app.getPath('documents'), + app.getPath('desktop'), + app.getPath('temp'), + app.getPath('userData'), + ) + // 添加平台特定的常用目录 + if (process.platform === 'darwin') { + ALLOWED_BASE_PATHS.push('/tmp', '/var/tmp') + } else if (process.platform === 'linux') { + ALLOWED_BASE_PATHS.push('/tmp', '/var/tmp', '/home') + } else if (process.platform === 'win32') { + ALLOWED_BASE_PATHS.push(process.env.LOCALAPPDATA || '', process.env.TEMP || '') + } +} + +/** + * 验证文件路径是否在允许的范围内 + * 防止路径遍历攻击(如 ../../../etc/passwd) + */ +function isPathAllowed(filePath: string): boolean { + // 规范化路径(解析 .. 和 .) + const resolved = path.resolve(filePath) + + // 检查是否在允许的基目录下 + const isUnderAllowedPath = ALLOWED_BASE_PATHS.some((base) => { + if (!base) return false + const relative = path.relative(base, resolved) + return !relative.startsWith('..') && !path.isAbsolute(relative) + }) + + if (isUnderAllowedPath) return true + + // 如果路径存在,检查其实际位置(处理符号链接) + try { + const real = fs.realpathSync(resolved) + return ALLOWED_BASE_PATHS.some((base) => { + if (!base) return false + const relative = path.relative(base, real) + return !relative.startsWith('..') && !path.isAbsolute(relative) + }) + } catch { + return false + } +} + +/** + * 安全地规范化路径 + */ +function sanitizePath(inputPath: string): string { + // 移除 null 字节 + const cleaned = inputPath.replace(/\0/g, '') + // 规范化路径 + return path.resolve(cleaned) +} + export function registerFileIPCHandlers(): void { + // 确保白名单已初始化 + if (ALLOWED_BASE_PATHS.length === 0) { + initializeAllowedPaths() + } + // Read file (for rz) ipcMain.handle(FILE_CHANNELS.READ, async (_event: IpcMainInvokeEvent, filePath: string) => { try { - const data = fs.readFileSync(filePath) + const safePath = sanitizePath(filePath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + const data = fs.readFileSync(safePath) return { success: true, data: data.toString('base64') } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Write file (for sz) ipcMain.handle(FILE_CHANNELS.WRITE, async (_event: IpcMainInvokeEvent, filePath: string, data: string) => { try { + const safePath = sanitizePath(filePath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } const buffer = Buffer.from(data, 'base64') - fs.writeFileSync(filePath, buffer) + fs.writeFileSync(safePath, buffer) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) @@ -48,15 +130,20 @@ export function registerFileIPCHandlers(): void { try { const homePath = app.getPath('home') return { success: true, path: homePath } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Read directory contents ipcMain.handle(FILE_CHANNELS.LOCAL_READ_DIR, async (_event: IpcMainInvokeEvent, dirPath: string) => { try { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }) + const safePath = sanitizePath(dirPath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + const entries = fs.readdirSync(safePath, { withFileTypes: true }) const files = entries.map((entry) => { const fullPath = path.join(dirPath, entry.name) let stats: fs.Stats | null = null @@ -84,63 +171,85 @@ export function registerFileIPCHandlers(): void { }) return { success: true, files } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Get file stats ipcMain.handle(FILE_CHANNELS.LOCAL_GET_STATS, async (_event: IpcMainInvokeEvent, filePath: string) => { try { - const stats = fs.lstatSync(filePath) + const safePath = sanitizePath(filePath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + const stats = fs.lstatSync(safePath) return { success: true, stats: { - name: path.basename(filePath), - path: filePath, + name: path.basename(safePath), + path: safePath, type: stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'file', size: stats.size, modified: Math.floor(stats.mtime.getTime() / 1000), permissions: formatPermissions(stats.mode), }, } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Delete file or directory ipcMain.handle(FILE_CHANNELS.LOCAL_DELETE, async (_event: IpcMainInvokeEvent, filePath: string) => { try { - const stats = fs.lstatSync(filePath) + const safePath = sanitizePath(filePath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + const stats = fs.lstatSync(safePath) if (stats.isDirectory()) { - fs.rmSync(filePath, { recursive: true, force: true }) + fs.rmSync(safePath, { recursive: true, force: true }) } else { - fs.unlinkSync(filePath) + fs.unlinkSync(safePath) } return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Rename file or directory ipcMain.handle(FILE_CHANNELS.LOCAL_RENAME, async (_event: IpcMainInvokeEvent, oldPath: string, newPath: string) => { try { - fs.renameSync(oldPath, newPath) + const safeOld = sanitizePath(oldPath) + const safeNew = sanitizePath(newPath) + if (!isPathAllowed(safeOld) || !isPathAllowed(safeNew)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + fs.renameSync(safeOld, safeNew) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // Create directory ipcMain.handle(FILE_CHANNELS.LOCAL_MKDIR, async (_event: IpcMainInvokeEvent, dirPath: string) => { try { - fs.mkdirSync(dirPath, { recursive: true }) + const safePath = sanitizePath(dirPath) + if (!isPathAllowed(safePath)) { + return { success: false, error: 'Access denied: path traversal detected' } + } + fs.mkdirSync(safePath, { recursive: true }) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) } diff --git a/electron/ipc/index.ts b/electron/ipc/index.ts index 1f4c22c..fe18866 100644 --- a/electron/ipc/index.ts +++ b/electron/ipc/index.ts @@ -16,9 +16,10 @@ import { registerSFTPIPCHandlers } from './sftp.ipc' import { registerSessionIPCHandlers } from './session.ipc' import { registerSettingsIPCHandlers } from './settings.ipc' import { registerDialogIPCHandlers } from './dialog.ipc' -import { registerFileIPCHandlers } from './file.ipc' +import { registerFileIPCHandlers, initializeAllowedPaths } from './file.ipc' import { registerZmodemIPCHandlers } from './zmodem.ipc' import { registerAppIPCHandlers } from './app.ipc' +import { ipcMain } from 'electron' export interface IPCOptions { sshService: SSHService @@ -45,7 +46,8 @@ export function registerAllIPCHandlers(options: IPCOptions): void { // Dialog handlers registerDialogIPCHandlers({ mainWindowGetter }) - // File handlers + // File handlers (初始化路径白名单) + initializeAllowedPaths() registerFileIPCHandlers() // Zmodem handlers @@ -54,6 +56,17 @@ export function registerAllIPCHandlers(options: IPCOptions): void { // App handlers registerAppIPCHandlers() + // Window handlers + ipcMain.handle('window:toggleMaximize', () => { + const win = mainWindowGetter() + if (!win) return + if (win.isMaximized()) { + win.unmaximize() + } else { + win.maximize() + } + }) + logger.info('[IPC] All handlers registered') } diff --git a/electron/ipc/session.ipc.ts b/electron/ipc/session.ipc.ts index b696029..ebf194a 100644 --- a/electron/ipc/session.ipc.ts +++ b/electron/ipc/session.ipc.ts @@ -5,6 +5,7 @@ import { ipcMain, IpcMainInvokeEvent } from 'electron' import type { DatabaseService } from '../services/database-service' +import type { SessionUpdateRequest } from '../../shared/types/session.types' // IPC channel names export const SESSION_CHANNELS = { @@ -27,8 +28,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void { try { const result = dbService.createSession(session) return { success: true, session: result } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -37,8 +38,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void { try { const sessions = dbService.getAllSessions() return { success: true, sessions } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -47,18 +48,18 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void { try { const session = dbService.getSessionById(id) return { success: true, session } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) // Update session - ipcMain.handle(SESSION_CHANNELS.UPDATE, async (_event: IpcMainInvokeEvent, id: string, updates: any) => { + ipcMain.handle(SESSION_CHANNELS.UPDATE, async (_event: IpcMainInvokeEvent, id: string, updates: SessionUpdateRequest) => { try { const result = dbService.updateSession(id, updates) return { success: true, session: result } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -67,8 +68,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void { try { dbService.deleteSession(id) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) } diff --git a/electron/ipc/settings.ipc.ts b/electron/ipc/settings.ipc.ts index 56dad41..3627ce4 100644 --- a/electron/ipc/settings.ipc.ts +++ b/electron/ipc/settings.ipc.ts @@ -24,8 +24,8 @@ export function registerSettingsIPCHandlers(options: SettingsIPCOptions): void { try { const value = dbService.getSetting(key) return { success: true, value } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -34,8 +34,8 @@ export function registerSettingsIPCHandlers(options: SettingsIPCOptions): void { try { dbService.setSetting(key, value) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) } diff --git a/electron/ipc/sftp.ipc.ts b/electron/ipc/sftp.ipc.ts index bbb6cf9..e642af1 100644 --- a/electron/ipc/sftp.ipc.ts +++ b/electron/ipc/sftp.ipc.ts @@ -26,11 +26,11 @@ export interface SFTPIPCOptions { } // ===== 彩色调试日志 ===== -const DEBUG = true +const _DEBUG = true function logHeader(msg: string) { console.log(`%c${msg}`, 'background: #16a085; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;') } -function logInfo(label: string, value: any) { +function logInfo(label: string, value: unknown) { console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #1abc9c; font-weight: bold;') } function logSuccess(msg: string) { @@ -52,9 +52,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { const sftp = await sshService.createSFTP(connectionId) logSuccess(`SFTP connected: ${sftp.id}`) return { success: true, sftpId: sftp.id } - } catch (error: any) { - logError(`SFTP connect failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`SFTP connect failed: ${message}`) + return { success: false, error: message } } }) @@ -68,9 +69,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { const files = await sshService.sftpListDirectory(sftpId, dirPath) logSuccess(`Listed ${files.length} items`) return { success: true, files } - } catch (error: any) { - logError(`List failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`List failed: ${message}`) + return { success: false, error: message } } }) @@ -97,9 +99,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { }) logSuccess('Upload complete') return { success: true } - } catch (error: any) { - logError(`Upload failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Upload failed: ${message}`) + return { success: false, error: message } } }) @@ -126,9 +129,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { }) logSuccess('Download complete') return { success: true } - } catch (error: any) { - logError(`Download failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Download failed: ${message}`) + return { success: false, error: message } } }) @@ -148,9 +152,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { await sshService.sftpRename(sftpId, oldPath, newPath) logSuccess('Rename complete') return { success: true } - } catch (error: any) { - logError(`Rename failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Rename failed: ${message}`) + return { success: false, error: message } } }) @@ -170,9 +175,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { await sshService.sftpDelete(sftpId, filePath, isDirectory) logSuccess('Delete complete') return { success: true } - } catch (error: any) { - logError(`Delete failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Delete failed: ${message}`) + return { success: false, error: message } } }) @@ -190,9 +196,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { await sshService.sftpMkdir(sftpId, dirPath) logSuccess('Mkdir complete') return { success: true } - } catch (error: any) { - logError(`Mkdir failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Mkdir failed: ${message}`) + return { success: false, error: message } } }) @@ -210,9 +217,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { const stats = await sshService.sftpStat(sftpId, filePath) logSuccess(`Stat complete: ${stats.size} bytes`) return { success: true, stats } - } catch (error: any) { - logError(`Stat failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Stat failed: ${message}`) + return { success: false, error: message } } }) @@ -232,9 +240,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void { await sshService.sftpChmod(sftpId, filePath, mode) logSuccess('Chmod complete') return { success: true } - } catch (error: any) { - logError(`Chmod failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logError(`Chmod failed: ${message}`) + return { success: false, error: message } } }) } diff --git a/electron/ipc/ssh.ipc.ts b/electron/ipc/ssh.ipc.ts index cdad6d3..fb62113 100644 --- a/electron/ipc/ssh.ipc.ts +++ b/electron/ipc/ssh.ipc.ts @@ -1,6 +1,7 @@ /** * SSH IPC Handlers * 处理 SSH 连接、Shell、会话、端口转发相关 IPC + * 安全增强:规范化错误处理,不暴露敏感信息 */ import { ipcMain, IpcMainInvokeEvent } from 'electron' @@ -10,22 +11,22 @@ import { createLogger } from '../utils/logger' const logger = createLogger('SSH_IPC') -// ===== 彩色调试日志 ===== -const DEBUG = true -function debug(...args: any[]) { - if (DEBUG) console.log('[SSH_IPC]', ...args) +// 生产环境日志:使用结构化日志,不输出敏感信息 +const DEBUG = process.env.NODE_ENV === 'development' +function debug(...args: unknown[]) { + if (DEBUG) logger.debug('[SSH_IPC]', ...args) } function logHeader(msg: string) { - console.log(`%c${msg}`, 'background: #9b59b6; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;') + logger.info(`[${msg}]`) } -function logInfo(label: string, value: any) { - console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #3498db; font-weight: bold;') +function logInfo(label: string, value: unknown) { + logger.info(`${label}: ${value}`) } function logSuccess(msg: string) { - console.log(`%c✓ ${msg}`, 'color: #27ae60; font-weight: bold;') + logger.info(`[OK] ${msg}`) } function logError(msg: string) { - console.log(`%c✗ ${msg}`, 'color: #e74c3c; font-weight: bold;') + logger.error(msg) } // IPC channel names @@ -49,15 +50,16 @@ export interface SSHIPCOptions { } export function registerSSHIPCHandlers(options: SSHIPCOptions): void { - const { sshService, zmodemService, mainWindowGetter } = options + const { sshService, zmodemService, mainWindowGetter: _mainWindowGetter } = options // SSH connect ipcMain.handle(SSH_CHANNELS.CONNECT, async (_event: IpcMainInvokeEvent, config) => { try { const connection = await sshService.connect(config) return { success: true, connectionId: connection.id } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) @@ -66,8 +68,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { try { await sshService.disconnect(connectionId) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) @@ -76,59 +79,49 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { try { const result = await sshService.exec(connectionId, command) return { success: true, result } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) // SSH create shell ipcMain.handle(SSH_CHANNELS.CREATE_SHELL, async (event: IpcMainInvokeEvent, connectionId: string, options?: { cols?: number; rows?: number }, shellId?: string) => { - console.log('=== SSH_IPC CREATE_SHELL ===') - console.log(' connectionId:', connectionId) - console.log(' options:', options) - console.log(' preferredShellId:', shellId) - + debug('CREATE_SHELL connectionId:', connectionId, 'preferredId:', shellId) + try { const preferredShellId = shellId logger.info('[SSH_IPC] Creating shell for connection:', connectionId, 'preferredId:', preferredShellId) debug('>>> Calling sshService.createShell...') - + const shell = await sshService.createShell(connectionId, options || {}, (shellId, data) => { // 检查 sender 是否已被销毁 if (event.sender.isDestroyed()) { - console.log('=== SHELL DATA CALLBACK - SKIPPED (sender destroyed) ===') + debug('SHELL DATA CALLBACK - SKIPPED (sender destroyed)') return } - - const text = data.toString('utf-8') - console.log('=== SHELL DATA CALLBACK ===') - console.log(' shellId:', shellId) - console.log(' data length:', data.length) - console.log(' data preview:', text.substring(0, 100).replace(/\n/g, '\\n')) - - // 打印所有收到的数据前100字符用于调试 - if (text.length > 0) { - logger.info(`[SSH_IPC] Shell data from ${shellId}:`, text.substring(0, 100).replace(/\n/g, '\\n')) - } - + + debug('SHELL DATA CALLBACK shellId:', shellId, 'data length:', data.length) + try { - console.log('>>> Sending SHELL_DATA to renderer...') + debug('>>> Sending SHELL_DATA to renderer...') event.sender.send(SSH_CHANNELS.SHELL_DATA, connectionId, shellId, data) - console.log('>>> SHELL_DATA sent successfully') + debug('>>> SHELL_DATA sent successfully') } catch (err) { - console.error('>>> FAILED to send SHELL_DATA:', err) + logger.error('>>> FAILED to send SHELL_DATA:', err) } - + zmodemService.feedShellData(shellId, data) }, preferredShellId) - + logger.info('[SSH_IPC] Shell created:', shell.id) debug('>>> Shell created successfully:', shell.id) return { success: true, shellId: shell.id } - } catch (error: any) { + } catch (error) { logger.error('[SSH_IPC] Shell creation failed:', error) - debug('>>> Shell creation FAILED:', error.message) - return { success: false, error: error.message } + const message = error instanceof Error ? error.message : String(error) + debug('>>> Shell creation FAILED:', message) + return { success: false, error: message } } }) @@ -137,8 +130,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { try { sshService.shellWrite(shellId, data) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) @@ -147,8 +141,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { try { sshService.shellResize(shellId, cols, rows) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { success: false, error: message } } }) @@ -160,19 +155,20 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { remoteHost: string, remotePort: number ) => { - logHeader('🔀 IPC FORWARD_LOCAL') + logHeader('FORWARD_LOCAL') logInfo('connectionId', connectionId) logInfo('localPort', localPort) logInfo('remoteHost', remoteHost) logInfo('remotePort', remotePort) - + try { const result = await sshService.forwardLocal(connectionId, localPort, remoteHost, remotePort) logSuccess(`Local forward bound to port ${result.boundPort}`) return { success: true, boundPort: result.boundPort } - } catch (error: any) { - logError(`Local forward failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logError(`Local forward failed: ${message}`) + return { success: false, error: message } } }) @@ -184,19 +180,20 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { localHost: string, localPort: number ) => { - logHeader('🔀 IPC FORWARD_REMOTE') + logHeader('FORWARD_REMOTE') logInfo('connectionId', connectionId) logInfo('remotePort', remotePort) logInfo('localHost', localHost) logInfo('localPort', localPort) - + try { - const result = await sshService.forwardRemote(connectionId, remotePort, localHost, localPort) + await sshService.forwardRemote(connectionId, remotePort, localHost, localPort) logSuccess('Remote forward established') return { success: true } - } catch (error: any) { - logError(`Remote forward failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logError(`Remote forward failed: ${message}`) + return { success: false, error: message } } }) @@ -206,17 +203,18 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void { connectionId: string, localPort: number ) => { - logHeader('🧦 IPC FORWARD_DYNAMIC (SOCKS5)') + logHeader('FORWARD_DYNAMIC (SOCKS5)') logInfo('connectionId', connectionId) logInfo('localPort', localPort) - + try { const result = await sshService.forwardDynamic(connectionId, localPort) logSuccess(`SOCKS5 proxy started on port ${result.boundPort}`) return { success: true, boundPort: result.boundPort } - } catch (error: any) { - logError(`Dynamic forward failed: ${error.message}`) - return { success: false, error: error.message } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logError(`Dynamic forward failed: ${message}`) + return { success: false, error: message } } }) } diff --git a/electron/ipc/zmodem.ipc.ts b/electron/ipc/zmodem.ipc.ts index c06181f..25fa297 100644 --- a/electron/ipc/zmodem.ipc.ts +++ b/electron/ipc/zmodem.ipc.ts @@ -77,9 +77,9 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void { ) return { success: true, sessionId, fileName } - } catch (error: any) { + } catch (error: unknown) { logger.error('Zmodem send error:', error) - return { success: false, error: error.message } + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -123,9 +123,9 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void { ) return { success: true, sessionId } - } catch (error: any) { + } catch (error: unknown) { logger.error('Zmodem receive error:', error) - return { success: false, error: error.message } + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) @@ -134,8 +134,8 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void { try { zmodemService.cancelSession(sessionId) return { success: true } - } catch (error: any) { - return { success: false, error: error.message } + } catch (error: unknown) { + return { success: false, error: error instanceof Error ? error.message : String(error) } } }) } diff --git a/electron/main.ts b/electron/main.ts index f12607a..0d2c3a2 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -87,12 +87,41 @@ function createApplicationMenu(): void { dialog.showMessageBox(mainWindow!, { type: 'info', title: '关于 OpsXShell', - message: 'OpsXShell v1.0.0', + message: 'OpsXShell v2.0.0', detail: '跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠\n\nMIT License', }) }, }, { type: 'separator' }, + { + label: '设置', + accelerator: process.platform === 'darwin' ? 'Cmd+,' : 'Ctrl+,', + click: () => { + mainWindow?.webContents.send('menu-open-settings') + }, + }, + { type: 'separator' }, + { + label: '服务', + role: 'services', + submenu: [], + }, + { type: 'separator' }, + { + label: `隐藏 ${app.name}`, + accelerator: 'Cmd+H', + role: 'hide', + }, + { + label: '隐藏其他', + accelerator: 'Cmd+Alt+H', + role: 'hideOthers', + }, + { + label: '全部显示', + role: 'unhide', + }, + { type: 'separator' }, { label: '退出', accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', diff --git a/electron/preload.ts b/electron/preload.ts index 0129231..9fb3b24 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,32 +1,64 @@ -const { contextBridge, ipcRenderer } = require('electron') +import { contextBridge, ipcRenderer } from 'electron' +import type { + SSHConnectRequest, + SSHConnectResponse, + SSHCreateShellRequest, + SessionCreateResponse, + SessionUpdateResponse, + DialogOptions, + DialogResult, + ZmodemProgress, + ZmodemComplete, +} from '../shared/types/ipc.types' +import type { Session } from '../shared/types/session.types' // 暴露 API 到渲染进程 const api = { // SSH 相关 - sshConnect: (config: any) => ipcRenderer.invoke('ssh:connect', config), - sshDisconnect: (connectionId: string) => ipcRenderer.invoke('ssh:disconnect', connectionId), - sshExec: (connectionId: string, command: string) => ipcRenderer.invoke('ssh:exec', connectionId, command), - sshCreateShell: (connectionId: string, options?: any, shellId?: string) => ipcRenderer.invoke('ssh:createShell', connectionId, options, shellId), - sshShellWrite: (shellId: string, data: string | Uint8Array) => ipcRenderer.invoke('ssh:shellWrite', shellId, data), - sshShellResize: (shellId: string, cols: number, rows: number) => ipcRenderer.invoke('ssh:shellResize', shellId, cols, rows), + sshConnect: (config: SSHConnectRequest): Promise => + ipcRenderer.invoke('ssh:connect', config), + sshDisconnect: (connectionId: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('ssh:disconnect', connectionId), + sshExec: (connectionId: string, command: string): Promise<{ success: boolean; result?: string; error?: string }> => + ipcRenderer.invoke('ssh:exec', connectionId, command), + sshCreateShell: (connectionId: string, options?: SSHCreateShellRequest, shellId?: string): Promise<{ success: boolean; shellId?: string; error?: string }> => + ipcRenderer.invoke('ssh:createShell', connectionId, options, shellId), + sshShellWrite: (shellId: string, data: string | Uint8Array): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('ssh:shellWrite', shellId, data), + sshShellResize: (shellId: string, cols: number, rows: number): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('ssh:shellResize', shellId, cols, rows), // 会话管理 - sessionGetAll: () => ipcRenderer.invoke('session:getAll'), - sessionGetById: (id: string) => ipcRenderer.invoke('session:getById', id), - sessionCreate: (session: any) => ipcRenderer.invoke('session:create', session), - sessionUpdate: (id: string, updates: any) => ipcRenderer.invoke('session:update', id, updates), - sessionDelete: (id: string) => ipcRenderer.invoke('session:delete', id), + sessionGetAll: (): Promise<{ success: boolean; sessions?: Session[]; error?: string }> => + ipcRenderer.invoke('session:getAll'), + sessionGetById: (id: string): Promise<{ success: boolean; session?: Session; error?: string }> => + ipcRenderer.invoke('session:getById', id), + sessionCreate: (session: Omit): Promise => + ipcRenderer.invoke('session:create', session), + sessionUpdate: (id: string, updates: Partial): Promise => + ipcRenderer.invoke('session:update', id, updates), + sessionDelete: (id: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('session:delete', id), // SFTP - sftpConnect: (connectionId: string) => ipcRenderer.invoke('sftp:connect', connectionId), - sftpListDirectory: (sftpId: string, path: string) => ipcRenderer.invoke('sftp:listDirectory', sftpId, path), - sftpUpload: (sftpId: string, localPath: string, remotePath: string) => ipcRenderer.invoke('sftp:upload', sftpId, localPath, remotePath), - sftpDownload: (sftpId: string, remotePath: string, localPath: string) => ipcRenderer.invoke('sftp:download', sftpId, remotePath, localPath), - sftpRename: (sftpId: string, oldPath: string, newPath: string) => ipcRenderer.invoke('sftp:rename', sftpId, oldPath, newPath), - sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => ipcRenderer.invoke('sftp:delete', sftpId, path, isDirectory), - sftpMkdir: (sftpId: string, path: string) => ipcRenderer.invoke('sftp:mkdir', sftpId, path), - sftpStat: (sftpId: string, filePath: string) => ipcRenderer.invoke('sftp:stat', sftpId, filePath), - sftpChmod: (sftpId: string, filePath: string, mode: number) => ipcRenderer.invoke('sftp:chmod', sftpId, filePath, mode), + sftpConnect: (connectionId: string): Promise<{ success: boolean; sftpId?: string; error?: string }> => + ipcRenderer.invoke('sftp:connect', connectionId), + sftpListDirectory: (sftpId: string, path: string): Promise<{ success: boolean; files?: unknown[]; error?: string }> => + ipcRenderer.invoke('sftp:listDirectory', sftpId, path), + sftpUpload: (sftpId: string, localPath: string, remotePath: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:upload', sftpId, localPath, remotePath), + sftpDownload: (sftpId: string, remotePath: string, localPath: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:download', sftpId, remotePath, localPath), + sftpRename: (sftpId: string, oldPath: string, newPath: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:rename', sftpId, oldPath, newPath), + sftpDelete: (sftpId: string, path: string, isDirectory: boolean): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:delete', sftpId, path, isDirectory), + sftpMkdir: (sftpId: string, path: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:mkdir', sftpId, path), + sftpStat: (sftpId: string, filePath: string): Promise<{ success: boolean; stats?: unknown; error?: string }> => + ipcRenderer.invoke('sftp:stat', sftpId, filePath), + sftpChmod: (sftpId: string, filePath: string, mode: number): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('sftp:chmod', sftpId, filePath, mode), // 端口转发 sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) => @@ -37,12 +69,16 @@ const api = { ipcRenderer.invoke('ssh:forwardDynamic', connectionId, localPort), // 设置 - settingsGet: (key: string) => ipcRenderer.invoke('settings:get', key), - settingsSet: (key: string, value: any) => ipcRenderer.invoke('settings:set', key, value), + settingsGet: (key: string): Promise<{ success: boolean; value?: unknown; error?: string }> => + ipcRenderer.invoke('settings:get', key), + settingsSet: (key: string, value: unknown): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('settings:set', key, value), // 对话框 - showSaveDialog: (options: any) => ipcRenderer.invoke('dialog:showSaveDialog', options), - showOpenDialog: (options: any) => ipcRenderer.invoke('dialog:showOpenDialog', options), + showSaveDialog: (options: DialogOptions): Promise => + ipcRenderer.invoke('dialog:showSaveDialog', options), + showOpenDialog: (options: DialogOptions): Promise => + ipcRenderer.invoke('dialog:showOpenDialog', options), // 文件操作 readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath), @@ -57,22 +93,25 @@ const api = { localRename: (oldPath: string, newPath: string) => ipcRenderer.invoke('local:rename', oldPath, newPath), localMkdir: (dirPath: string) => ipcRenderer.invoke('local:mkdir', dirPath), - // Zmodem rz/sz + // Zmodem zmodemSend: (shellId: string) => ipcRenderer.invoke('zmodem:send', shellId), zmodemReceive: (shellId: string) => ipcRenderer.invoke('zmodem:receive', shellId), zmodemCancel: (sessionId: string) => ipcRenderer.invoke('zmodem:cancel', sessionId), // Zmodem events - onZmodemProgress: (callback: (event: any, sessionId: string, progress: any) => void) => { + onZmodemProgress: (callback: (event: Electron.IpcRendererEvent, sessionId: string, progress: ZmodemProgress) => void) => { ipcRenderer.on('zmodem:progress', callback) }, - onZmodemComplete: (callback: (event: any, sessionId: string, result: any) => void) => { + onZmodemComplete: (callback: (event: Electron.IpcRendererEvent, sessionId: string, result: ZmodemComplete) => void) => { ipcRenderer.on('zmodem:complete', callback) }, // 应用信息 getVersion: () => ipcRenderer.invoke('app:getVersion'), + // Window + toggleMaximize: () => ipcRenderer.invoke('window:toggleMaximize'), + // 事件监听 onMenuNewSession: (callback: () => void) => { ipcRenderer.on('menu-new-session', callback) @@ -86,13 +125,16 @@ const api = { onMenuExportSessions: (callback: () => void) => { ipcRenderer.on('menu-export-sessions', callback) }, - onShellData: (callback: (event: any, connectionId: string, shellId: string, data: Buffer) => void) => { + onMenuOpenSettings: (callback: () => void) => { + ipcRenderer.on('menu-open-settings', callback) + }, + onShellData: (callback: (event: Electron.IpcRendererEvent, connectionId: string, shellId: string, data: Buffer) => void) => { ipcRenderer.on('ssh:shellData', callback) }, - onSFTPProgress: (callback: (event: any, sftpId: string, progress: any) => void) => { + onSFTPProgress: (callback: (event: Electron.IpcRendererEvent, sftpId: string, progress: { transferred: number; total: number; percentage: number }) => void) => { ipcRenderer.on('sftp:progress', callback) }, - removeListener: (channel: string, callback: (...args: any[]) => void) => { + removeListener: (channel: string, callback: (...args: unknown[]) => void) => { ipcRenderer.removeListener(channel, callback) }, removeAllListeners: (channel: string) => { diff --git a/electron/services/crypto.ts b/electron/services/crypto.ts index dcbcd3e..46e9f7a 100644 --- a/electron/services/crypto.ts +++ b/electron/services/crypto.ts @@ -1,15 +1,31 @@ import crypto from 'crypto' +import { promisify } from 'util' const ALGORITHM = 'aes-256-gcm' const KEY_LENGTH = 32 const IV_LENGTH = 16 -const TAG_LENGTH = 16 +const _TAG_LENGTH = 16 const SALT_LENGTH = 32 -const ITERATIONS = 100000 +// OWASP 2023 建议至少 600,000 次(针对 SHA256) +const ITERATIONS = 600000 +const CRYPTO_VERSION = 'v1' -// 从密码派生密钥 -function deriveKey(password: string, salt: Buffer): Buffer { - return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha256') +// 异步 PBKDF2 避免阻塞事件循环 +const pbkdf2Async = promisify(crypto.pbkdf2) + +/** + * 从密码异步派生密钥 + * 使用异步版本避免阻塞 Electron 主进程事件循环 + */ +async function deriveKeyAsync(password: string, salt: Buffer): Promise { + return pbkdf2Async(password, salt, ITERATIONS, KEY_LENGTH, 'sha512') +} + +/** + * 从密码同步派生密钥(仅在启动等必要时使用) + */ +function deriveKeySync(password: string, salt: Buffer): Buffer { + return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha512') } // 生成随机密钥(用于主密钥) @@ -17,18 +33,26 @@ export function generateMasterKey(): string { return crypto.randomBytes(KEY_LENGTH).toString('hex') } -// 加密数据 +/** + * 加密数据 + * 格式: version:salt:iv:tag:encrypted (全部 hex) + * version 用于未来加密方案升级时的安全迁移 + */ export function encrypt(plaintext: string, password: string): string { const salt = crypto.randomBytes(SALT_LENGTH) - const key = deriveKey(password, salt) + const key = deriveKeySync(password, salt) const iv = crypto.randomBytes(IV_LENGTH) const cipher = crypto.createCipheriv(ALGORITHM, key, iv) const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) const tag = cipher.getAuthTag() - // 格式: salt:iv:tag:encrypted (全部 hex) + // 立即清零密钥内存 + key.fill(0) + + // 格式: version:salt:iv:tag:encrypted (全部 hex) return [ + CRYPTO_VERSION, salt.toString('hex'), iv.toString('hex'), tag.toString('hex'), @@ -36,10 +60,50 @@ export function encrypt(plaintext: string, password: string): string { ].join(':') } -// 解密数据 +/** + * 异步加密(推荐,不阻塞事件循环) + */ +export async function encryptAsync(plaintext: string, password: string): Promise { + const salt = crypto.randomBytes(SALT_LENGTH) + const key = await deriveKeyAsync(password, salt) + const iv = crypto.randomBytes(IV_LENGTH) + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv) + const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) + const tag = cipher.getAuthTag() + + // 立即清零密钥内存 + key.fill(0) + + return [ + CRYPTO_VERSION, + salt.toString('hex'), + iv.toString('hex'), + tag.toString('hex'), + encrypted.toString('hex'), + ].join(':') +} + +/** + * 解密数据 + * 支持带版本标识的新格式和不带版本标识的旧格式(向后兼容) + */ export function decrypt(ciphertext: string, password: string): string { try { - const [saltHex, ivHex, tagHex, encryptedHex] = ciphertext.split(':') + const parts = ciphertext.split(':') + + // 判断格式:新格式有5部分(含version),旧格式有4部分 + let saltHex: string, ivHex: string, tagHex: string, encryptedHex: string + + if (parts.length === 5) { + // 新格式: version:salt:iv:tag:encrypted + [, saltHex, ivHex, tagHex, encryptedHex] = parts + } else if (parts.length === 4) { + // 旧格式: salt:iv:tag:encrypted(向后兼容) + [saltHex, ivHex, tagHex, encryptedHex] = parts + } else { + throw new Error('Invalid ciphertext format') + } if (!saltHex || !ivHex || !tagHex || !encryptedHex) { throw new Error('Invalid ciphertext format') @@ -50,22 +114,32 @@ export function decrypt(ciphertext: string, password: string): string { const tag = Buffer.from(tagHex, 'hex') const encrypted = Buffer.from(encryptedHex, 'hex') - const key = deriveKey(password, salt) + const key = deriveKeySync(password, salt) const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) decipher.setAuthTag(tag) const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]) + // 清零密钥内存 + key.fill(0) return decrypted.toString('utf8') } catch (error) { throw new Error('Decryption failed: ' + (error as Error).message) } } -// 简单加密(不带 salt,用于可逆加密) +/** + * 简单加密(使用主密钥,不经过 PBKDF2) + * 用于内部数据加密,密钥长度必须正确 + */ export function simpleEncrypt(plaintext: string, key: string): string { const iv = crypto.randomBytes(IV_LENGTH) - const keyBuffer = Buffer.from(key, 'hex').slice(0, KEY_LENGTH) + const keyBuffer = Buffer.from(key, 'hex') + + // 验证密钥长度 + if (keyBuffer.length !== KEY_LENGTH) { + throw new Error(`Invalid key length: expected ${KEY_LENGTH} bytes, got ${keyBuffer.length}`) + } const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv) const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) @@ -74,15 +148,26 @@ export function simpleEncrypt(plaintext: string, key: string): string { return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted.toString('hex') } -// 简单解密 +/** + * 简单解密 + */ export function simpleDecrypt(ciphertext: string, key: string): string { try { const [ivHex, tagHex, encryptedHex] = ciphertext.split(':') + if (!ivHex || !tagHex || !encryptedHex) { + throw new Error('Invalid ciphertext format') + } + const iv = Buffer.from(ivHex, 'hex') const tag = Buffer.from(tagHex, 'hex') const encrypted = Buffer.from(encryptedHex, 'hex') - const keyBuffer = Buffer.from(key, 'hex').slice(0, KEY_LENGTH) + const keyBuffer = Buffer.from(key, 'hex') + + // 验证密钥长度 + if (keyBuffer.length !== KEY_LENGTH) { + throw new Error(`Invalid key length: expected ${KEY_LENGTH} bytes, got ${keyBuffer.length}`) + } const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv) decipher.setAuthTag(tag) diff --git a/electron/services/database-service.ts b/electron/services/database-service.ts index 5d9aee7..d36d698 100644 --- a/electron/services/database-service.ts +++ b/electron/services/database-service.ts @@ -5,13 +5,14 @@ import fs from 'fs' import { simpleEncrypt, simpleDecrypt } from './crypto' import { keyService } from './key-service' import { createLogger } from '../utils/logger' +import type { Session, SessionCreateRequest, SessionUpdateRequest } from '../../shared/types/session.types' const logger = createLogger('DatabaseService') // sql.js statement wrapper to match better-sqlite3 API interface StatementResult { id: number - [key: string]: any + [key: string]: unknown } class PreparedStatement { @@ -23,16 +24,18 @@ class PreparedStatement { this.sql = sql } - run(...params: any[]): StatementResult { - this.db.run(this.sql, params as any) - return { id: 0 } as StatementResult + run(...params: unknown[]): StatementResult { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.db.run(this.sql, params as any[]) + return { id: 0 } } - get(...params: any[]): any { + get(...params: unknown[]): unknown { // 使用 exec 模式获取单行 const stmt = this.db.prepare(this.sql) - stmt.bind(params as any) - let result: any = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stmt.bind(params as any[]) + let result: unknown = null if (stmt.step()) { result = stmt.getAsObject() } @@ -40,10 +43,11 @@ class PreparedStatement { return result } - all(...params: any[]): any[] { + all(...params: unknown[]): unknown[] { const stmt = this.db.prepare(this.sql) - stmt.bind(params as any) - const results: any[] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stmt.bind(params as any[]) + const results: unknown[] = [] while (stmt.step()) { results.push(stmt.getAsObject()) } @@ -200,7 +204,6 @@ export class DatabaseService { } try { const decrypted = simpleDecrypt(value, this.masterKey) - logger.info('Decrypted field length:', decrypted?.length || 0) return decrypted } catch (error) { logger.error('Decrypt failed:', error) @@ -209,10 +212,10 @@ export class DatabaseService { } // 会话 CRUD 操作 - createSession(session: any): any { + createSession(session: SessionCreateRequest): Session | null { if (!this.db) throw new Error('Database not initialized') - logger.info('Creating session with password:', session.password ? `len=${session.password.length}` : 'null/undefined') + logger.info('Creating session:', session.name, session.host) const stmt = this.prepare(` INSERT INTO sessions (id, name, host, port, username, auth_type, password, private_key_path, passphrase_encrypted, group_name, tags) @@ -221,7 +224,6 @@ export class DatabaseService { const id = `session-${Date.now()}` const encryptedPassword = this.encryptField(session.password) - logger.info('Encrypted password:', encryptedPassword ? `len=${encryptedPassword.length}` : 'null') stmt.run( id, @@ -241,56 +243,56 @@ export class DatabaseService { return this.getSessionById(id) } - getAllSessions(): any[] { + getAllSessions(): Session[] { if (!this.db) throw new Error('Database not initialized') const stmt = this.prepare('SELECT * FROM sessions ORDER BY created_at DESC') - const sessions = stmt.all() + const sessions = stmt.all() as Record[] - return sessions.map((s: any) => ({ - id: s.id, - name: s.name, - host: s.host, - port: s.port, - username: s.username, - authType: s.auth_type, - password: this.decryptField(s.password), - privateKey: s.private_key_path, - passphrase: this.decryptField(s.passphrase_encrypted), - group: s.group_name, - tags: s.tags ? JSON.parse(s.tags) : [], + return sessions.map((s) => ({ + id: s.id as string, + name: s.name as string, + host: s.host as string, + port: s.port as number, + username: s.username as string, + authType: s.auth_type as Session['authType'], + password: this.decryptField(s.password as string | null) || undefined, + privateKey: s.private_key_path as string | undefined, + passphrase: this.decryptField(s.passphrase_encrypted as string | null) || undefined, + group: s.group_name as string | undefined, + tags: s.tags ? JSON.parse(s.tags as string) as string[] : [], })) } - getSessionById(id: string): any { + getSessionById(id: string): Session | null { if (!this.db) throw new Error('Database not initialized') const stmt = this.prepare('SELECT * FROM sessions WHERE id = ?') - const session = stmt.get(id) as any + const session = stmt.get(id) as Record | undefined if (session) { return { - id: session.id, - name: session.name, - host: session.host, - port: session.port, - username: session.username, - authType: session.auth_type, - password: this.decryptField(session.password), - privateKey: session.private_key_path, - passphrase: this.decryptField(session.passphrase_encrypted), - group: session.group_name, - tags: session.tags ? JSON.parse(session.tags) : [], + id: session.id as string, + name: session.name as string, + host: session.host as string, + port: session.port as number, + username: session.username as string, + authType: session.auth_type as Session['authType'], + password: this.decryptField(session.password as string | null) || undefined, + privateKey: session.private_key_path as string | undefined, + passphrase: this.decryptField(session.passphrase_encrypted as string | null) || undefined, + group: session.group_name as string | undefined, + tags: session.tags ? JSON.parse(session.tags as string) as string[] : [], } } return null } - updateSession(id: string, updates: any): any { + updateSession(id: string, updates: SessionUpdateRequest): Session | null { if (!this.db) throw new Error('Database not initialized') const fields: string[] = [] - const values: any[] = [] + const values: unknown[] = [] if (updates.name !== undefined) { fields.push('name = ?') @@ -320,6 +322,10 @@ export class DatabaseService { fields.push('passphrase_encrypted = ?') values.push(this.encryptField(updates.passphrase)) } + if (updates.privateKey !== undefined) { + fields.push('private_key_path = ?') + values.push(updates.privateKey || null) + } if (updates.group !== undefined) { fields.push('group_name = ?') values.push(updates.group) @@ -350,15 +356,15 @@ export class DatabaseService { } // 设置操作 - getSetting(key: string): any { + getSetting(key: string): unknown { if (!this.db) throw new Error('Database not initialized') const stmt = this.prepare('SELECT value FROM settings WHERE key = ?') - const row: any = stmt.get(key) - return row ? JSON.parse(row.value) : null + const row = stmt.get(key) as Record | undefined + return row ? JSON.parse(row.value as string) : null } - setSetting(key: string, value: any): void { + setSetting(key: string, value: unknown): void { if (!this.db) throw new Error('Database not initialized') const stmt = this.prepare(` diff --git a/electron/services/key-service.ts b/electron/services/key-service.ts index 3a996e7..d681f0d 100644 --- a/electron/services/key-service.ts +++ b/electron/services/key-service.ts @@ -1,4 +1,4 @@ -import { app } from 'electron' +import { app, safeStorage } from 'electron' import path from 'path' import fs from 'fs' import crypto from 'crypto' @@ -6,33 +6,81 @@ import { createLogger } from '../utils/logger' const logger = createLogger('KeyService') -const KEY_FILE = 'encryption.key' +const KEY_FILE = 'encryption.key.enc' +const LEGACY_KEY_FILE = 'encryption.key' export class KeyService { private key: string | null = null - // 获取或创建主密钥 + /** + * 获取或创建主密钥 + * 使用 Electron safeStorage 加密存储,替代明文存储 + */ getMasterKey(): string { if (this.key) return this.key - const keyPath = path.join(app.getPath('userData'), KEY_FILE) + const userDataPath = app.getPath('userData') + const keyPath = path.join(userDataPath, KEY_FILE) + const legacyKeyPath = path.join(userDataPath, LEGACY_KEY_FILE) - // 尝试读取现有密钥 + // 1. 尝试读取新的加密格式密钥 if (fs.existsSync(keyPath)) { try { - this.key = fs.readFileSync(keyPath, 'utf8').trim() + const encrypted = fs.readFileSync(keyPath) + this.key = safeStorage.decryptString(encrypted) + logger.info('Master key loaded from encrypted storage') return this.key } catch (error) { - logger.error('Failed to read master key:', error) + logger.error('Failed to decrypt master key, generating new one:', error) } } - // 生成新密钥 + // 2. 迁移旧的明文密钥(向后兼容) + if (fs.existsSync(legacyKeyPath)) { + try { + this.key = fs.readFileSync(legacyKeyPath, 'utf8').trim() + // 迁移到加密存储 + this.saveEncryptedKey(this.key) + // 安全删除旧文件(覆盖后删除) + this.secureDelete(legacyKeyPath) + logger.info('Migrated master key from plaintext to encrypted storage') + return this.key + } catch (error) { + logger.error('Failed to migrate legacy key:', error) + } + } + + // 3. 生成新密钥 this.key = crypto.randomBytes(32).toString('hex') - fs.writeFileSync(keyPath, this.key, { mode: 0o600 }) + this.saveEncryptedKey(this.key) + logger.info('Generated new master key with encrypted storage') return this.key } + // 使用 safeStorage 加密保存密钥 + private saveEncryptedKey(key: string): void { + const keyPath = path.join(app.getPath('userData'), KEY_FILE) + const encrypted = safeStorage.encryptString(key) + fs.writeFileSync(keyPath, encrypted, { mode: 0o600 }) + } + + // 安全删除文件(先覆盖再删除) + private secureDelete(filePath: string): void { + try { + const stats = fs.statSync(filePath) + const size = stats.size + // 用随机数据覆盖 + const randomData = crypto.randomBytes(size) + fs.writeFileSync(filePath, randomData) + // 再用零覆盖 + fs.writeFileSync(filePath, Buffer.alloc(size, 0)) + // 最后删除 + fs.unlinkSync(filePath) + } catch (error) { + logger.error('Failed to securely delete file:', error) + } + } + // 清除密钥(登出时) clearKey(): void { this.key = null diff --git a/electron/services/ssh-service.ts b/electron/services/ssh-service.ts index ef59def..b6cba5a 100644 --- a/electron/services/ssh-service.ts +++ b/electron/services/ssh-service.ts @@ -1,30 +1,32 @@ -import { Client, ClientChannel } from 'ssh2' -import { SFTPWrapper } from 'ssh2' +import { Client, ClientChannel, SFTPWrapper, FileEntryWithStats } from 'ssh2' import { v4 as uuidv4 } from 'uuid' import { createLogger } from '../utils/logger' import * as net from 'net' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' const logger = createLogger('SSHService') -// ===== 彩色调试日志 ===== -const DEBUG = true -function debug(...args: any[]) { - if (DEBUG) console.log('[SSHService]', ...args) +// 生产环境日志:使用结构化日志,不输出敏感信息 +const DEBUG = process.env.NODE_ENV === 'development' +function debug(...args: unknown[]) { + if (DEBUG) logger.debug('[SSHService]', ...args) } function logHeader(msg: string) { - console.log(`%c${msg}`, 'background: #e74c3c; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;') + logger.info(`[${msg}]`) } -function logInfo(label: string, value: any) { - console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #3498db; font-weight: bold;') +function logInfo(label: string, value: unknown) { + logger.info(`${label}: ${value}`) } function logSuccess(msg: string) { - console.log(`%c✓ ${msg}`, 'color: #27ae60; font-weight: bold;') + logger.info(`[OK] ${msg}`) } function logError(msg: string) { - console.log(`%c✗ ${msg}`, 'color: #e74c3c; font-weight: bold;') + logger.error(msg) } -function logWarning(msg: string) { - console.log(`%c⚠ ${msg}`, 'color: #f39c12; font-weight: bold;') +function _logWarning(msg: string) { + logger.warn(msg) } export interface SSHConfig { @@ -63,11 +65,34 @@ export class SSHService { private shells: Map = new Map() private sftps: Map = new Map() + private resolvePrivateKey(privateKey?: string): string | Buffer | undefined { + if (!privateKey) return undefined + + // 如果已经是 PEM 格式内容,直接返回 + if (privateKey.includes('-----BEGIN')) { + return privateKey + } + + // 展开 ~ 为用户主目录 + let keyPath = privateKey + if (keyPath.startsWith('~')) { + keyPath = path.join(os.homedir(), keyPath.slice(1)) + } + + // 当作文件路径读取 + try { + const content = fs.readFileSync(keyPath) + debug('>>> Private key loaded from:', keyPath) + return content + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + logger.error(`Failed to read private key file: ${keyPath} - ${msg}`) + throw new Error(`私钥文件读取失败: ${keyPath}`) + } + } + async connect(config: SSHConfig): Promise { - console.log('=== SSHService connect ===') - console.log(' host:', config.host) - console.log(' port:', config.port || 22) - console.log(' username:', config.username) + debug('SSHService connect', config.host, config.port || 22, config.username) return new Promise((resolve, reject) => { const client = new Client() @@ -105,13 +130,24 @@ export class SSHService { this.connections.delete(connectionId) }) + // 解析私钥(路径 → 内容) + let resolvedPrivateKey: string | Buffer | undefined + try { + resolvedPrivateKey = this.resolvePrivateKey(config.privateKey) + } catch (err) { + connection.status = 'error' + this.connections.delete(connectionId) + reject(err) + return + } + debug('>>> Starting SSH client.connect...') client.connect({ host: config.host, port: config.port || 22, username: config.username, password: config.password, - privateKey: config.privateKey, + privateKey: resolvedPrivateKey, passphrase: config.passphrase, agent: config.agent, readyTimeout: config.readyTimeout || 20000, @@ -190,14 +226,10 @@ export class SSHService { onData?: (shellId: string, data: Buffer) => void, preferredShellId?: string ): Promise { - console.log('=== SSHService createShell ===') - console.log(' connectionId:', connectionId) - console.log(' options:', options) - console.log(' preferredShellId:', preferredShellId) + debug('SSHService createShell', connectionId, options, preferredShellId) const connection = this.connections.get(connectionId) - console.log(' connection found:', !!connection) - console.log(' connection status:', connection?.status) + debug('connection found:', !!connection, 'status:', connection?.status) if (!connection || connection.status !== 'connected') { throw new Error('Connection not established') @@ -236,19 +268,8 @@ export class SSHService { if (onData) { debug('>>> Setting up channel.on(data) handler') channel.on('data', (data: Buffer) => { - // Log first few chunks for debugging - const text = data.toString('utf-8') - console.log('=== SSHService channel.on(data) ===') - console.log(' shellId:', shellId) - console.log(' data length:', data.length) - console.log(' data preview:', text.substring(0, 100).replace(/\n/g, '\\n')) - - if (text.includes('Last login') || text.includes('Welcome')) { - logger.info('[SSHService] Shell data (first 200 chars):', text.substring(0, 200)) - } - debug('>>> Calling onData callback...') + debug('SSHService channel.on(data) shellId:', shellId, 'data length:', data.length) onData(shellId, data) - debug('>>> onData callback returned') }) } else { debug('>>> WARNING: onData callback is undefined!') @@ -321,7 +342,7 @@ export class SSHService { }) } - async sftpListDirectory(sftpId: string, path: string): Promise { + async sftpListDirectory(sftpId: string, path: string): Promise { logHeader('📁 SFTP LIST DIRECTORY') logInfo('sftpId', sftpId) logInfo('path', path) diff --git a/electron/services/zmodem-service.ts b/electron/services/zmodem-service.ts index c10af83..cd26c0e 100644 --- a/electron/services/zmodem-service.ts +++ b/electron/services/zmodem-service.ts @@ -10,17 +10,17 @@ const logger = createLogger('ZmodemService') interface ZmodemSendSession { type: 'send' set_sender(func: (octets: number[]) => void): void - send_offer(params: { name: string; size: number; mtime?: number }): Promise - on(event: string, handler: (data: any) => void): void + send_offer(params: { name: string; size: number; mtime?: number }): Promise + on(event: string, handler: (data: unknown) => void): void abort(): void - close(): Promise + close(): Promise } interface ZmodemReceiveSession { type: 'receive' set_sender(func: (octets: number[]) => void): void - start(): Promise - on(event: string, handler: (data: any) => void): void + start(): Promise + on(event: string, handler: (data: unknown) => void): void abort(): void } @@ -28,6 +28,20 @@ interface ZmodemSentry { consume(octets: number[]): void } +interface ZmodemDetection { + get_session(): { type: string } & ZmodemSendSession +} + +interface ZmodemOffer { + getDetails(): { name: string; size: number } + accept(options: { on_input: string }): Promise +} + +interface ZmodemTransfer { + on(event: 'input', handler: (data: Uint8Array) => void): void + on(event: 'complete', handler: () => void): void +} + export interface ZmodemProgress { percentage: number transferred: number @@ -80,7 +94,7 @@ export class ZmodemService { id: sessionId, shellId, type: 'send', - session: null as any, + session: null as unknown as ZmodemSendSession, fileName, totalSize, transferred: 0 @@ -94,13 +108,12 @@ export class ZmodemService { } // Create Sentry to detect ZRINIT from server - // @ts-ignore - zmodem-ts types are incomplete const sentry = new Zmodem.Sentry({ to_terminal: (octets: number[]) => { // Echo to terminal writeFunc(new Uint8Array(octets)) }, - on_detect: (detection: any) => { + on_detect: (detection: ZmodemDetection) => { const detectedSession = detection.get_session() if (detectedSession && detectedSession.type === 'send') { // Server is ready for file transfer (received ZRINIT) @@ -119,9 +132,10 @@ export class ZmodemService { this.sessions.delete(sessionId) }) - sendSession.on('error', (err: Error) => { - logger.error('Zmodem send error:', err) - onComplete?.(false, err.message) + sendSession.on('error', (err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)) + logger.error('Zmodem send error:', error) + onComplete?.(false, error.message) }) // Send the file @@ -181,9 +195,9 @@ export class ZmodemService { // Transfer was skipped or rejected onComplete?.(false, 'Transfer skipped or rejected') } - } catch (err: any) { + } catch (err: unknown) { logger.error('Zmodem send error:', err) - onComplete?.(false, err.message) + onComplete?.(false, err instanceof Error ? err.message : String(err)) } } @@ -198,7 +212,7 @@ export class ZmodemService { } // Also check all sessions for matching shellId - for (const [sessionId, session] of this.sessions) { + for (const [, session] of this.sessions) { if (session.shellId === shellId && session.sentry) { // Feed to sentry for detection } @@ -222,7 +236,7 @@ export class ZmodemService { const fileName = path.basename(filePath) // Create receive session - // @ts-ignore - zmodem-ts types are incomplete + // @ts-expect-error - zmodem-ts types are incomplete const session: ZmodemReceiveSession = new Zmodem.ReceiveSession() const sessionInfo: ZmodemSessionInfo = { @@ -248,17 +262,18 @@ export class ZmodemService { }) // Handle file offer - session.on('offer', (offer: any) => { - const details = offer.getDetails() + session.on('offer', (offer: unknown) => { + const zmodemOffer = offer as ZmodemOffer + const details = zmodemOffer.getDetails() logger.info('Zmodem offer received:', details) sessionInfo.fileName = details.name || fileName sessionInfo.totalSize = details.size || 0 // Accept the offer - offer.accept({ + zmodemOffer.accept({ on_input: 'spool_array' - }).then((transfer: any) => { + }).then((transfer) => { // Handle incoming data transfer.on('input', (data: Uint8Array) => { sessionInfo.transferred = (sessionInfo.transferred || 0) + data.length @@ -266,7 +281,7 @@ export class ZmodemService { // Write to file try { fs.writeFileSync(filePath, data, { flag: 'a' }) - } catch (err: any) { + } catch (err: unknown) { logger.error('Error writing Zmodem file:', err) } @@ -302,9 +317,10 @@ export class ZmodemService { this.sessions.delete(sessionId) }) - session.on('error', (err: Error) => { - logger.error('Zmodem receive error:', err) - onComplete?.(false, err.message) + session.on('error', (err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)) + logger.error('Zmodem receive error:', error) + onComplete?.(false, error.message) }) // Start receiving - this sends ZRINIT to signal we're ready @@ -324,10 +340,10 @@ export class ZmodemService { const session = this.sessions.get(sessionId) if (session) { try { - const s = session.session as any + const s = session.session as { abort?: () => void; close?: () => void } s.abort?.() s.close?.() - } catch (err: any) { + } catch (err: unknown) { logger.error('Error canceling Zmodem session:', err) } diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 4aa9f7e..d6b46ff 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -4,7 +4,6 @@ "module": "commonjs", "lib": ["ES2020"], "outDir": "../dist-electron", - "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/index.html b/index.html index 86da00b..c1ace4d 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,15 @@ - + - + + OpsXShell · 跨平台 SSH/SFTP 终端工具 + diff --git a/package-lock.json b/package-lock.json index 7945b12..52bf9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,33 @@ { "name": "opsxshell-gui", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opsxshell-gui", - "version": "1.0.0", + "version": "2.0.0", "license": "MIT", "dependencies": { + "@vitest/coverage-v8": "^4.1.4", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.16.0", "@xterm/xterm": "^5.5.0", + "concurrently": "^9.2.1", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "lucide-react": "^0.294.0", + "prettier": "^3.8.3", + "prettier-plugin-tailwindcss": "^0.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.0", + "rimraf": "^6.1.3", "socksv5": "^0.0.6", "sql.js": "^1.11.0", "ssh2": "^1.15.0", "uuid": "^9.0.1", + "wait-on": "^9.0.5", "zmodem-ts": "^1.0.8", "zustand": "^4.4.7" }, @@ -76,7 +84,7 @@ "version": "5.1.11", "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -93,7 +101,7 @@ "version": "7.1.0", "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.0.tgz", "integrity": "sha512-ASf825+5vsGuYWoyFyNsex2mNtPTXpCvYTR942+w/eNw7PqS0Lhl/PE1hC7bajneI3m/Oxi+yrP3vTOPxfwM8A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -110,7 +118,7 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -120,7 +128,7 @@ "version": "2.3.9", "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@babel/code-frame": { @@ -289,7 +297,6 @@ "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -299,7 +306,6 @@ "version": "7.28.5", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -333,7 +339,6 @@ "version": "7.29.2", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -425,7 +430,6 @@ "version": "7.29.0", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -435,11 +439,20 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "css-tree": "^3.0.0" @@ -452,7 +465,7 @@ "version": "6.0.2", "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -472,7 +485,7 @@ "version": "3.2.0", "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.0.tgz", "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -496,7 +509,7 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -524,7 +537,7 @@ "version": "4.0.0", "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -547,7 +560,7 @@ "version": "1.1.3", "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -572,7 +585,7 @@ "version": "4.0.0", "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -893,7 +906,6 @@ "version": "1.9.2", "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -905,7 +917,6 @@ "version": "1.9.2", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -916,7 +927,6 @@ "version": "1.2.1", "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1405,7 +1415,7 @@ "version": "1.15.0", "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.0.tgz", "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -1419,6 +1429,54 @@ } } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1610,7 +1668,6 @@ "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1620,14 +1677,12 @@ "version": "1.5.5", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1716,7 +1771,6 @@ "version": "1.1.4", "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1773,7 +1827,6 @@ "version": "0.124.0", "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.124.0.tgz", "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -1806,7 +1859,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1823,7 +1875,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1840,7 +1891,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1857,7 +1907,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1874,7 +1923,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1891,7 +1939,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1908,7 +1955,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1925,7 +1971,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1942,7 +1987,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1959,7 +2003,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1976,7 +2019,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1993,7 +2035,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2010,7 +2051,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2029,7 +2069,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2046,7 +2085,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2430,7 +2468,6 @@ "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, "license": "MIT" }, "node_modules/@szmarczak/http-timer": { @@ -2550,7 +2587,6 @@ "version": "0.10.1", "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2627,7 +2663,6 @@ "version": "5.2.3", "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -2648,7 +2683,6 @@ "version": "4.0.2", "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, "license": "MIT" }, "node_modules/@types/emscripten": { @@ -2662,7 +2696,6 @@ "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/fs-extra": { @@ -2710,7 +2743,7 @@ "version": "25.6.0", "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.19.0" @@ -3063,11 +3096,40 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", + "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.4", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.4", + "vitest": "4.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.4.tgz", "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", - "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -3085,7 +3147,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", - "dev": true, "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -3098,7 +3159,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.4.tgz", "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", - "dev": true, "license": "MIT", "dependencies": { "@vitest/utils": "4.1.4", @@ -3112,7 +3172,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.4.tgz", "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", - "dev": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.4", @@ -3128,7 +3187,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.4.tgz", "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" @@ -3138,7 +3196,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.4.tgz", "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", - "dev": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.4", @@ -3257,11 +3314,25 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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, "license": "MIT", "engines": { "node": ">=8" @@ -3271,7 +3342,6 @@ "version": "4.3.0", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3542,12 +3612,28 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3580,7 +3666,6 @@ "version": "0.4.0", "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -3630,6 +3715,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3683,7 +3779,7 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -3972,7 +4068,6 @@ "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, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4027,7 +4122,6 @@ "version": "6.2.2", "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4037,7 +4131,6 @@ "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4132,6 +4225,21 @@ "node": ">=0.2.5" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "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", @@ -4167,7 +4275,6 @@ "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, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4195,7 +4302,6 @@ "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, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4208,7 +4314,12 @@ "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, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, "node_modules/colors": { @@ -4224,7 +4335,6 @@ "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4276,6 +4386,45 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/config-file-ts": { "version": "0.2.6", "resolved": "https://registry.npmmirror.com/config-file-ts/-/config-file-ts-0.2.6.tgz", @@ -4338,7 +4487,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/core-util-is": { @@ -4419,7 +4567,7 @@ "version": "3.2.1", "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mdn-data": "2.27.1", @@ -4468,7 +4616,7 @@ "version": "7.0.0", "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", @@ -4500,7 +4648,7 @@ "version": "10.6.0", "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -4591,7 +4739,6 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4611,7 +4758,6 @@ "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4812,7 +4958,6 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5067,7 +5212,6 @@ "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, "license": "MIT" }, "node_modules/end-of-stream": { @@ -5084,7 +5228,7 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5103,6 +5247,18 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", @@ -5114,7 +5270,6 @@ "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, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5124,7 +5279,6 @@ "version": "1.3.0", "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5134,14 +5288,12 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5154,7 +5306,6 @@ "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, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5217,7 +5368,6 @@ "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5428,7 +5578,6 @@ "version": "3.0.3", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -5444,11 +5593,16 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -5646,6 +5800,23 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/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, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", @@ -5653,6 +5824,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", @@ -5674,7 +5865,6 @@ "version": "4.0.5", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5767,7 +5957,6 @@ "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5782,7 +5971,6 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5802,17 +5990,27 @@ "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, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5837,7 +6035,6 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5997,7 +6194,6 @@ "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6050,7 +6246,6 @@ "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6074,7 +6269,6 @@ "version": "1.1.0", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6087,7 +6281,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6103,7 +6296,6 @@ "version": "2.0.3", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6149,7 +6341,7 @@ "version": "6.0.0", "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.6.0" @@ -6158,6 +6350,12 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -6208,6 +6406,21 @@ "node": ">= 6" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -6380,7 +6593,6 @@ "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, "license": "MIT", "engines": { "node": ">=8" @@ -6423,7 +6635,7 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/isarray": { @@ -6460,6 +6672,42 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "license": "MIT" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", @@ -6498,12 +6746,30 @@ "version": "1.21.7", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "18.1.2", + "resolved": "https://registry.npmmirror.com/joi/-/joi-18.1.2.tgz", + "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6527,7 +6793,7 @@ "version": "29.0.2", "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.0.2.tgz", "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^5.1.5", @@ -6568,7 +6834,7 @@ "version": "11.3.5", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.5.tgz", "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -6724,7 +6990,6 @@ "version": "1.32.0", "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6757,7 +7022,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6778,7 +7042,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6799,7 +7062,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6820,7 +7082,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6841,7 +7102,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6862,7 +7122,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6883,7 +7142,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6904,7 +7162,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6925,7 +7182,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6946,7 +7202,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6967,7 +7222,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7001,6 +7255,209 @@ "dev": true, "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -7021,7 +7478,6 @@ "version": "4.18.1", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash.defaults": { @@ -7071,6 +7527,135 @@ "license": "MIT", "peer": true }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7127,12 +7712,37 @@ "version": "0.30.21", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", @@ -7151,7 +7761,6 @@ "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7161,7 +7770,7 @@ "version": "2.27.1", "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, + "devOptional": true, "license": "CC0-1.0" }, "node_modules/merge2": { @@ -7205,7 +7814,6 @@ "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7215,7 +7823,6 @@ "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, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -7224,6 +7831,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", @@ -7264,7 +7883,6 @@ "version": "1.2.8", "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7357,7 +7975,6 @@ "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -7452,7 +8069,6 @@ "version": "2.1.1", "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.1.tgz", "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -7468,6 +8084,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -7532,7 +8163,6 @@ "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, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -7552,7 +8182,7 @@ "version": "8.0.0", "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.0.tgz", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -7635,7 +8265,6 @@ "version": "2.0.3", "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/pend": { @@ -7649,7 +8278,6 @@ "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7713,7 +8341,6 @@ "version": "8.5.10", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.10.tgz", "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7882,6 +8509,99 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz", + "integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", @@ -7944,6 +8664,15 @@ "node": ">=10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", @@ -7959,7 +8688,7 @@ "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -8174,7 +8903,6 @@ "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, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8184,7 +8912,7 @@ "version": "2.0.2", "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8242,6 +8970,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", @@ -8263,18 +9007,113 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "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, - "license": "ISC", + "version": "6.1.3", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8303,7 +9142,6 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.124.0", @@ -8337,7 +9175,6 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", - "dev": true, "license": "MIT" }, "node_modules/rollup": { @@ -8409,6 +9246,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8461,7 +9307,7 @@ "version": "6.0.0", "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -8483,7 +9329,6 @@ "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8554,18 +9399,28 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, "license": "ISC" }, "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, "license": "ISC", "engines": { "node": ">=14" @@ -8677,7 +9532,6 @@ "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8738,7 +9592,6 @@ "version": "0.0.2", "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, "license": "MIT" }, "node_modules/stat-mode": { @@ -8755,7 +9608,6 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.1.0.tgz", "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, "license": "MIT" }, "node_modules/string_decoder": { @@ -8769,11 +9621,19 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "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, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8804,7 +9664,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8903,7 +9762,6 @@ "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, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -8929,7 +9787,7 @@ "version": "3.2.4", "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tailwindcss": { @@ -9096,14 +9954,12 @@ "version": "2.9.0", "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.1.tgz", "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9113,7 +9969,6 @@ "version": "0.2.16", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -9130,7 +9985,6 @@ "version": "6.5.0", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9148,7 +10002,6 @@ "version": "4.0.4", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9161,7 +10014,6 @@ "version": "3.1.0", "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz", "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -9171,7 +10023,7 @@ "version": "7.0.28", "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.0.28.tgz", "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^7.0.28" @@ -9184,7 +10036,7 @@ "version": "7.0.28", "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.28.tgz", "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tmp": { @@ -9224,7 +10076,7 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -9237,7 +10089,7 @@ "version": "6.0.0", "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -9246,6 +10098,15 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -9280,9 +10141,7 @@ "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -9334,7 +10193,7 @@ "version": "7.25.0", "resolved": "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -9344,7 +10203,7 @@ "version": "7.19.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -9536,7 +10395,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.4.tgz", "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", - "dev": true, "license": "MIT", "dependencies": { "@vitest/expect": "4.1.4", @@ -9626,7 +10484,6 @@ "version": "4.1.4", "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.4.tgz", "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", - "dev": true, "license": "MIT", "dependencies": { "@vitest/spy": "4.1.4", @@ -9653,7 +10510,6 @@ "version": "4.0.4", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9666,7 +10522,6 @@ "version": "8.0.8", "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.8.tgz", "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", - "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -9744,7 +10599,7 @@ "version": "5.0.0", "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -9753,11 +10608,30 @@ "node": ">=18" } }, + "node_modules/wait-on": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/wait-on/-/wait-on-9.0.5.tgz", + "integrity": "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA==", + "license": "MIT", + "dependencies": { + "axios": "^1.15.0", + "joi": "^18.1.2", + "lodash": "^4.18.1", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" @@ -9767,7 +10641,7 @@ "version": "5.0.0", "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=20" @@ -9777,7 +10651,7 @@ "version": "16.0.1", "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.11.0", @@ -9808,7 +10682,6 @@ "version": "2.3.0", "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -9865,7 +10738,6 @@ "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, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9908,7 +10780,7 @@ "version": "5.0.0", "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -9928,14 +10800,13 @@ "version": "2.2.0", "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "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, "license": "ISC", "engines": { "node": ">=10" @@ -9948,11 +10819,25 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -9971,7 +10856,6 @@ "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, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index eccb935..31bc773 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,50 @@ { "name": "opsxshell-gui", - "version": "1.0.0", - "description": "跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠", + "version": "2.0.0", + "description": "跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · v2.0 安全增强版", "main": "dist-electron/main.js", "author": "OpsXShell Team", "license": "MIT", "scripts": { - "dev": "vite & sleep 2 && electron dist-electron/main.js --inspect=9222", - "build": "tsc && vite build && npm run build:electron", + "dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron dist-electron/main.js --inspect=9222\"", + "build": "tsc --noEmit && vite build && npm run build:electron", "build:electron": "tsc -p electron/tsconfig.json", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx --fix", + "format": "prettier --write \"**/*.{ts,tsx,css,json,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,css,json,md}\"", + "typecheck": "tsc --noEmit && tsc --noEmit -p electron/tsconfig.json", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", + "clean": "rimraf dist dist-electron release", + "prepare": "husky install || true", "dist": "npm run build && electron-builder", "dist:win": "npm run build && electron-builder --win", "dist:mac": "npm run build && electron-builder --mac", "dist:linux": "npm run build && electron-builder --linux" }, "dependencies": { + "@vitest/coverage-v8": "^4.1.4", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.16.0", "@xterm/xterm": "^5.5.0", + "concurrently": "^9.2.1", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "lucide-react": "^0.294.0", + "prettier": "^3.8.3", + "prettier-plugin-tailwindcss": "^0.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.0", + "rimraf": "^6.1.3", "socksv5": "^0.0.6", "sql.js": "^1.11.0", "ssh2": "^1.15.0", "uuid": "^9.0.1", + "wait-on": "^9.0.5", "zmodem-ts": "^1.0.8", "zustand": "^4.4.7" }, diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..33ad091 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,4 @@ -export default { +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, diff --git a/shared/types/ipc.types.ts b/shared/types/ipc.types.ts index 28a2dec..1bbaf19 100644 --- a/shared/types/ipc.types.ts +++ b/shared/types/ipc.types.ts @@ -34,7 +34,7 @@ export interface SSHExecResponse { } export interface SSHCreateShellRequest { - connectionId: string + connectionId?: string cols?: number rows?: number } diff --git a/shared/types/session.types.ts b/shared/types/session.types.ts index a038a94..3fc8d18 100644 --- a/shared/types/session.types.ts +++ b/shared/types/session.types.ts @@ -14,6 +14,7 @@ export interface SSHConnectionConfig { password?: string privateKey?: string passphrase?: string + authType?: AuthType } // 持久化会话 diff --git a/src/App.tsx b/src/App.tsx index d9a04f7..7b8fab8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,8 @@ -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, lazy, Suspense } from 'react' import Sidebar from './components/Sidebar' import TabBar from './components/TabBar' import TabContent from './components/TabContent' import StatusBar from './components/StatusBar' -import ConnectionPanel from './components/ConnectionPanel' -import SessionDialog from './components/SessionDialog' -import QuickConnectDialog from './components/QuickConnectDialog' -import { CommandPalette } from './components/common/CommandPalette' import { useTabStore } from './stores/tabStore' import { useSessionStore, useCommandPaletteStore, type Session } from './stores' import { createLogger } from './utils/logger' @@ -16,6 +12,12 @@ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useIsMobile } from './hooks/useMediaQuery' import { useTheme } from './hooks/useTheme' +// Lazy load heavy components that are not always visible +const ConnectionPanel = lazy(() => import('./components/ConnectionPanel')) +const SessionDialog = lazy(() => import('./components/SessionDialog')) +const QuickConnectDialog = lazy(() => import('./components/QuickConnectDialog')) +const CommandPalette = lazy(() => import('./components/common/CommandPalette').then(m => ({ default: m.CommandPalette }))) + const logger = createLogger('App') function App() { @@ -63,7 +65,7 @@ function App() { }, []) // 导出会话 - const handleExportSessions = async () => { + const handleExportSessions = useCallback(async () => { try { const result = await window.electronAPI?.showSaveDialog({ title: '导出会话', @@ -91,10 +93,10 @@ function App() { logger.error('Export failed:', error) toast.error('导出会话失败') } - } + }, [sessions, toast]) // 导入会话 - const handleImportSessions = async () => { + const handleImportSessions = useCallback(async () => { try { const result = await window.electronAPI?.showOpenDialog({ title: '导入会话', @@ -130,7 +132,7 @@ function App() { importSessions = data.sessions } else if (data.hosts && Array.isArray(data.hosts)) { // SSH config 格式 { hosts: [...] } - importSessions = data.hosts.map((h: any) => ({ + importSessions = data.hosts.map((h: { name?: string; host?: string; port?: number; user?: string; password?: string; privateKey?: string; group?: string }) => ({ name: h.name || h.host, host: h.host, port: h.port || 22, @@ -145,7 +147,7 @@ function App() { // 不是 JSON,尝试 SSH Config 格式 const lines = fileResult.data.split('\n') const hosts: Partial[] = [] - let currentHost: any = null + let currentHost: { name?: string; host?: string; port?: number; username?: string; authType?: 'password' | 'key'; privateKey?: string } | null = null for (const line of lines) { const trimmed = line.trim() @@ -185,9 +187,9 @@ function App() { try { await window.electronAPI?.sessionCreate({ name: session.name || `${session.username}@${session.host}`, - host: session.host, + host: session.host!, port: session.port || 22, - username: session.username, + username: session.username!, authType: session.authType || 'password', password: session.password, privateKey: session.privateKey, @@ -210,7 +212,7 @@ function App() { logger.error('Import failed:', error) toast.error('导入会话失败') } - } + }, [toast]) useEffect(() => { // Listen for menu events from main process @@ -226,6 +228,8 @@ function App() { window.electronAPI.onMenuImportSessions(handleImportSessions) window.electronAPI.onMenuExportSessions(handleExportSessions) + + window.electronAPI.onMenuOpenSettings(handleOpenSettings) } return () => { @@ -234,14 +238,21 @@ function App() { window.electronAPI.removeAllListeners('menu-quick-connect') window.electronAPI.removeAllListeners('menu-import-sessions') window.electronAPI.removeAllListeners('menu-export-sessions') + window.electronAPI.removeAllListeners('menu-open-settings') } } - }, [sessions]) + }, [handleExportSessions, handleImportSessions, handleOpenSettings]) return ( -
- {/* Title bar spacer for macOS */} -
+
+ {/* Title bar spacer for macOS — left area draggable, right area handles double-click */} +
+
+
window.electronAPI?.toggleMaximize()} + /> +
{/* Main content */}
@@ -256,7 +267,7 @@ function App() { {/* Sidebar container with transition */}
{/* Main area */} -
+
{/* Tab bar with mobile sidebar toggle */}
{isMobile && ( @@ -319,44 +330,52 @@ function App() { {/* Connection management panel (self-contained via connectionPanelStore) */} - + + + {/* Dialogs */} - { - setIsSessionDialogOpen(false) - setEditSession(null) - }} - editSession={editSession} - /> + + { + setIsSessionDialogOpen(false) + setEditSession(null) + }} + editSession={editSession} + /> + - setIsQuickConnectOpen(false)} - /> + + setIsQuickConnectOpen(false)} + /> + {/* Command Palette — isOpen/close managed via commandPaletteStore */} - { - closeCommandPalette() - setEditSession(null) - setIsSessionDialogOpen(true) - }} - onQuickConnect={() => { - closeCommandPalette() - setIsQuickConnectOpen(true) - }} - onOpenSettings={() => { - closeCommandPalette() - handleOpenSettings() - }} - onEditSession={(session) => { - closeCommandPalette() - setEditSession(session) - setIsSessionDialogOpen(true) - }} - /> + + { + closeCommandPalette() + setEditSession(null) + setIsSessionDialogOpen(true) + }} + onQuickConnect={() => { + closeCommandPalette() + setIsQuickConnectOpen(true) + }} + onOpenSettings={() => { + closeCommandPalette() + handleOpenSettings() + }} + onEditSession={(session) => { + closeCommandPalette() + setEditSession(session) + setIsSessionDialogOpen(true) + }} + /> + {/* Toast Notifications */} diff --git a/src/components/ConnectionPanel.tsx b/src/components/ConnectionPanel.tsx index 1c469dc..5d23d84 100644 --- a/src/components/ConnectionPanel.tsx +++ b/src/components/ConnectionPanel.tsx @@ -281,11 +281,11 @@ export default function ConnectionPanel() { {/* Panel */}
{/* Header */} -
+

连接管理 @@ -300,7 +300,7 @@ export default function ConnectionPanel() {

{/* Stats Overview */} -
+
( - -
{/* Recent connections */} @@ -113,7 +100,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie + +
+ +

+ 如果问题持续存在,请检查日志或联系支持 +

+
+
+ ) +} diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index bea00e7..fe11170 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -7,7 +7,7 @@ import { Component, type ReactNode } from 'react' import { createLogger } from '../../utils/logger' -import { handleError } from '../../utils/error-handler' +import DefaultFallback from './DefaultFallback' const logger = createLogger('ErrorBoundary') @@ -22,57 +22,6 @@ interface State { error: Error | null } -/** - * 默认错误_fallback UI - */ -function DefaultFallback({ error, onReset }: { error: Error; onReset: () => void }) { - const appError = handleError(error) - - return ( -
-
-
-
- - - -
-
-

出错了

-

应用遇到意外问题

-
-
- -
-

{appError.message}

- {appError.recoverable && ( -

此错误可以恢复,请尝试刷新页面

- )} -
- -
- - -
- -

- 如果问题持续存在,请检查日志或联系支持 -

-
-
- ) -} - /** * 错误边界组件 * diff --git a/src/components/common/Skeleton.tsx b/src/components/common/Skeleton.tsx index 532b551..a3e0cda 100644 --- a/src/components/common/Skeleton.tsx +++ b/src/components/common/Skeleton.tsx @@ -202,7 +202,7 @@ export function SessionTreeSkeleton({ {Array.from({ length: groups }).map((_, gIdx) => { const count = sessionsPerGroup[0] + (gIdx % (sessionsPerGroup[1] - sessionsPerGroup[0] + 1)) return ( -
+
{/* 分组头 */}
diff --git a/src/components/common/Toast.tsx b/src/components/common/Toast.tsx index aa63344..32d59b8 100644 --- a/src/components/common/Toast.tsx +++ b/src/components/common/Toast.tsx @@ -67,19 +67,19 @@ function ToastItemView({ toast, onClose }: ToastItemProps) { }, []) // 自动关闭 + const handleClose = useCallback(() => { + setVisible(false) + // 等待退场动画结束后再移除 + setTimeout(() => onClose(toast.id), 200) + }, [onClose, toast.id]) + useEffect(() => { if (toast.duration <= 0) return const timer = setTimeout(() => { handleClose() }, toast.duration) return () => clearTimeout(timer) - }, [toast.duration]) - - const handleClose = useCallback(() => { - setVisible(false) - // 等待退场动画结束后再移除 - setTimeout(() => onClose(toast.id), 200) - }, [onClose, toast.id]) + }, [toast.duration, handleClose]) return (
{/* File info */} -
+

{fileName}

diff --git a/src/components/sftp/ConnectionPanel.tsx b/src/components/sftp/ConnectionPanel.tsx index 34b8f67..4f37a33 100644 --- a/src/components/sftp/ConnectionPanel.tsx +++ b/src/components/sftp/ConnectionPanel.tsx @@ -70,7 +70,7 @@ export function ConnectionPanel({ // Disconnected State return ( <> -
+
未连接
) } + +export default memo(TerminalSearchBar) diff --git a/src/electron.d.ts b/src/electron.d.ts index db19481..027c313 100644 --- a/src/electron.d.ts +++ b/src/electron.d.ts @@ -1,93 +1,43 @@ // Type declarations for Electron IPC API exposed via preload - -interface SSHConfig { - host: string - port?: number - username: string - password?: string - privateKey?: string - passphrase?: string - agent?: string - readyTimeout?: number - keepaliveInterval?: number -} - -interface Session { - id: string - name: string - host: string - port: number - username: string - authType: 'password' | 'key' | 'agent' - password?: string - privateKey?: string - passphrase?: string - group?: string - tags?: string[] -} - -interface IPCResponse { - success: boolean - error?: string - connectionId?: string - shellId?: string - sftpId?: string - sessions?: Session[] - session?: Session - files?: any[] - result?: any - value?: any - path?: string - boundPort?: number -} +// All types imported from shared/types for consistency interface ElectronAPI { // SSH - sshConnect: (config: SSHConfig) => Promise - sshDisconnect: (connectionId: string) => Promise - sshExec: (connectionId: string, command: string) => Promise - sshCreateShell: (connectionId: string, options?: any, shellId?: string) => Promise - sshShellWrite: (shellId: string, data: string) => Promise - sshShellResize: (shellId: string, cols: number, rows: number) => Promise - sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) => Promise - sshForwardRemote: (connectionId: string, remotePort: number, localHost: string, localPort: number) => Promise - sshForwardDynamic: (connectionId: string, localPort: number) => Promise + sshConnect: (config: import('../shared/types/ipc.types').SSHConnectRequest) => Promise + sshDisconnect: (connectionId: string) => Promise<{ success: boolean; error?: string }> + sshExec: (connectionId: string, command: string) => Promise<{ success: boolean; result?: string; error?: string }> + sshCreateShell: (connectionId: string, options?: import('../shared/types/ipc.types').SSHCreateShellRequest, shellId?: string) => Promise<{ success: boolean; shellId?: string; error?: string }> + sshShellWrite: (shellId: string, data: string | Uint8Array) => Promise<{ success: boolean; error?: string }> + sshShellResize: (shellId: string, cols: number, rows: number) => Promise<{ success: boolean; error?: string }> + sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) => Promise + sshForwardRemote: (connectionId: string, remotePort: number, localHost: string, localPort: number) => Promise + sshForwardDynamic: (connectionId: string, localPort: number) => Promise // Sessions - sessionGetAll: () => Promise - sessionGetById: (id: string) => Promise - sessionCreate: (session: Partial) => Promise - sessionUpdate: (id: string, updates: Partial) => Promise - sessionDelete: (id: string) => Promise + sessionGetAll: () => Promise<{ success: boolean; sessions?: import('../shared/types/session.types').Session[]; error?: string }> + sessionGetById: (id: string) => Promise<{ success: boolean; session?: import('../shared/types/session.types').Session; error?: string }> + sessionCreate: (session: Omit) => Promise + sessionUpdate: (id: string, updates: Partial) => Promise + sessionDelete: (id: string) => Promise<{ success: boolean; error?: string }> // SFTP - sftpConnect: (connectionId: string) => Promise - sftpListDirectory: (sftpId: string, path: string) => Promise - sftpUpload: (sftpId: string, localPath: string, remotePath: string) => Promise - sftpDownload: (sftpId: string, remotePath: string, localPath: string) => Promise - sftpRename: (sftpId: string, oldPath: string, newPath: string) => Promise - sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => Promise - sftpMkdir: (sftpId: string, path: string) => Promise - sftpStat: (sftpId: string, filePath: string) => Promise - sftpChmod: (sftpId: string, filePath: string, mode: number) => Promise + sftpConnect: (connectionId: string) => Promise<{ success: boolean; sftpId?: string; error?: string }> + sftpListDirectory: (sftpId: string, path: string) => Promise<{ success: boolean; files?: unknown[]; error?: string }> + sftpUpload: (sftpId: string, localPath: string, remotePath: string) => Promise<{ success: boolean; error?: string }> + sftpDownload: (sftpId: string, remotePath: string, localPath: string) => Promise<{ success: boolean; error?: string }> + sftpRename: (sftpId: string, oldPath: string, newPath: string) => Promise<{ success: boolean; error?: string }> + sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => Promise<{ success: boolean; error?: string }> + sftpMkdir: (sftpId: string, path: string) => Promise<{ success: boolean; error?: string }> + sftpStat: (sftpId: string, filePath: string) => Promise<{ success: boolean; stats?: unknown; error?: string }> + sftpChmod: (sftpId: string, filePath: string, mode: number) => Promise<{ success: boolean; error?: string }> // Settings - settingsGet: (key: string) => Promise - settingsSet: (key: string, value: any) => Promise + settingsGet: (key: string) => Promise<{ success: boolean; value?: unknown; error?: string }> + settingsSet: (key: string, value: unknown) => Promise<{ success: boolean; error?: string }> // Dialogs - showSaveDialog: (options: any) => Promise<{ canceled: boolean; filePath?: string }> - showOpenDialog: (options: any) => Promise<{ canceled: boolean; filePaths?: string[] }> + showSaveDialog: (options: import('../shared/types/ipc.types').DialogOptions) => Promise + showOpenDialog: (options: import('../shared/types/ipc.types').DialogOptions) => Promise // File operations readFile: (filePath: string) => Promise<{ success: boolean; data?: string; error?: string }> @@ -96,23 +46,27 @@ interface ElectronAPI { // Local filesystem getHomePath: () => Promise<{ success: boolean; path?: string; error?: string }> getDownloadsPath: () => Promise - localReadDir: (dirPath: string) => Promise - localGetStats: (filePath: string) => Promise - localDelete: (filePath: string) => Promise - localRename: (oldPath: string, newPath: string) => Promise - localMkdir: (dirPath: string) => Promise + localReadDir: (dirPath: string) => Promise + localGetStats: (filePath: string) => Promise + localDelete: (filePath: string) => Promise + localRename: (oldPath: string, newPath: string) => Promise + localMkdir: (dirPath: string) => Promise // App getVersion: () => Promise + // Window + toggleMaximize: () => Promise + // Events onMenuNewSession: (callback: () => void) => void onMenuQuickConnect: (callback: () => void) => void onMenuImportSessions: (callback: () => void) => void onMenuExportSessions: (callback: () => void) => void - onShellData: (callback: (event: any, connectionId: string, shellId: string, data: Buffer) => void) => void - onSFTPProgress: (callback: (event: any, sftpId: string, progress: any) => void) => void - removeListener: (channel: string, callback: (...args: any[]) => void) => void + onMenuOpenSettings: (callback: () => void) => void + onShellData: (callback: (event: Electron.IpcRendererEvent, connectionId: string, shellId: string, data: Buffer) => void) => void + onSFTPProgress: (callback: (event: Electron.IpcRendererEvent, sftpId: string, progress: { transferred: number; total: number; percentage: number }) => void) => void + removeListener: (channel: string, callback: (...args: unknown[]) => void) => void removeAllListeners: (channel: string) => void } diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index 38ef0cc..096042b 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -89,7 +89,7 @@ export function useKeyboardShortcuts(): void { e.preventDefault() // Electron 渲染进程通过 IPC 请求全屏切换 if (window.electronAPI?.settingsSet) { - window.electronAPI.settingsGet('fullscreen').then((res: any) => { + window.electronAPI.settingsGet('fullscreen').then((res: { success: boolean; value?: unknown }) => { const isFullscreen = res?.value === true window.electronAPI?.settingsSet('fullscreen', !isFullscreen) }).catch(() => { diff --git a/src/hooks/useSFTP.ts b/src/hooks/useSFTP.ts index 7f00866..6816fd8 100644 --- a/src/hooks/useSFTP.ts +++ b/src/hooks/useSFTP.ts @@ -4,7 +4,7 @@ * 从 SFTPView 提取状态管理和业务逻辑 */ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { useSessionStore } from '../stores' import { useTabStore, type ConnectionInstance } from '../stores/tabStore' import { useConnectionPool, getSessionConfig } from '../stores/connection' @@ -13,6 +13,14 @@ import type { FileItem, TransferTask, SortField, SortOrder } from '../../shared/ const logger = createLogger('useSFTP') +// 本地目录条目类型 +interface LocalDirEntry { + name: string + isDirectory: boolean + size?: number + mtime?: number +} + export interface UseSFTPOptions { tabId: string } @@ -132,12 +140,13 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { } else { logger.error('Failed to get home path') } - }).catch((err: any) => { - logger.error(`getHomePath error: ${err?.message || err}`) + }).catch((err: unknown) => { + logger.error(`getHomePath error: ${err instanceof Error ? err.message : String(err)}`) }) } else { logger.warn('electronAPI not available') } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Load local directory @@ -150,11 +159,11 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { } try { - const result = await window.electronAPI.localReadDir(path) + const result = await window.electronAPI.localReadDir(path) as { success: boolean; files?: LocalDirEntry[]; error?: string } logger.debug('localReadDir result', result) if (result.success && result.files) { - const files: FileItem[] = result.files.map((f: any) => ({ + const files: FileItem[] = result.files.map((f: LocalDirEntry) => ({ name: f.name, path: `${path}/${f.name}`, type: f.isDirectory ? 'directory' : 'file', @@ -167,8 +176,8 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { } else { logger.error(`Failed to load local directory: ${result.error}`) } - } catch (err: any) { - logger.error(`loadLocalDirectory error: ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`loadLocalDirectory error: ${err instanceof Error ? err.message : String(err)}`, err) } }, []) @@ -186,21 +195,24 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { logger.debug('sftpListDirectory result', result) if (result.success && result.files) { - const files: FileItem[] = result.files.map((f: any) => ({ - name: f.filename, - path: `${path}/${f.filename}`.replace(/\/+/g, '/'), - type: f.longname?.startsWith('d') ? 'directory' : 'file', - size: f.attrs?.size || 0, - modified: f.attrs?.mtime || Date.now() / 1000, - permissions: f.longname?.substring(0, 10) || '-rw-r--r--', - })) + const files: FileItem[] = result.files.map((f: unknown) => { + const entry = f as { filename?: string; longname?: string; attrs?: { size?: number; mtime?: number } } + return { + name: entry.filename || '', + path: `${path}/${entry.filename || ''}`.replace(/\/+/g, '/'), + type: entry.longname?.startsWith('d') ? 'directory' : 'file', + size: entry.attrs?.size || 0, + modified: entry.attrs?.mtime || Date.now() / 1000, + permissions: entry.longname?.substring(0, 10) || '-rw-r--r--', + } + }) setRemoteFiles(files) logger.info(`Loaded ${files.length} remote files`) } else { logger.error(`Failed to load remote directory: ${result.error}`) } - } catch (err: any) { - logger.error(`loadRemoteDirectory error: ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`loadRemoteDirectory error: ${err instanceof Error ? err.message : String(err)}`, err) setError('Failed to load remote directory') } }, [sftpId]) @@ -373,7 +385,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { setTransferQueue(prev => [...prev, ...newTasks]) // Set up progress listener scoped to this batch - const progressHandler = (_event: any, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => { + const progressHandler = (_event: unknown, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => { // Update the first task that is currently transferring setTransferQueue(prev => { const idx = prev.findIndex(t => t.type === 'upload' && t.status === 'transferring') @@ -387,7 +399,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { }) } - window.electronAPI.onSFTPProgress(progressHandler) + window.electronAPI.onSFTPProgress(progressHandler as (...args: unknown[]) => void) try { for (const task of newTasks) { @@ -413,10 +425,10 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { )) logger.error(`Upload failed: ${task.fileName} - ${result.error}`) } - } catch (err: any) { + } catch (err: unknown) { logger.error('Upload failed', err) setTransferQueue(prev => prev.map(t => - t.id === task.id ? { ...t, status: 'failed', error: err?.message || 'Upload failed' } : t + t.id === task.id ? { ...t, status: 'failed', error: err instanceof Error ? err.message : 'Upload failed' } : t )) } } @@ -424,7 +436,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { // Refresh remote directory after all uploads loadRemoteDirectory(remotePath) } finally { - window.electronAPI.removeListener?.('sftp:progress', progressHandler) + window.electronAPI.removeListener?.('sftp:progress', progressHandler as (...args: unknown[]) => void) } }, [sftpId, localPath, remotePath, loadRemoteDirectory]) @@ -448,7 +460,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { setTransferQueue(prev => [...prev, ...newTasks]) // Set up progress listener - const progressHandler = (_event: any, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => { + const progressHandler = (_event: unknown, _sftpId: string, progress: { transferred: number; total: number; percentage: number }) => { setTransferQueue(prev => { const idx = prev.findIndex(t => t.type === 'download' && t.status === 'transferring') if (idx === -1) return prev @@ -461,7 +473,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { }) } - window.electronAPI.onSFTPProgress(progressHandler) + window.electronAPI.onSFTPProgress(progressHandler as (...args: unknown[]) => void) try { for (const task of newTasks) { @@ -486,10 +498,10 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { )) logger.error(`Download failed: ${task.fileName} - ${result.error}`) } - } catch (err: any) { + } catch (err: unknown) { logger.error('Download failed', err) setTransferQueue(prev => prev.map(t => - t.id === task.id ? { ...t, status: 'failed', error: err?.message || 'Download failed' } : t + t.id === task.id ? { ...t, status: 'failed', error: err instanceof Error ? err.message : 'Download failed' } : t )) } } @@ -497,7 +509,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { // Refresh local directory after all downloads loadLocalDirectory(localPath) } finally { - window.electronAPI.removeListener?.('sftp:progress', progressHandler) + window.electronAPI.removeListener?.('sftp:progress', progressHandler as (...args: unknown[]) => void) } }, [sftpId, remoteFiles, remoteSelected, localPath, loadLocalDirectory]) @@ -522,8 +534,8 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { logger.error(`Delete failed: ${file.name} - ${result.error}`) allSuccess = false } - } catch (err: any) { - logger.error(`Delete error: ${file.name} - ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`Delete error: ${file.name} - ${err instanceof Error ? err.message : String(err)}`, err) allSuccess = false } } @@ -556,8 +568,8 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { logger.error(`Rename failed: ${result.error}`) return false } - } catch (err: any) { - logger.error(`Rename error: ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`Rename error: ${err instanceof Error ? err.message : String(err)}`, err) return false } }, [sftpId, remotePath, loadRemoteDirectory]) @@ -570,15 +582,15 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { } try { - const result = await window.electronAPI.sftpStat(sftpId, filePath) + const result = await window.electronAPI.sftpStat(sftpId, filePath) as { success: boolean; stats?: { size: number; mode: number; mtime: number; atime: number; isDirectory: boolean; isFile: boolean; isSymbolicLink: boolean }; error?: string } if (result.success && result.stats) { return result.stats } else { logger.error(`Stat failed: ${result.error}`) return null } - } catch (err: any) { - logger.error(`Stat error: ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`Stat error: ${err instanceof Error ? err.message : String(err)}`, err) return null } }, [sftpId]) @@ -600,18 +612,23 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn { logger.error(`Chmod failed: ${result.error}`) return false } - } catch (err: any) { - logger.error(`Chmod error: ${err?.message}`, err) + } catch (err: unknown) { + logger.error(`Chmod error: ${err instanceof Error ? err.message : String(err)}`, err) return false } }, [sftpId, remotePath, loadRemoteDirectory]) - // Cleanup on unmount + // Cleanup on unmount — use refs to avoid re-running when connectionId changes + const connectionIdRef = useRef(connectionId) + connectionIdRef.current = connectionId useEffect(() => { return () => { - disconnect() + const cid = connectionIdRef.current + if (cid && window.electronAPI) { + window.electronAPI.sshDisconnect(cid).catch(() => {}) + } } - }, [disconnect]) + }, []) return { sessions, diff --git a/src/hooks/useTerminalConnection.ts b/src/hooks/useTerminalConnection.ts index 2dfbea6..022ee5b 100644 --- a/src/hooks/useTerminalConnection.ts +++ b/src/hooks/useTerminalConnection.ts @@ -4,6 +4,7 @@ import { useTabStore, type ConnectionInstance } from '../stores/tabStore' import { getSessionConfig, getConnectionStore, type ConnectionState } from '../stores/connection' import { useTerminalSnapshotStore } from '../stores/terminalSnapshotStore' import { createLogger } from '../utils/logger' +import type { SSHConnectionConfig } from '../../shared/types/session.types' const logger = createLogger('useTerminalConnection') @@ -121,14 +122,8 @@ export function useTerminalConnection({ try { // 获取配置 - let config: { - host: string - port: number - username: string - password?: string - privateKey?: string - passphrase?: string - } | null = null + let config: SSHConnectionConfig | null = null + let authType: string | undefined if (currentTab.connectionConfig) { config = { @@ -139,14 +134,22 @@ export function useTerminalConnection({ privateKey: currentTab.connectionConfig.privateKey, passphrase: currentTab.connectionConfig.passphrase, } + authType = currentTab.connectionConfig.authType } else if (currentTab.sessionId) { config = await getSessionConfig(currentTab.sessionId) + authType = config?.authType } if (!config) { throw new Error('无可用连接信息') } + // 非密钥认证时,不传递 privateKey/passphrase,避免后端误解析 + if (authType && authType !== 'key') { + delete config.privateKey + delete config.passphrase + } + // 创建连接 const result = await getConnectionStore().createConnection(tabId, config) @@ -205,16 +208,17 @@ export function useTerminalConnection({ xtermRef.current?.focus() - } catch (error: any) { + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error) setConnectionState('error') updateTabStatus(tabId, 'error') - setStatusMessage(`错误: ${error.message}`) - xtermRef.current?.writeln(`\r\n\x1b[31m错误: ${error.message}\x1b[0m`) + setStatusMessage(`错误: ${errorMessage}`) + xtermRef.current?.writeln(`\r\n\x1b[31m错误: ${errorMessage}\x1b[0m`) } finally { isConnectingRef.current = false hasConnectedRef.current = true } - }, [tabId, updateTabConnectionInstance, updateTabStatus, getTabById, xtermRef]) + }, [tabId, updateTabConnectionInstance, updateTabStatus, updateTab, getTabById, xtermRef]) // 恢复挂起的连接 const resumeConnection = useCallback(async () => { diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index 0689c50..e6bc477 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -6,10 +6,9 @@ import { darkColors, lightColors, type AppThemeMode, type ColorMap } from '../th * Resolves the *effective* theme ('dark' | 'light') from the user preference, * including the 'system' → prefers-color-scheme mapping. */ -function resolveEffectiveTheme(preference: AppThemeMode): 'dark' | 'light' { - if (preference !== 'system') return preference - if (typeof window === 'undefined') return 'dark' - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +function resolveEffectiveTheme(_preference: AppThemeMode): 'dark' | 'light' { + // 当前版本固定使用 dark 主题,后续版本开放主题切换 + return 'dark' } /** @@ -32,26 +31,28 @@ function applyCSSVars(colors: ColorMap) { */ export function useTheme() { const theme = useSettingsStore((s) => s.settings.theme) - const isLoaded = useSettingsStore((s) => s.isLoaded) const effective = useMemo(() => resolveEffectiveTheme(theme), [theme]) // Apply theme to DOM whenever effective theme changes useEffect(() => { - if (!isLoaded) return - const colors = effective === 'dark' ? darkColors : lightColors // Set data attribute for potential direct CSS selectors document.documentElement.setAttribute('data-theme', effective) + if (effective === 'dark') { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } // Apply CSS custom properties applyCSSVars(colors) // Update body bg & colour (mirrors index.css body rules but via vars) - document.body.style.backgroundColor = 'var(--color-dark-900)' - document.body.style.color = 'var(--color-text-primary)' - }, [effective, isLoaded]) + document.body.style.backgroundColor = '#0f172a' + document.body.style.color = '#e2e8f0' + }, [effective]) // Listen for system colour-scheme changes when preference is 'system' useEffect(() => { @@ -64,8 +65,8 @@ export function useTheme() { const colors = e.matches ? darkColors : lightColors document.documentElement.setAttribute('data-theme', newEffective) applyCSSVars(colors) - document.body.style.backgroundColor = 'var(--color-dark-900)' - document.body.style.color = 'var(--color-text-primary)' + document.body.style.backgroundColor = '#0f172a' + document.body.style.color = '#e2e8f0' } mql.addEventListener('change', handler) diff --git a/src/index.css b/src/index.css index c8d31af..5ab1923 100644 --- a/src/index.css +++ b/src/index.css @@ -17,16 +17,36 @@ --color-dark-700: #334155; --color-dark-600: #475569; + /* Semantic tokens (dark default) */ + --color-surface-base: #0f172a; + --color-surface-raised: #1e293b; + --color-surface-overlay: #334155; + --color-surface-inset: #475569; + --color-text-primary: #e2e8f0; --color-text-secondary: #cbd5e1; --color-text-muted: #94a3b8; --color-text-faint: #64748b; --color-text-dim: #475569; + --color-content-primary: #e2e8f0; + --color-content-secondary: #cbd5e1; + --color-content-tertiary: #94a3b8; + --color-content-inverse: #0f172a; + + --color-border: #334155; + --color-border-strong: #475569; + --color-accent-blue: #3b82f6; --color-accent-green: #10b981; --color-accent-purple: #8b5cf6; --color-accent-orange: #f97316; + --color-accent-red: #ef4444; + + --color-feedback-success: #10b981; + --color-feedback-warning: #f59e0b; + --color-feedback-error: #ef4444; + --color-feedback-info: #3b82f6; --color-scrollbar-track: #1e293b; --color-scrollbar-thumb: #475569; @@ -35,45 +55,20 @@ --color-terminal-bg: #0f172a; } -/* ---- Light theme ---- */ -:root[data-theme='light'] { - --color-dark-900: #ffffff; - --color-dark-800: #f3f4f6; - --color-dark-700: #e5e7eb; - --color-dark-600: #d1d5db; - - --color-text-primary: #1f2937; - --color-text-secondary: #374151; - --color-text-muted: #6b7280; - --color-text-faint: #9ca3af; - --color-text-dim: #d1d5db; - - --color-accent-blue: #2563eb; - --color-accent-green: #059669; - --color-accent-purple: #7c3aed; - --color-accent-orange: #ea580c; - - --color-scrollbar-track: #f3f4f6; - --color-scrollbar-thumb: #d1d5db; - --color-scrollbar-thumb-h: #9ca3af; - - --color-terminal-bg: #ffffff; -} - * { margin: 0; padding: 0; box-sizing: border-box; } -body { +html, body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background-color: var(--color-dark-900); - color: var(--color-text-primary); + background-color: #0f172a !important; + color: #e2e8f0; overflow: hidden; } @@ -82,6 +77,15 @@ body { height: 100vh; } +/* Window chrome utilities */ +.drag { + -webkit-app-region: drag; +} + +.no-drag { + -webkit-app-region: no-drag; +} + /* Custom scrollbar */ ::-webkit-scrollbar { width: 8px; diff --git a/src/stores/connection/poolStore.ts b/src/stores/connection/poolStore.ts index dc6bec4..3b1be91 100644 --- a/src/stores/connection/poolStore.ts +++ b/src/stores/connection/poolStore.ts @@ -201,10 +201,10 @@ export const usePoolStore = create((set, get) => ({ }) return { success: true, connectionId } - } catch (error: any) { + } catch (error: unknown) { return { success: false, - error: error.message || 'Unknown error creating connection', + error: error instanceof Error ? error.message : 'Unknown error creating connection', } } }, diff --git a/src/stores/connection/sessionHelper.ts b/src/stores/connection/sessionHelper.ts index d559da6..4e27261 100644 --- a/src/stores/connection/sessionHelper.ts +++ b/src/stores/connection/sessionHelper.ts @@ -26,6 +26,7 @@ export async function getSessionConfig(sessionId: string): Promise((set) => ({ setSessions: (sessions) => set({ sessions }), - addSession: (session) => { - set((state) => ({ - sessions: [...state.sessions, session], - })) + addSession: async (session) => { + // 同步到数据库 + const result = await window.electronAPI?.sessionCreate(session) + if (result?.success && result.session) { + set((state) => ({ + sessions: [...state.sessions, result.session!], + })) + } else { + // 失败时仍然添加到本地状态,避免 UI 卡住 + set((state) => ({ + sessions: [...state.sessions, session], + })) + } }, removeSession: (id) => { diff --git a/src/stores/tabStore.ts b/src/stores/tabStore.ts index 733f732..601054a 100644 --- a/src/stores/tabStore.ts +++ b/src/stores/tabStore.ts @@ -1,22 +1,13 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' import { createLogger } from '../utils/logger' +import type { SSHConnectionConfig } from '../../shared/types/session.types' const logger = createLogger('TabStore') export type TabType = 'welcome' | 'terminal' | 'sftp' | 'portforward' | 'settings' export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error' -// SSH 连接配置(用于快速连接) -export interface SSHConnectionConfig { - host: string - port: number - username: string - password?: string - privateKey?: string - passphrase?: string -} - // 连接实例信息 export interface ConnectionInstance { connectionId: string // 实际的 SSH 连接 ID @@ -341,7 +332,7 @@ export const useTabStore = create()( logger.info('Restoring tabs from persisted state:', persisted.tabs.length, 'tabs') // 将持久化的标签转换为运行时标签 - const restoredTabs: Tab[] = persisted.tabs.map((tab: any) => ({ + const restoredTabs: Tab[] = persisted.tabs.map((tab: Tab) => ({ ...tab, isActive: false, connectionStatus: 'disconnected' as ConnectionStatus, diff --git a/src/themes/app-theme.ts b/src/themes/app-theme.ts index ef70971..6513463 100644 --- a/src/themes/app-theme.ts +++ b/src/themes/app-theme.ts @@ -30,6 +30,12 @@ export const darkColors: ColorMap = { 'dark-700': '#334155', 'dark-600': '#475569', + // Semantic surface tokens + 'surface-base': '#0f172a', + 'surface-raised': '#1e293b', + 'surface-overlay': '#334155', + 'surface-inset': '#475569', + // Text 'text-primary': '#e2e8f0', // gray-200 'text-secondary': '#cbd5e1', // gray-300 @@ -37,11 +43,28 @@ export const darkColors: ColorMap = { 'text-faint': '#64748b', // gray-500 'text-dim': '#475569', // gray-600 + // Content tokens + 'content-primary': '#e2e8f0', + 'content-secondary': '#cbd5e1', + 'content-tertiary': '#94a3b8', + 'content-inverse': '#0f172a', + + // Border tokens + 'border': '#334155', + 'border-strong': '#475569', + // Accent 'accent-blue': '#3b82f6', 'accent-green': '#10b981', 'accent-purple': '#8b5cf6', 'accent-orange': '#f97316', + 'accent-red': '#ef4444', + + // Feedback tokens + 'feedback-success': '#10b981', + 'feedback-warning': '#f59e0b', + 'feedback-error': '#ef4444', + 'feedback-info': '#3b82f6', // UI chrome 'scrollbar-track': '#1e293b', @@ -60,6 +83,12 @@ export const lightColors: ColorMap = { 'dark-700': '#e5e7eb', // gray-200 'dark-600': '#d1d5db', // gray-300 + // Semantic surface tokens + 'surface-base': '#ffffff', + 'surface-raised': '#f3f4f6', + 'surface-overlay': '#e5e7eb', + 'surface-inset': '#d1d5db', + // Text 'text-primary': '#1f2937', // gray-800 'text-secondary': '#374151', // gray-700 @@ -67,11 +96,28 @@ export const lightColors: ColorMap = { 'text-faint': '#9ca3af', // gray-400 'text-dim': '#d1d5db', // gray-300 + // Content tokens + 'content-primary': '#1f2937', + 'content-secondary': '#374151', + 'content-tertiary': '#6b7280', + 'content-inverse': '#ffffff', + + // Border tokens + 'border': '#e5e7eb', + 'border-strong': '#d1d5db', + // Accent (kept the same hue family, slightly adjusted for light bg) 'accent-blue': '#2563eb', 'accent-green': '#059669', 'accent-purple': '#7c3aed', 'accent-orange': '#ea580c', + 'accent-red': '#dc2626', + + // Feedback tokens + 'feedback-success': '#059669', + 'feedback-warning': '#d97706', + 'feedback-error': '#dc2626', + 'feedback-info': '#2563eb', // UI chrome 'scrollbar-track': '#f3f4f6', diff --git a/tailwind.config.js b/tailwind.config.js index fbe7a25..b0002cf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,21 +4,82 @@ export default { "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], + darkMode: 'class', theme: { extend: { colors: { - // Background surfaces — driven by CSS custom properties set per theme + // Semantic color tokens — driven by CSS custom properties set per theme + surface: { + base: 'var(--color-surface-base)', + raised: 'var(--color-surface-raised)', + overlay: 'var(--color-surface-overlay)', + inset: 'var(--color-surface-inset)', + }, + // Legacy dark-* aliases for backward compatibility dark: { - 900: 'var(--color-dark-900)', - 800: 'var(--color-dark-800)', - 700: 'var(--color-dark-700)', - 600: 'var(--color-dark-600)', + 900: 'var(--color-surface-base)', + 800: 'var(--color-surface-raised)', + 700: 'var(--color-surface-overlay)', + 600: 'var(--color-surface-inset)', }, accent: { blue: 'var(--color-accent-blue)', green: 'var(--color-accent-green)', purple: 'var(--color-accent-purple)', orange: 'var(--color-accent-orange)', + red: 'var(--color-accent-red)', + }, + content: { + primary: 'var(--color-content-primary)', + secondary: 'var(--color-content-secondary)', + tertiary: 'var(--color-content-tertiary)', + inverse: 'var(--color-content-inverse)', + }, + border: { + DEFAULT: 'var(--color-border)', + strong: 'var(--color-border-strong)', + }, + }, + fontFamily: { + sans: [ + 'Inter', + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + '"Noto Sans SC"', + '"PingFang SC"', + '"Microsoft YaHei"', + 'sans-serif', + ], + mono: [ + 'Menlo', + 'Monaco', + 'Consolas', + '"Liberation Mono"', + '"Courier New"', + 'monospace', + ], + }, + animation: { + 'fade-in': 'fadeIn 0.15s ease-out', + 'slide-up': 'slideUp 0.2s ease-out', + 'scale-in': 'scaleIn 0.15s ease-out', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideUp: { + '0%': { opacity: '0', transform: 'translateY(8px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + scaleIn: { + '0%': { opacity: '0', transform: 'scale(0.96)' }, + '100%': { opacity: '1', transform: 'scale(1)' }, }, }, }, diff --git a/test/services/crypto.test.ts b/test/services/crypto.test.ts new file mode 100644 index 0000000..eff24bb --- /dev/null +++ b/test/services/crypto.test.ts @@ -0,0 +1,95 @@ +/** + * Crypto Service Integration Tests + * 验证加密/解密核心逻辑的正确性和安全性 + */ + +import { describe, it, expect } from 'vitest' +import { encrypt, decrypt, encryptAsync, simpleEncrypt, simpleDecrypt } from '../../electron/services/crypto' + +describe('Crypto Service', () => { + const testPassword = 'test-password-123' + const testData = 'Hello, OpsXShell! 你好,世界!' + const testKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + + describe('encrypt / decrypt', () => { + it('应该能正确加密和解密数据', () => { + const encrypted = encrypt(testData, testPassword) + expect(encrypted).toBeTruthy() + expect(encrypted).toContain(':') // 格式: version:salt:iv:tag:ciphertext + + const decrypted = decrypt(encrypted, testPassword) + expect(decrypted).toBe(testData) + }) + + it('密文应该包含版本标识', () => { + const encrypted = encrypt(testData, testPassword) + const parts = encrypted.split(':') + expect(parts.length).toBe(5) + expect(parts[0]).toBe('v1') + }) + + it('不同密码应该产生不同密文', () => { + const encrypted1 = encrypt(testData, testPassword) + const encrypted2 = encrypt(testData, 'different-password') + expect(encrypted1).not.toBe(encrypted2) + }) + + it('相同数据多次加密应该产生不同密文(随机 salt/iv)', () => { + const encrypted1 = encrypt(testData, testPassword) + const encrypted2 = encrypt(testData, testPassword) + expect(encrypted1).not.toBe(encrypted2) + + // 但都能正确解密 + expect(decrypt(encrypted1, testPassword)).toBe(testData) + expect(decrypt(encrypted2, testPassword)).toBe(testData) + }) + + it('错误密码应该抛出异常', () => { + const encrypted = encrypt(testData, testPassword) + expect(() => decrypt(encrypted, 'wrong-password')).toThrow('Decryption failed') + }) + + it('应该向后兼容旧格式密文(4部分,无版本标识)', () => { + // 构造一个旧格式密文: salt:iv:tag:ciphertext + const encrypted = encrypt(testData, testPassword) + const parts = encrypted.split(':') + const oldFormat = parts.slice(1).join(':') // 去掉版本标识 + + expect(decrypt(oldFormat, testPassword)).toBe(testData) + }) + }) + + describe('encryptAsync / decrypt', () => { + it('异步加密应该产生可解密的结果', async () => { + const encrypted = await encryptAsync(testData, testPassword) + const decrypted = decrypt(encrypted, testPassword) + expect(decrypted).toBe(testData) + }) + + it('异步加密密文应该包含版本标识', async () => { + const encrypted = await encryptAsync(testData, testPassword) + const parts = encrypted.split(':') + expect(parts.length).toBe(5) + expect(parts[0]).toBe('v1') + }) + }) + + describe('simpleEncrypt / simpleDecrypt', () => { + it('应该能正确加密和解密数据', () => { + const encrypted = simpleEncrypt(testData, testKey) + expect(encrypted).toBeTruthy() + + const decrypted = simpleDecrypt(encrypted, testKey) + expect(decrypted).toBe(testData) + }) + + it('错误密钥应该抛出异常', () => { + const encrypted = simpleEncrypt(testData, testKey) + expect(() => simpleDecrypt(encrypted, 'wrong-key')).toThrow('Decryption failed') + }) + + it('无效密钥长度应该抛出异常', () => { + expect(() => simpleEncrypt(testData, 'short')).toThrow('Invalid key length') + }) + }) +}) diff --git a/test/setup.ts b/test/setup.ts index a77e0ba..55859cd 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -64,7 +64,7 @@ const mockElectronAPI = { removeListener: vi.fn(), } -window.electronAPI = mockElectronAPI as any +window.electronAPI = mockElectronAPI as unknown as typeof window.electronAPI // Mock navigator Object.defineProperty(window, 'navigator', { @@ -98,7 +98,7 @@ Object.defineProperty(window, 'matchMedia', { // Suppress console errors in tests const originalError = console.error beforeAll(() => { - console.error = (...args: any[]) => { + console.error = (...args: unknown[]) => { if ( typeof args[0] === 'string' && (args[0].includes('Warning:') || args[0].includes('ReactDOM.render')) diff --git a/test/stores/connectionPool.test.ts b/test/stores/connectionPool.test.ts index be2da15..ee4f7ea 100644 --- a/test/stores/connectionPool.test.ts +++ b/test/stores/connectionPool.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect } from 'vitest' -import { useConnectionPool, useConnectionStore, getConnectionStore } from '../../src/stores/connectionPool' +import { useConnectionPool, useConnectionStore, getConnectionStore } from '../../src/stores/connection' describe('ConnectionPool Store', () => { describe('store 初始化', () => { diff --git a/test/stores/tabStore.test.ts b/test/stores/tabStore.test.ts index 1894854..b3707b2 100644 --- a/test/stores/tabStore.test.ts +++ b/test/stores/tabStore.test.ts @@ -44,7 +44,7 @@ describe('TabStore', () => { }) it('添加 settings 类型时如果已存在则激活而不是创建新的', () => { - const { addTab, tabs } = useTabStore.getState() + const { addTab } = useTabStore.getState() addTab({ title: 'Settings', type: 'settings' }) addTab({ title: 'Settings 2', type: 'settings' }) @@ -86,12 +86,12 @@ describe('TabStore', () => { addTab({ title: 'Tab 1', type: 'terminal' }) addTab({ title: 'Tab 2', type: 'sftp' }) - const { tabs, activeTabId } = useTabStore.getState() + const { tabs } = useTabStore.getState() const tabToClose = tabs.find(t => t.title === 'Tab 1') if (tabToClose) { closeTab(tabToClose.id) - const { tabs: newTabs, activeTabId: newActiveId } = useTabStore.getState() + const { tabs: newTabs } = useTabStore.getState() expect(newTabs.find(t => t.isActive)?.title).toBe('Tab 2') } }) @@ -112,7 +112,7 @@ describe('TabStore', () => { describe('closeOtherTabs', () => { it('应该关闭除指定标签外的所有标签', () => { - const { addTab, closeOtherTabs, tabs } = useTabStore.getState() + const { addTab, closeOtherTabs } = useTabStore.getState() addTab({ title: 'Tab 1', type: 'terminal' }) addTab({ title: 'Tab 2', type: 'sftp' }) diff --git a/vite.config.ts b/vite.config.ts index cc38660..f75a18f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,23 +9,42 @@ export default defineConfig({ build: { outDir: 'dist', emptyOutDir: true, + sourcemap: true, + cssCodeSplit: true, rollupOptions: { output: { manualChunks: { + 'vendor-core': ['react', 'react-dom', 'react-router-dom'], 'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-search', '@xterm/addon-webgl'], - 'vendor-react': ['react', 'react-dom'], - 'vendor-lucide': ['lucide-react'], - 'vendor-zustand': ['zustand'], + 'vendor-utils': ['zustand', 'lucide-react'], + }, + chunkFileNames: 'assets/js/[name]-[hash].js', + entryFileNames: 'assets/js/[name]-[hash].js', + assetFileNames: (assetInfo) => { + const info = assetInfo.name || '' + if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(info)) { + return 'assets/images/[name]-[hash][extname]' + } + if (/\.css$/i.test(info)) { + return 'assets/css/[name]-[hash][extname]' + } + return 'assets/[name]-[hash][extname]' }, }, }, + assetsInlineLimit: 4096, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), + '@shared': path.resolve(__dirname, './shared'), }, }, server: { port: 5173, + strictPort: true, + }, + optimizeDeps: { + include: ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-search', '@xterm/addon-webgl', 'react', 'react-dom'], }, })