fix: move to jest-worker and pure node for whisper execution to escape issues with gpu buffer allocation due to electron restrictions

This commit is contained in:
haritabh-z01 2025-07-13 19:15:06 +05:30
parent feebe5cae4
commit 2818db8037
12 changed files with 611 additions and 34 deletions

View file

@ -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: {

View file

@ -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",

View file

@ -0,0 +1,4 @@
# Ignore downloaded binaries
*
!.gitignore
!README.md

View file

@ -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.

View file

@ -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<void> {
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<string> {
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<void> {
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 };

View file

@ -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<void>;
transcribeAudio(
aggregatedAudio: Float32Array,
options: {
language: string;
initial_prompt: string;
suppress_blank: boolean;
suppress_non_speech_tokens: boolean;
no_timestamps: boolean;
}
): Promise<string>;
dispose(): Promise<void>;
}
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<void> {
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<void> {
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;
}
}

View file

@ -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<void> {
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<string> {
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<void> {
if (whisperInstance) {
await whisperInstance.free();
whisperInstance = null;
currentModelPath = null;
}
}

View file

@ -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",
);
}

View file

@ -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",

View file

@ -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"
]
}
}

104
pnpm-lock.yaml generated
View file

@ -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: