diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index 320ffbb..ec86339 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -43,12 +43,45 @@ export const EXTERNAL_DEPENDENCIES = [ const config: ForgeConfig = { hooks: { - prePackage: async () => { - console.error("prePackage"); + prePackage: async (_forgeConfig, platform, arch) => { + console.error("prePackage", { platform, arch }); const projectRoot = normalize(__dirname); // In a monorepo, node_modules are typically at the root level const monorepoRoot = join(projectRoot, "../../"); // Go up to monorepo root + // Copy platform-specific Node.js binary + console.log(`Copying Node.js binary for ${platform}-${arch}...`); + const nodeBinarySource = join( + projectRoot, + "resources", + "node-binaries", + `${platform}-${arch}`, + platform === "win32" ? "node.exe" : "node" + ); + const nodeBinaryDest = join( + projectRoot, + "resources", + "node-binaries", + `${platform}-${arch}` + ); + + // Check if the binary exists + if (existsSync(nodeBinarySource)) { + // Ensure destination directory exists + if (!existsSync(nodeBinaryDest)) { + mkdirSync(nodeBinaryDest, { recursive: true }); + } + console.log(`✓ Node.js binary found for ${platform}-${arch}`); + } else { + console.error( + `✗ Node.js binary not found for ${platform}-${arch} at ${nodeBinarySource}` + ); + console.error( + ` Please run 'pnpm download-node' or 'pnpm download-node:all' first` + ); + throw new Error(`Missing Node.js binary for ${platform}-${arch}`); + } + const getExternalNestedDependencies = async ( nodeModuleNames: string[], includeNestedDeps = true, @@ -249,6 +282,7 @@ const config: ForgeConfig = { extraResource: [ "../../packages/native-helpers/swift-helper/bin", "./src/db/migrations", + "./resources", "./src/assets", ], extendInfo: { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index db7b9c0..4817b65 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -30,7 +30,9 @@ "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit migrate", "build:swift-helper": "pnpm --filter @amical/swift-helper build", - "dev": "pnpm start" + "dev": "pnpm start", + "download-node": "tsx scripts/download-node-binaries.ts", + "download-node:all": "tsx scripts/download-node-binaries.ts --all" }, "keywords": [], "license": "MIT", @@ -72,6 +74,7 @@ "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@libsql/client": "^0.15.9", + "@libsql/darwin-x64": "0.5.13", "@openrouter/ai-sdk-provider": "^0.7.2", "@radix-ui/react-accordion": "^1.2.10", "@radix-ui/react-alert-dialog": "^1.1.13", @@ -123,6 +126,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.10.5", "input-otp": "^1.4.2", + "jest-worker": "^29.7.0", "keytar": "^7.9.0", "libsql": "^0.5.13", "lucide-react": "^0.510.0", diff --git a/apps/desktop/src/assets/silero_vad_v5.onnx b/apps/desktop/resources/models/silero_vad_v5.onnx similarity index 100% rename from apps/desktop/src/assets/silero_vad_v5.onnx rename to apps/desktop/resources/models/silero_vad_v5.onnx diff --git a/apps/desktop/resources/node-binaries/.gitignore b/apps/desktop/resources/node-binaries/.gitignore new file mode 100644 index 0000000..23d5d16 --- /dev/null +++ b/apps/desktop/resources/node-binaries/.gitignore @@ -0,0 +1,4 @@ +# Ignore downloaded binaries +* +!.gitignore +!README.md \ No newline at end of file diff --git a/apps/desktop/resources/node-binaries/README.md b/apps/desktop/resources/node-binaries/README.md new file mode 100644 index 0000000..4fdd5ed --- /dev/null +++ b/apps/desktop/resources/node-binaries/README.md @@ -0,0 +1,41 @@ +# Node.js Binaries + +This directory contains platform-specific Node.js binaries for running the Whisper worker process. + +## Structure + +``` +node-binaries/ +├── darwin-arm64/ +│ └── node +├── darwin-x64/ +│ └── node +├── win32-x64/ +│ └── node.exe +└── linux-x64/ + └── node +``` + +## Download + +Run the download script to populate this directory: + +```bash +# Download for current platform only (recommended for development) +pnpm download-node + +# Download for all platforms (for CI/CD or cross-platform builds) +pnpm download-node:all +``` + +## Purpose + +These binaries are used to spawn a separate Node.js process for Whisper transcription, providing: +- Avoidance of Electron's V8 memory cage limitations (4GB max heap) +- Proper GPU/Metal framework initialization +- Ability to load large Whisper models (3GB+) without OOM errors +- Clean process isolation from Electron's runtime + +## Version + +Currently using Node.js v22.17.0 LTS binaries. \ No newline at end of file diff --git a/apps/desktop/scripts/download-node-binaries.ts b/apps/desktop/scripts/download-node-binaries.ts new file mode 100644 index 0000000..8d29495 --- /dev/null +++ b/apps/desktop/scripts/download-node-binaries.ts @@ -0,0 +1,263 @@ +#!/usr/bin/env tsx + +import * as https from 'node:https'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { execSync } from 'node:child_process'; +import { pipeline } from 'node:stream/promises'; +import { createWriteStream, mkdirSync, chmodSync } from 'node:fs'; + +// Node.js version to download +const NODE_VERSION = '22.17.0'; + +// Platform/arch types +type Platform = 'darwin' | 'win32' | 'linux'; +type Architecture = 'arm64' | 'x64'; + +interface PlatformConfig { + platform: Platform; + arch: Architecture; + url: string; + binary: string; +} + +// Platform configurations +const PLATFORMS: PlatformConfig[] = [ + { + platform: 'darwin', + arch: 'arm64', + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz`, + binary: 'bin/node' + }, + { + platform: 'darwin', + arch: 'x64', + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-x64.tar.gz`, + binary: 'bin/node' + }, + { + platform: 'win32', + arch: 'x64', + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-win-x64.zip`, + binary: 'node.exe' + }, + { + platform: 'linux', + arch: 'x64', + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`, + binary: 'bin/node' + } +]; + +const RESOURCES_DIR = path.join(__dirname, '..', 'resources', 'node-binaries'); + +// Parse command line arguments +const args = process.argv.slice(2); +const downloadAll = args.includes('--all'); + +async function downloadFile(url: string, dest: string): Promise { + return new Promise((resolve, reject) => { + const file = createWriteStream(dest); + + https.get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Handle redirect + const redirectUrl = response.headers.location; + if (!redirectUrl) { + reject(new Error('Redirect without location header')); + return; + } + https.get(redirectUrl, async (redirectResponse) => { + if (redirectResponse.statusCode !== 200) { + reject(new Error(`Failed to download: ${redirectResponse.statusCode}`)); + return; + } + + // Show download progress + const totalSize = parseInt(redirectResponse.headers['content-length'] || '0', 10); + let downloadedSize = 0; + + redirectResponse.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize > 0) { + const percent = Math.round((downloadedSize / totalSize) * 100); + process.stdout.write(`\r Downloading: ${percent}%`); + } + }); + + await pipeline(redirectResponse, file); + process.stdout.write('\n'); + resolve(); + }).on('error', reject); + } else if (response.statusCode === 200) { + // Direct download + const totalSize = parseInt(response.headers['content-length'] || '0', 10); + let downloadedSize = 0; + + response.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize > 0) { + const percent = Math.round((downloadedSize / totalSize) * 100); + process.stdout.write(`\r Downloading: ${percent}%`); + } + }); + + pipeline(response, file).then(() => { + process.stdout.write('\n'); + resolve(); + }).catch(reject); + } else { + reject(new Error(`Failed to download: ${response.statusCode}`)); + } + }).on('error', reject); + }); +} + +async function extractArchive(archivePath: string, platform: Platform): Promise { + const tempDir = path.join(path.dirname(archivePath), 'temp'); + mkdirSync(tempDir, { recursive: true }); + + console.log(' Extracting archive...'); + + if (platform === 'win32') { + // Use unzip command (available on macOS) to extract zip files + execSync(`unzip -q "${archivePath}" -d "${tempDir}"`, { stdio: 'inherit' }); + } else { + // Use tar for Unix-like systems + execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: 'inherit' }); + } + + return tempDir; +} + +async function downloadNodeBinary(config: PlatformConfig): Promise { + const { platform, arch, url, binary } = config; + const platformDir = path.join(RESOURCES_DIR, `${platform}-${arch}`); + const binaryPath = path.join(platformDir, platform === 'win32' ? 'node.exe' : 'node'); + + // Skip if already exists + if (fs.existsSync(binaryPath)) { + console.log(`✓ ${platform}-${arch} binary already exists`); + return; + } + + console.log(`\nDownloading Node.js for ${platform}-${arch}...`); + + // Create directory + mkdirSync(platformDir, { recursive: true }); + + // Download archive + const archiveExt = platform === 'win32' ? '.zip' : '.tar.gz'; + const archivePath = path.join(platformDir, `node-v${NODE_VERSION}${archiveExt}`); + + try { + await downloadFile(url, archivePath); + console.log(' Download complete'); + + // Extract archive + const tempDir = await extractArchive(archivePath, platform); + + // Find the node binary in extracted files + // Windows uses different directory naming convention (win instead of win32) + const extractedDirName = platform === 'win32' + ? `node-v${NODE_VERSION}-win-${arch}` + : `node-v${NODE_VERSION}-${platform}-${arch}`; + const extractedBinaryPath = path.join(tempDir, extractedDirName, binary); + + // Verify binary exists + if (!fs.existsSync(extractedBinaryPath)) { + throw new Error(`Binary not found at expected path: ${extractedBinaryPath}`); + } + + // Copy binary to final location + console.log(' Installing binary...'); + fs.copyFileSync(extractedBinaryPath, binaryPath); + + // Make executable on Unix-like systems + if (platform !== 'win32') { + chmodSync(binaryPath, '755'); + } + + // Clean up + fs.rmSync(tempDir, { recursive: true, force: true }); + fs.unlinkSync(archivePath); + + console.log(`✓ Successfully installed ${platform}-${arch} binary`); + } catch (error) { + console.error(`✗ Failed to download ${platform}-${arch}:`, error instanceof Error ? error.message : error); + // Clean up on failure + if (fs.existsSync(archivePath)) { + fs.unlinkSync(archivePath); + } + throw error; + } +} + +function getCurrentPlatform(): PlatformConfig | undefined { + const currentPlatform = process.platform as string; + const currentArch = process.arch as string; + + return PLATFORMS.find(p => + p.platform === currentPlatform && + p.arch === currentArch + ); +} + +async function main() { + console.log(`Node.js Binary Downloader v${NODE_VERSION}`); + console.log('=====================================\n'); + + // Create base directory + mkdirSync(RESOURCES_DIR, { recursive: true }); + + if (downloadAll) { + console.log('Mode: Download all platforms\n'); + + // Download binaries for all platforms + let success = 0; + let failed = 0; + + for (const platform of PLATFORMS) { + try { + await downloadNodeBinary(platform); + success++; + } catch (error) { + failed++; + } + } + + console.log(`\nSummary: ${success} succeeded, ${failed} failed`); + if (failed > 0) { + process.exit(1); + } + } else { + console.log('Mode: Download current platform only\n'); + + // Download only for current platform + const currentPlatform = getCurrentPlatform(); + + if (!currentPlatform) { + console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`); + console.error(' Supported platforms:'); + PLATFORMS.forEach(p => { + console.error(` - ${p.platform}-${p.arch}`); + }); + process.exit(1); + } + + await downloadNodeBinary(currentPlatform); + } + + console.log('\nDone! Node.js binaries available at:', RESOURCES_DIR); +} + +// Run if called directly +if (require.main === module) { + main().catch((error) => { + console.error('\nFatal error:', error); + process.exit(1); + }); +} + +// Export for potential programmatic use +export { downloadNodeBinary, PLATFORMS, NODE_VERSION, getCurrentPlatform }; \ No newline at end of file diff --git a/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts b/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts index 70370ba..c0b1ff4 100644 --- a/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts +++ b/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts @@ -4,13 +4,30 @@ import { } from "../../core/pipeline-types"; import { logger } from "../../../main/logger"; import { ModelManagerService } from "../../../services/model-manager"; -import { Whisper } from "smart-whisper"; +import { Worker as JestWorker } from "jest-worker"; +import * as path from "path"; +import { app } from "electron"; + +interface WhisperWorkerMethods { + initializeModel(modelPath: string): Promise; + transcribeAudio( + aggregatedAudio: Float32Array, + options: { + language: string; + initial_prompt: string; + suppress_blank: boolean; + suppress_non_speech_tokens: boolean; + no_timestamps: boolean; + } + ): Promise; + dispose(): Promise; +} export class WhisperProvider implements TranscriptionProvider { readonly name = "whisper-local"; private modelManager: ModelManagerService; - private whisperInstance: Whisper | null = null; + private whisperWorker: (JestWorker & WhisperWorkerMethods) | null = null; // Frame aggregation state private frameBuffer: Float32Array[] = []; @@ -18,6 +35,30 @@ export class WhisperProvider implements TranscriptionProvider { private silenceFrameCount = 0; private lastSpeechTimestamp = 0; + private getNodeBinaryPath(): string { + const platform = process.platform; + const arch = process.arch; + const binaryName = platform === 'win32' ? 'node.exe' : 'node'; + + if (app.isPackaged) { + // In production, use the binary from resources + return path.join( + process.resourcesPath, + 'node-binaries', + `${platform}-${arch}`, + binaryName + ); + } else { + // In development, use the local binary + return path.join( + __dirname, + '../../resources/node-binaries', + `${platform}-${arch}`, + binaryName + ); + } + } + // Configuration private readonly FRAME_SIZE = 512; // 32ms at 16kHz private readonly MIN_SPEECH_DURATION_MS = 500; // Minimum speech duration to transcribe @@ -99,8 +140,8 @@ export class WhisperProvider implements TranscriptionProvider { ); // Transcribe using smart-whisper - if (!this.whisperInstance) { - throw new Error("Whisper instance is not initialized"); + if (!this.whisperWorker) { + throw new Error("Whisper worker is not initialized"); } // Generate initial prompt from vocabulary and recent context @@ -109,7 +150,7 @@ export class WhisperProvider implements TranscriptionProvider { aggregatedTranscription, ); - const { result } = await this.whisperInstance.transcribe( + const text = await this.whisperWorker.transcribeAudio( aggregatedAudio, { language: "auto", @@ -117,17 +158,9 @@ export class WhisperProvider implements TranscriptionProvider { suppress_blank: true, suppress_non_speech_tokens: true, no_timestamps: true, - }, + } ); - const transcription = await result; - - // Combine all transcription segments into a single string - const text = transcription - .map((segment) => segment.text) - .join(" ") - .trim(); - logger.transcription.debug( `Transcription completed, length: ${text.length}`, ); @@ -260,8 +293,31 @@ export class WhisperProvider implements TranscriptionProvider { } async initializeWhisper(): Promise { - if (this.whisperInstance) { - return; // Already initialized + if (!this.whisperWorker) { + // Initialize jest-worker with single worker process + // Determine the correct path for the worker script + const workerPath = app.isPackaged + ? path.join(__dirname, "whisper-worker.js") // In production, same directory as main.js + : path.join(process.cwd(), ".vite/build/whisper-worker.js"); // In development + + logger.transcription.info(`Initializing Whisper worker at: ${workerPath}`); + this.whisperWorker = new JestWorker( + workerPath, + { + exposedMethods: ["initializeModel", "transcribeAudio", "dispose"], + numWorkers: 1, + enableWorkerThreads: false, + forkOptions: { + execPath: this.getNodeBinaryPath(), + env: { + ...process.env, + GGML_METAL_PATH_RESOURCES: process.env.GGML_METAL_PATH_RESOURCES, + NODE_OPTIONS: "--max-old-space-size=8192" + }, + silent: false // Enable output from worker for debugging + } + } + ) as JestWorker & WhisperWorkerMethods; } const modelPath = await this.modelManager.getBestAvailableModelPath(); @@ -272,10 +328,7 @@ export class WhisperProvider implements TranscriptionProvider { } try { - const { Whisper } = await import("smart-whisper"); - this.whisperInstance = new Whisper(modelPath, { gpu: true }); - this.whisperInstance.load(); - logger.transcription.info(`Initialized with model: ${modelPath}`); + await this.whisperWorker.initializeModel(modelPath); } catch (error) { logger.transcription.error(`Failed to initialize:`, error); throw new Error(`Failed to initialize smart-whisper: ${error}`); @@ -284,14 +337,15 @@ export class WhisperProvider implements TranscriptionProvider { // Simple cleanup method async dispose(): Promise { - if (this.whisperInstance) { + if (this.whisperWorker) { try { - await this.whisperInstance.free(); - logger.transcription.debug("Instance freed"); + await this.whisperWorker.dispose(); + await this.whisperWorker.end(); // Terminate the worker process + logger.transcription.debug("Worker terminated"); } catch (error) { - logger.transcription.warn("Error freeing instance:", error); + logger.transcription.warn("Error disposing whisper worker:", error); } finally { - this.whisperInstance = null; + this.whisperWorker = null; } } diff --git a/apps/desktop/src/pipeline/providers/transcription/whisper-worker.ts b/apps/desktop/src/pipeline/providers/transcription/whisper-worker.ts new file mode 100644 index 0000000..3d8cf91 --- /dev/null +++ b/apps/desktop/src/pipeline/providers/transcription/whisper-worker.ts @@ -0,0 +1,68 @@ +// This file contains just the Whisper-specific operations that need to run in a separate process +import { Whisper } from "smart-whisper"; + +// Simple console-based logging for worker process +const logger = { + transcription: { + info: (message: string, ...args: any[]) => console.log(`[whisper-worker] INFO: ${message}`, ...args), + error: (message: string, ...args: any[]) => console.error(`[whisper-worker] ERROR: ${message}`, ...args), + debug: (message: string, ...args: any[]) => console.log(`[whisper-worker] DEBUG: ${message}`, ...args), + } +}; + +let whisperInstance: Whisper | null = null; +let currentModelPath: string | null = null; + +export async function initializeModel(modelPath: string): Promise { + if (whisperInstance && currentModelPath === modelPath) { + return; // Already initialized with same model + } + + // Cleanup existing instance + if (whisperInstance) { + await whisperInstance.free(); + whisperInstance = null; + } + + const { Whisper } = await import("smart-whisper"); + whisperInstance = new Whisper(modelPath, { gpu: true }); + try { + await whisperInstance.load(); + } catch (e) { + logger.transcription.error('Failed to load Whisper model:', e); + throw e; + } + currentModelPath = modelPath; + logger.transcription.info(`Initialized with model: ${modelPath}`); +} + +export async function transcribeAudio( + aggregatedAudio: Float32Array, + options: { + language: string; + initial_prompt: string; + suppress_blank: boolean; + suppress_non_speech_tokens: boolean; + no_timestamps: boolean; + } +): Promise { + if (!whisperInstance) { + throw new Error("Whisper instance is not initialized"); + } + + const { result } = await whisperInstance.transcribe(aggregatedAudio, options); + const transcription = await result; + + return transcription + .map((segment) => segment.text) + .join(" ") + .trim(); +} + +export async function dispose(): Promise { + if (whisperInstance) { + await whisperInstance.free(); + whisperInstance = null; + currentModelPath = null; + } +} \ No newline at end of file diff --git a/apps/desktop/src/services/vad-service.ts b/apps/desktop/src/services/vad-service.ts index fbc5c98..09279f1 100644 --- a/apps/desktop/src/services/vad-service.ts +++ b/apps/desktop/src/services/vad-service.ts @@ -32,14 +32,14 @@ export class VADService extends EventEmitter { // In production, the assets are copied to the resources folder this.modelPath = path.join( process.resourcesPath, - "assets", + "models", "silero_vad_v5.onnx", ); } else { // In development, use the source path this.modelPath = path.join( __dirname, - "../../src/assets/silero_vad_v5.onnx", + "../../resources/models/silero_vad_v5.onnx", ); } diff --git a/apps/desktop/vite.main.config.mts b/apps/desktop/vite.main.config.mts index e511152..53ac890 100644 --- a/apps/desktop/vite.main.config.mts +++ b/apps/desktop/vite.main.config.mts @@ -5,8 +5,16 @@ import { resolve } from "path"; export default defineConfig({ build: { rollupOptions: { + input: { + main: resolve(__dirname, "src/main/main.ts"), + "whisper-worker": resolve(__dirname, "src/pipeline/providers/transcription/whisper-worker.ts"), + }, + output: { + entryFileNames: "[name].js", + }, external: [ "smart-whisper", + "jest-worker", "@libsql/client", "@libsql/darwin-arm64", "@libsql/darwin-x64", diff --git a/package.json b/package.json index 7d6fcd8..7144166 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "turbo": "^2.5.3", "typescript": "5.8.2" }, - "packageManager": "pnpm@10.4.0", + "packageManager": "pnpm@10.13.1", "engines": { "node": ">=24" }, @@ -42,7 +42,8 @@ "@libsql", "macos-alias", "fs-xattr", - "onnxruntime-node" + "onnxruntime-node", + "jest-worker" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b68127..8664f94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@libsql/client': specifier: ^0.15.9 version: 0.15.9 + '@libsql/darwin-x64': + specifier: 0.5.13 + version: 0.5.13 '@openrouter/ai-sdk-provider': specifier: ^0.7.2 version: 0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67) @@ -203,6 +206,9 @@ importers: input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + jest-worker: + specifier: ^29.7.0 + version: 29.7.0 keytar: specifier: ^7.9.0 version: 7.9.0 @@ -1795,6 +1801,14 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -2817,6 +2831,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -3287,6 +3304,15 @@ packages: '@types/inquirer@6.5.0': resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3363,6 +3389,12 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -3841,6 +3873,10 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -5676,6 +5712,14 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -7557,6 +7601,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -9804,6 +9852,19 @@ snapshots: dependencies: minipass: 7.1.2 + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.15.12 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -9844,8 +9905,7 @@ snapshots: '@libsql/darwin-arm64@0.5.13': optional: true - '@libsql/darwin-x64@0.5.13': - optional: true + '@libsql/darwin-x64@0.5.13': {} '@libsql/hrana-client@0.7.0': dependencies: @@ -10896,6 +10956,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.27.8': {} + '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -11487,6 +11549,16 @@ snapshots: '@types/through': 0.0.33 rxjs: 6.6.7 + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -11562,6 +11634,12 @@ snapshots: dependencies: '@types/node': 22.15.12 + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.15.12 @@ -12119,6 +12197,8 @@ snapshots: chrome-trace-event@1.0.4: {} + ci-info@3.9.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -14326,6 +14406,22 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.12 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.15.12 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jiti@2.4.2: {} js-base64@3.7.7: {} @@ -16811,6 +16907,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} swap-case@1.1.2: