diff --git a/windows-jp-ko-translator-transfer-20260504/.gitignore b/windows-jp-ko-translator-transfer-20260504/.gitignore new file mode 100644 index 0000000..2f2cae3 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +dist-electron/ +vendor/ +release/ +artifacts/ +.tmp*/ +*.zip diff --git a/windows-jp-ko-translator-transfer-20260504/JP-KO Translator.command b/windows-jp-ko-translator-transfer-20260504/JP-KO Translator.command new file mode 100644 index 0000000..35b5aec --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/JP-KO Translator.command @@ -0,0 +1,8 @@ +#!/bin/zsh +set -euo pipefail +cd "/Users/mingu/.openclaw/workspace-codingbot/windows-jp-ko-translator" +if [ ! -d node_modules ]; then + echo "Installing desktop dependencies..." + /usr/bin/env npm install +fi +/usr/bin/env npm start diff --git a/windows-jp-ko-translator-transfer-20260504/README.md b/windows-jp-ko-translator-transfer-20260504/README.md new file mode 100644 index 0000000..4457e4c --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/README.md @@ -0,0 +1,122 @@ +# JP-KO Translator + +Electron + React + TypeScript 기반의 일본어 → 한국어 데스크톱 번역기입니다. + +이번 정리에서 실제로 반영한 핵심은 아래 5가지입니다. + +- mac Apple Silicon 경로에 `distil-large-v3(faster-whisper/ct2)` 기반 로컬 STT 백엔드를 추가하고, 모델 + Python 런타임을 앱 리소스로 묶을 수 있게 정리 +- Windows 배포물에 `ffmpeg.exe + whisper.cpp 실행 파일 + ggml-base.bin 모델`을 함께 넣어 별도 STT 설치 없이 바로 실행되게 정리 +- 실사용 기본 조합을 `실사용 권장 + 자동 선택`으로 정리 +- Provider 카드를 누르면 실제 입력/저장이 되는 설정 모달과 연결 테스트가 뜨도록 변경 +- Codex auth 의존을 기본값에서 빼고, API 키 기반 provider가 있으면 자동으로 우선 선택되게 변경 + +## 실행 방법 + +### macOS + +- `JP-KO Translator.command` + +mac Apple Silicon 배포에서는 아래 준비 스크립트로 STT 런타임과 모델을 같이 넣을 수 있습니다. + +```bash +npm run prepare:stt:mac +``` + +이 스크립트는 아래를 준비합니다. + +- `vendor/python/darwin/arm64` : faster-whisper 실행용 Python 런타임 +- `vendor/models/darwin/arm64/distil-large-v3-ct2` : distil-large-v3 모델 + +### Windows exe bundle + +압축 해제 후 아래 파일을 실행합니다. + +- `JP-KO Translator.exe` + +이 번들은 Windows에서 아래 STT 도구를 같이 포함합니다. + +- `ffmpeg.exe` +- `whisper-cli.exe` +- `ggml-base.bin` + +즉, Windows 사용자는 ffmpeg나 whisper를 따로 설치할 필요가 없습니다. + +## 앱 안에서 바뀐 점 + +### 1) STT + +- mac Apple Silicon은 `ffmpeg + bundled Python + faster-whisper(distil-large-v3-ct2)`를 우선 사용합니다. +- mac 번들 안에 distil-large-v3 모델 디렉터리와 Python 런타임을 같이 넣을 수 있습니다. +- mac에서 위 리소스를 찾지 못하면 기존 `whisper/whisper.cpp` 탐색 경로로 자동 fallback 합니다. +- Windows 기본 배포물은 `WASAPI loopback + ffmpeg + whisper.cpp` 조합으로 동작합니다. +- 오디오 캡처와 전사 파일이 앱 번들에 같이 들어갑니다. +- 압축만 제대로 풀면 STT 도구를 따로 찾을 필요가 없습니다. +- 실제 흐름은 `Windows 기본 출력 장치 재생 -> WASAPI loopback 캡처 -> whisper.cpp 전사 -> 번역 provider` 입니다. +- 기본값에서는 현재 Windows 기본 출력 장치를 자동으로 따라가며, Stereo Mix/VB-Cable 같은 가상 장치를 따로 잡을 필요가 없습니다. + +### 2) UI 문구 + +메인/설정 흐름에서 아래 문구를 걷어냈습니다. + +- `로컬 데모` +- `설정 필요` + +대신 사용자는 아래처럼 보게 됩니다. + +- `기본 확인` +- `입력 필요` +- `로그인 필요` + +### 3) Provider 설정 UX + +주의: `기본 확인`은 실오디오/STT 흐름 점검용입니다. 실제 번역을 쓰려면 아래 provider 중 하나를 설정해야 합니다. + +가장 단순한 실사용 경로는 아래입니다. + +1. 설정 화면에서 `실행 모드 = 실사용 권장` +2. `번역 경로 = 자동 선택` +3. GPT API / Gemini / DeepL 중 하나의 키만 저장 +4. 설정 화면의 `첫 실행 기준` 카드에서 4개 체크가 채워졌는지 바로 확인 +5. `연결 테스트` 또는 `자동 선택 테스트`로 바로 확인 + +자동 선택 우선순위는 아래 순서입니다. + +- `GPT API` +- `Gemini` +- `DeepL` +- `Codex auth` + +즉, Codex auth는 기본 경로가 아니라 고급/대체 경로입니다. + +설정 화면에서 provider 카드를 누르면 실제 모달이 열립니다. + +- `자동 선택`: 현재 저장된 키 기준 자동 선택 테스트 +- `Codex auth`: Codex CLI 설치 + 로그인 페이지 열기 + auth 파일 경로 입력 + 모델 입력 + 연결 테스트 +- `GPT API`: API 키 / Base URL / 모델 입력 + 연결 테스트 +- `Gemini`: API 키 / Base URL / 모델 입력 + 연결 테스트 +- `DeepL`: API 키 / Base URL 입력 + 연결 테스트 + +저장하면 앱 전용 설정 파일에 보관되고, 다음 실행부터 바로 반영됩니다. + +## QA / Release + +```bash +npm run prepare:stt:mac +npm run typecheck +npm run build +npm run verify:runtime:report +npm run release:windows +``` + +Windows 산출물은 `release/` 아래에 생성됩니다. +런타임 smoke 결과는 `artifacts/runtime-verify-latest.json`에 남길 수 있습니다. + +## 현재 남는 리스크 + +- mac 실오디오 E2E는 입력 장치, 마이크 권한, 가상 오디오 라우팅이 준비되지 않으면 캡처 단계에서 막힙니다. +- distil-large-v3 런타임은 실제로 묶을 수 있게 바꿨지만, 리소스 크기가 커서 mac 배포물 용량이 꽤 증가합니다. +- faster-whisper 경로는 CPU int8 기준으로 붙여 두었고, Apple Silicon 최적화 전용 경로는 아직 아닙니다. +- Windows STT는 번들형으로 바꿨지만, 실제 인식 품질은 `ggml-base.bin` 모델 기준입니다. 더 높은 품질이 필요하면 더 큰 모델로 교체해야 합니다. +- Codex auth는 브라우저 로그인 진입만 앱에서 제공합니다. 실제 사용에는 해당 PC에 Codex CLI가 설치되어 있고 로그인 세션(auth.json)이 모두 준비돼 있어야 합니다. +- `기본 확인`은 실제 번역 엔진이 아니라 캡처/STT 점검용 경로라서, 원격 provider를 설정하지 않으면 실사용 번역기로 쓰기 어렵습니다. +- macOS는 여전히 시스템 오디오 라우팅 환경에 따라 입력 장치 구성이 달라질 수 있습니다. diff --git a/windows-jp-ko-translator-transfer-20260504/docs/PRACTICAL_ROLLOUT_PLAN.md b/windows-jp-ko-translator-transfer-20260504/docs/PRACTICAL_ROLLOUT_PLAN.md new file mode 100644 index 0000000..6d3510c --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/docs/PRACTICAL_ROLLOUT_PLAN.md @@ -0,0 +1,61 @@ +# Practical Rollout Plan + +## 실사용 기준 + +아래 4개를 만족하면 `조건부 가능`이 아니라 `실사용 가능`로 봅니다. + +1. 사용자가 Windows 압축본을 풀고 exe를 실행할 수 있다. +2. 일본어 오디오가 Windows 기본 출력 장치로 재생될 때 STT가 실제로 잡힌다. +3. 번역 provider를 한 번만 넣으면 다음 실행부터 별도 설명 없이 실제 한국어 번역이 바로 나온다. +4. 번역이 안 될 때 사용자는 `어디가 비었는지`를 앱 안에서 바로 알 수 있다. + +## 남은 갭 + +### 필수 + +- 기본 provider 전략이 `수동 선택` 중심이라 첫 설정 판단 비용이 높음 +- 저장 후 실제 번역 연결 확인 경로가 약함 +- Codex auth가 보여서 기본 경로처럼 오해될 수 있음 +- Windows 오디오 입력선 흐름이 사용 설명보다 앱 안 UX에서 덜 분명함 + +### 후순위 + +- 더 큰 whisper 모델 선택 UX +- 번역 라인 버퍼/지연 튜닝 +- 설치형 인스톨러/서명 배포 +- 실제 오디오 샘플 기반 회귀 QA 자동화 + +## 단계 1 / 2 / 3 + +### 단계 1 — 바로 번역이 붙는 기본선 만들기 + +- 기본 권장 조합을 `실사용 권장 + 자동 선택`으로 고정 +- 자동 선택 우선순위를 `GPT API → Gemini → DeepL → Codex auth`로 정리 +- Codex는 기본값이 아니라 대체/고급 경로로 위치 조정 +- 저장 후 provider 연결 테스트를 앱에서 바로 실행 가능하게 추가 + +### 단계 2 — Windows 실사용 흐름 명료화 + +- 앱 안에 `기본 출력 장치 재생 → WASAPI 캡처 → whisper 전사 → 번역 provider` 흐름을 노출 +- 오류 문구를 `압축 다시 풀기`, `API 키 필요`, `로그인 필요`처럼 행동 가능한 메시지로 유지 +- QA에서 실제 Windows 출력 장치 기준 캡처 확인 + +### 단계 3 — 배포 마감선 + +- Windows 실제 기기에서 exe bundle smoke test +- API 키 1개만 넣고 즉시 번역되는지 검증 +- release note/README를 `무설정 데모` 중심이 아니라 `최소 설정 실사용` 중심으로 재작성 + +## 지금 바로 반영한 것 + +- `자동 선택` provider 추가 +- 원격 번역 시작 기본값을 `auto`로 변경 +- 자동 선택 우선순위 구현: GPT API → Gemini → DeepL → Codex auth +- provider별 `연결 테스트`와 `자동 선택 테스트` 추가 +- 설정 화면에 `실사용 권장` 및 Windows 오디오 흐름 설명 추가 + +## 아직 사용자 도움이 필요한 것 + +- 실제 Windows PC에서 오디오가 기본 출력으로 재생되는 환경 확인 +- 어떤 기본 provider를 실제 주력으로 쓸지 결정용 키 1개 제공(OpenAI/Gemini/DeepL 중 하나) +- Codex를 계속 지원할지, 고급 옵션으로만 둘지 제품 방향 확정 diff --git a/windows-jp-ko-translator-transfer-20260504/docs/QA_RELEASE_CHECKLIST.md b/windows-jp-ko-translator-transfer-20260504/docs/QA_RELEASE_CHECKLIST.md new file mode 100644 index 0000000..bafc1b8 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/docs/QA_RELEASE_CHECKLIST.md @@ -0,0 +1,19 @@ +# JP-KO Translator QA Checklist + +## 이번 정리에서 확인한 항목 + +- 개발자용 문구 `로컬 데모`, `설정 필요` 제거 후 `기본 확인 / 입력 필요 / 로그인 필요`로 정리 +- provider 카드 클릭 시 실제 모달 열림 +- API 키 / Base URL / 모델 저장 가능 +- Codex auth 로그인 진입 버튼 추가 +- `기본 확인`은 실오디오/STT 점검용이고, 실제 번역은 원격 provider 설정이 필요함을 문구로 명시 +- Windows 배포물에 `ffmpeg.exe`, `whisper-cli.exe`, `ggml-base.bin` 포함 +- `npm run typecheck` +- `npm run build` +- `npm run release:windows` + +## 현재 남는 리스크 + +- Windows STT는 번들형으로 바뀌었지만 기본 모델이 `ggml-base.bin`이라 고품질 모델보다 인식 품질이 낮을 수 있음 +- Codex auth는 브라우저 로그인 진입을 제공하지만 실제 사용 여부는 로컬 세션 상태에 의존함 +- macOS는 시스템 오디오 라우팅 환경에 따라 입력 장치 설정이 달라질 수 있음 diff --git a/windows-jp-ko-translator-transfer-20260504/docs/WINDOWS_RELEASE_GUIDE.md b/windows-jp-ko-translator-transfer-20260504/docs/WINDOWS_RELEASE_GUIDE.md new file mode 100644 index 0000000..6e7660d --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/docs/WINDOWS_RELEASE_GUIDE.md @@ -0,0 +1,67 @@ +# Windows release guide + +Windows 기본 배포물은 이제 실행 파일만 있는 껍데기가 아닙니다. + +압축을 풀면 아래가 같이 들어 있습니다. + +- `JP-KO Translator.exe` +- `resources/bin/win32/x64/ffmpeg.exe` +- `resources/bin/win32/x64/whisper-cli.exe` +- `resources/bin/win32/x64/ggml-base.bin` + +즉, Windows 사용자는 ffmpeg / whisper를 따로 설치하지 않아도 됩니다. + +## 사용 방법 + +1. zip을 **완전히** 압축 해제합니다. +2. `JP-KO Translator.exe`를 실행합니다. +3. 설정 화면에서 `첫 실행 기준` 카드가 보이는지 확인합니다. +4. provider 카드를 눌러 키 또는 로그인 정보를 입력합니다. +5. `API 키 1개 저장`과 `실사용 조합 유지`가 완료로 바뀌는지 봅니다. +6. 저장 후 `연결 테스트` 또는 `자동 선택 테스트`를 한 번 실행합니다. +7. 시작합니다. + +## Provider 설정 흐름 + +앱 안에서 직접 설정할 수 있습니다. + +- `Codex auth` + - Codex CLI 설치 + - 로그인 페이지 열기 + - auth 파일 경로 입력 + - 모델 입력 +- `GPT API` + - API 키 / Base URL / 모델 입력 +- `Gemini` + - API 키 / Base URL / 모델 입력 +- `DeepL` + - API 키 / Base URL 입력 + +예전처럼 카드에 `설정 필요`만 보이고 끝나지 않습니다. + +`기본 확인`은 실오디오/STT 경로만 점검하는 내장 확인 모드입니다. 실제 번역을 사용하려면 GPT API, Gemini, DeepL, Codex auth 중 하나를 설정해야 합니다. + +## STT 동작 방식 + +- 오디오 캡처: `ffmpeg.exe` +- 음성 인식: `whisper.cpp` +- 기본 모델: `ggml-base.bin` +- 입력 소스: Windows WASAPI loopback +- 기본값은 현재 Windows 기본 출력 장치를 자동으로 따라가므로, Stereo Mix/VB-Cable 같은 가상 장치를 따로 만들 필요가 없습니다. + +## smoke 확인용 명령 + +```bash +npm run verify:runtime:report +``` + +성공/실패와 provider probe, capture 준비 상태, end-to-end 요약은 `artifacts/runtime-verify-latest.json`에 남습니다. + +non-Windows 호스트에서 이 명령을 돌릴 때 Windows용 번들 모델만 들어 있는 경우에는, 이제 `Windows에서 직접 실행해 확인` 안내로 분리해 표시합니다. 이 경우는 앱 결함이라기보다 호스트 불일치입니다. + +## 현재 남는 제한 + +- 기본 모델은 `ggml-base.bin`이라 더 큰 모델보다 정확도가 낮을 수 있습니다. +- Codex auth는 로그인 진입만 앱에서 열어줍니다. 실제 사용에는 로컬 PC에 Codex CLI 설치와 로그인 세션(auth.json)이 모두 필요합니다. +- `기본 확인`은 실제 번역 엔진이 아니라 캡처/STT 점검용 경로입니다. +- Apple Silicon macOS에서 Windows용 `portable` 단일 exe 패키징은 여전히 제한이 있어서, 현재 주 산출물은 exe bundle zip입니다. diff --git a/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/main.ts b/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/main.ts new file mode 100644 index 0000000..3edc1f5 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/main.ts @@ -0,0 +1,149 @@ +import { app, BrowserWindow, ipcMain } from 'electron'; + +let helperWindow: BrowserWindow | null = null; +import { mkdir, rename, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const segmentDirectory = process.env.MACOS_SYSTEM_AUDIO_SEGMENT_DIR?.trim(); +const timesliceMs = Number.parseInt(process.env.MACOS_SYSTEM_AUDIO_TIMESLICE_MS ?? '', 10); + +function emit(event: string, payload: Record = {}) { + console.log(JSON.stringify({ source: 'mac-system-audio-helper', event, timestamp: new Date().toISOString(), ...payload })); +} + +async function writeSegment(index: number, buffer: Buffer) { + if (!segmentDirectory) { + throw new Error('Segment directory is missing.'); + } + + await mkdir(segmentDirectory, { recursive: true }); + const filePath = path.join(segmentDirectory, `chunk-${String(index).padStart(5, '0')}.webm`); + const tempPath = `${filePath}.part`; + await writeFile(tempPath, buffer); + await rename(tempPath, filePath); + return filePath; +} + +async function createWindow() { + const preloadPath = path.join(__dirname, 'preload.js'); + const win = new BrowserWindow({ + show: true, + width: 440, + height: 180, + resizable: false, + maximizable: false, + minimizable: false, + fullscreenable: false, + autoHideMenuBar: true, + title: 'JP-KO Translator System Audio Helper', + webPreferences: { + preload: preloadPath, + contextIsolation: true, + sandbox: false, + backgroundThrottling: false + } + }); + helperWindow = win; + + emit('window-created', { + preloadPath, + url: win.webContents.getURL() || null + }); + + win.webContents.on('did-finish-load', () => { + emit('did-finish-load', { + url: win.webContents.getURL() + }); + }); + + win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { + emit('error', { + phase: 'did-fail-load', + errorCode, + errorDescription, + validatedURL + }); + }); + + win.webContents.on('console-message', (_event, level, message, line, sourceId) => { + emit('renderer-console', { + level, + message, + line, + sourceId + }); + }); + + win.webContents.on('render-process-gone', (_event, details) => { + emit('error', { + phase: 'render-process-gone', + details + }); + }); + + ipcMain.handle('mac-system-audio:write-segment', async (_event, payload: { index: number; buffer: Uint8Array }) => { + const filePath = await writeSegment(payload.index, Buffer.from(payload.buffer)); + emit('segment-written', { index: payload.index, filePath }); + return { ok: true, filePath }; + }); + + ipcMain.on('mac-system-audio:status', (_event, payload: Record) => { + emit('status', payload); + if (payload.phase === 'ready') { + win.hide(); + } + }); + + ipcMain.on('mac-system-audio:error', (_event, payload: Record) => { + emit('error', payload); + }); + + const html = '

macOS 시스템 오디오 연결 중…

처음 한 번은 macOS 화면 및 시스템 오디오 권한 승인 또는 캡처 선택 창이 나타날 수 있습니다. 승인되면 이 창은 자동으로 숨겨집니다.

'; + const helperHtmlDirectory = segmentDirectory ?? app.getPath('temp'); + await mkdir(helperHtmlDirectory, { recursive: true }); + const helperHtmlPath = path.join(helperHtmlDirectory, 'mac-system-audio-helper.html'); + await writeFile(helperHtmlPath, html, 'utf8'); + const helperUrl = pathToFileURL(helperHtmlPath); + helperUrl.searchParams.set('timesliceMs', String(Number.isFinite(timesliceMs) && timesliceMs >= 500 ? timesliceMs : 4000)); + emit('loading-url', { + helperUrl: helperUrl.toString() + }); + await win.loadURL(helperUrl.toString()); +} + +app.commandLine.appendSwitch('enable-features', 'MacLoopbackAudioForScreenShare'); +app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); + +void app.whenReady().then(async () => { + if (!segmentDirectory) { + emit('fatal', { message: 'MACOS_SYSTEM_AUDIO_SEGMENT_DIR is missing.' }); + app.exit(1); + return; + } + + emit('boot', { + segmentDirectory, + timesliceMs: Number.isFinite(timesliceMs) && timesliceMs >= 500 ? timesliceMs : 4000 + }); + + try { + await createWindow(); + } catch (error) { + emit('fatal', { + message: error instanceof Error ? error.message : String(error) + }); + app.exit(1); + } +}); + +app.on('window-all-closed', () => { + // Keep helper alive until parent stops it. +}); + +app.on('before-quit', () => { + helperWindow = null; +}); diff --git a/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/preload.ts b/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/preload.ts new file mode 100644 index 0000000..057346b --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/mac-capture-helper/preload.ts @@ -0,0 +1,170 @@ +import { ipcRenderer } from 'electron'; + +function emit(channel: 'mac-system-audio:status' | 'mac-system-audio:error', payload: Record) { + ipcRenderer.send(channel, { + ...payload, + timestamp: new Date().toISOString() + }); +} + +let activeStream: MediaStream | null = null; +let activeRecorder: MediaRecorder | null = null; +let startupWatchdog: number | null = null; + +function clearStartupWatchdog() { + if (startupWatchdog !== null) { + window.clearTimeout(startupWatchdog); + startupWatchdog = null; + } +} + +function stopActiveCapture() { + clearStartupWatchdog(); + activeRecorder?.stop(); + activeRecorder = null; + activeStream?.getTracks().forEach((track) => track.stop()); + activeStream = null; +} + +async function startCapture() { + try { + const timesliceMs = Number.parseInt(new URLSearchParams(window.location.search).get('timesliceMs') ?? '', 10); + startupWatchdog = window.setTimeout(() => { + emit('mac-system-audio:error', { + phase: 'startup-timeout', + message: 'ScreenCaptureKit system audio capture did not start within 10 seconds. The helper window may need to stay visible until macOS permission prompts or the capture sheet are accepted.' + }); + }, 10_000); + const stream = await navigator.mediaDevices.getDisplayMedia({ + video: { + frameRate: 1, + width: { ideal: 2, max: 8 }, + height: { ideal: 2, max: 8 } + } as MediaTrackConstraints, + audio: true + }); + + activeStream = stream; + clearStartupWatchdog(); + const audioTracks = stream.getAudioTracks(); + const videoTracks = stream.getVideoTracks(); + + emit('mac-system-audio:status', { + phase: 'stream-opened', + audioTrackCount: audioTracks.length, + videoTrackCount: videoTracks.length, + audioTrackLabels: audioTracks.map((track) => track.label), + videoTrackLabels: videoTracks.map((track) => track.label) + }); + + if (audioTracks.length === 0) { + throw new Error('ScreenCaptureKit stream opened but no system audio track was attached.'); + } + + const supportedMimeType = [ + 'video/webm;codecs=vp8,opus', + 'video/webm', + 'audio/webm;codecs=opus', + 'audio/webm' + ].find((candidate) => MediaRecorder.isTypeSupported(candidate)); + + let index = 0; + let readyEmitted = false; + const recorder = new MediaRecorder(stream, supportedMimeType ? { + mimeType: supportedMimeType, + audioBitsPerSecond: 128000, + videoBitsPerSecond: 32000 + } : { + audioBitsPerSecond: 128000, + videoBitsPerSecond: 32000 + }); + activeRecorder = recorder; + + recorder.addEventListener('start', () => { + emit('mac-system-audio:status', { + phase: 'capture-started', + backendLabel: 'macOS ScreenCaptureKit Audio', + deviceLabel: audioTracks[0]?.label || 'System Audio', + mimeType: recorder.mimeType || supportedMimeType || 'default', + keepingVideoTrackAlive: videoTracks.length > 0 + }); + }); + + recorder.addEventListener('dataavailable', async (event) => { + emit('mac-system-audio:status', { + phase: 'dataavailable', + index, + size: event.data?.size ?? 0, + recorderState: recorder.state + }); + + if (!event.data || event.data.size === 0) { + return; + } + + const arrayBuffer = await event.data.arrayBuffer(); + await ipcRenderer.invoke('mac-system-audio:write-segment', { + index, + buffer: Array.from(new Uint8Array(arrayBuffer)) + }); + if (!readyEmitted) { + readyEmitted = true; + emit('mac-system-audio:status', { + phase: 'ready', + backendLabel: 'macOS ScreenCaptureKit Audio', + deviceLabel: audioTracks[0]?.label || 'System Audio', + mimeType: recorder.mimeType || supportedMimeType || 'default', + keepingVideoTrackAlive: videoTracks.length > 0, + firstChunkIndex: index, + firstChunkBytes: event.data.size + }); + } + index += 1; + }); + + recorder.addEventListener('stop', () => { + emit('mac-system-audio:status', { + phase: 'recorder-stopped', + recorderState: recorder.state + }); + }); + + recorder.addEventListener('error', (event) => { + const message = event.error?.message || 'MediaRecorder failed.'; + emit('mac-system-audio:error', { + phase: 'recording-error', + message + }); + }); + + for (const track of [...audioTracks, ...videoTracks]) { + track.addEventListener('ended', () => { + emit('mac-system-audio:error', { + phase: 'track-ended', + kind: track.kind, + label: track.label, + readyState: track.readyState, + message: `${track.kind} track ended.` + }); + }); + } + + recorder.start(Number.isFinite(timesliceMs) && timesliceMs >= 500 ? timesliceMs : 4000); + } catch (error) { + clearStartupWatchdog(); + emit('mac-system-audio:error', { + phase: 'startup-error', + message: error instanceof Error ? error.message : String(error), + name: error instanceof Error ? error.name : null, + stack: error instanceof Error ? error.stack : null + }); + } +} + +window.addEventListener('beforeunload', () => { + stopActiveCapture(); +}); + +window.addEventListener('DOMContentLoaded', () => { + void startCapture(); +}); diff --git a/windows-jp-ko-translator-transfer-20260504/electron/main/config-store.ts b/windows-jp-ko-translator-transfer-20260504/electron/main/config-store.ts new file mode 100644 index 0000000..319575b --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/main/config-store.ts @@ -0,0 +1,114 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +export interface ProviderConfigValues { + chatgptApiKey: string; + openaiBaseUrl: string; + chatgptModel: string; + geminiApiKey: string; + geminiBaseUrl: string; + geminiModel: string; + deeplApiKey: string; + deeplBaseUrl: string; + codexModel: string; + macosCaptureDevice: string; + windowsCaptureDevice: string; + ffmpegPath: string; + whisperPath: string; + whisperModelPath: string; + userContext: string; + glossary: string; +} + +export interface ProviderConfigState { + values: ProviderConfigValues; + codexAuthPath: string; +} + +const defaultValues: ProviderConfigValues = { + chatgptApiKey: '', + openaiBaseUrl: '', + chatgptModel: '', + geminiApiKey: '', + geminiBaseUrl: '', + geminiModel: '', + deeplApiKey: '', + deeplBaseUrl: '', + codexModel: '', + macosCaptureDevice: '', + windowsCaptureDevice: '', + ffmpegPath: '', + whisperPath: '', + whisperModelPath: '', + userContext: '', + glossary: '' +}; + +export function createDefaultProviderConfigState(): ProviderConfigState { + return { + values: { ...defaultValues }, + codexAuthPath: path.join(os.homedir(), '.codex', 'auth.json') + }; +} + +export async function loadProviderConfigState(userDataPath: string): Promise { + const target = path.join(userDataPath, 'provider-config.json'); + try { + const raw = await readFile(target, 'utf8'); + const parsed = JSON.parse(raw) as Partial & { values?: Partial }; + return { + codexAuthPath: parsed.codexAuthPath || createDefaultProviderConfigState().codexAuthPath, + values: { + ...defaultValues, + ...(parsed.values || {}) + } + }; + } catch { + return createDefaultProviderConfigState(); + } +} + +export async function saveProviderConfigState(userDataPath: string, next: ProviderConfigState): Promise { + const target = path.join(userDataPath, 'provider-config.json'); + await mkdir(path.dirname(target), { recursive: true }); + const normalized: ProviderConfigState = { + codexAuthPath: next.codexAuthPath || createDefaultProviderConfigState().codexAuthPath, + values: { + ...defaultValues, + ...next.values + } + }; + await writeFile(target, JSON.stringify(normalized, null, 2), 'utf8'); + return normalized; +} + +export function buildWorkerEnv(config: ProviderConfigState) { + const nextEnv: Record = {}; + const setIf = (key: string, value: string) => { + if (value.trim()) { + nextEnv[key] = value.trim(); + } + }; + + setIf('CHATGPT_API_KEY', config.values.chatgptApiKey); + setIf('OPENAI_API_KEY', config.values.chatgptApiKey); + setIf('OPENAI_BASE_URL', config.values.openaiBaseUrl); + setIf('CHATGPT_TRANSLATION_MODEL', config.values.chatgptModel); + setIf('GEMINI_API_KEY', config.values.geminiApiKey); + setIf('GEMINI_BASE_URL', config.values.geminiBaseUrl); + setIf('GEMINI_TRANSLATION_MODEL', config.values.geminiModel); + setIf('DEEPL_API_KEY', config.values.deeplApiKey); + setIf('DEEPL_BASE_URL', config.values.deeplBaseUrl); + setIf('CODEX_TRANSLATION_MODEL', config.values.codexModel); + setIf('CODEX_AUTH_PATH', config.codexAuthPath); + setIf('MACOS_TRANSLATOR_CAPTURE_DEVICE', config.values.macosCaptureDevice); + setIf('WINDOWS_TRANSLATOR_CAPTURE_DEVICE', config.values.windowsCaptureDevice); + setIf('FFMPEG_PATH', config.values.ffmpegPath); + setIf('WHISPER_PATH', config.values.whisperPath); + setIf('WHISPER_MODEL_PATH', config.values.whisperModelPath); + setIf('TRANSLATOR_USER_CONTEXT', config.values.userContext); + setIf('TRANSLATOR_GLOSSARY', config.values.glossary); + + return nextEnv; +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/main/main.ts b/windows-jp-ko-translator-transfer-20260504/electron/main/main.ts new file mode 100644 index 0000000..9657983 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/main/main.ts @@ -0,0 +1,813 @@ +import { app, BrowserWindow, desktopCapturer, ipcMain, session, shell, systemPreferences, dialog } from 'electron'; +import { promises as fs } from 'node:fs'; +import { execFile, spawn, type ChildProcess } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createCaptureSnapshot } from '../shared/capture-backends.js'; +import { createDebugLogger, type DebugLogger } from '../shared/debug-log.js'; +import type { RuntimeMode, TranslationProviderId, WorkerSnapshot } from '../shared/runtime-contract.js'; +import { runTranslationProbe } from '../worker/runtime-core.js'; +import { + buildWorkerEnv, + createDefaultProviderConfigState, + loadProviderConfigState, + saveProviderConfigState, + type ProviderConfigState +} from './config-store.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const rendererIndex = path.join(__dirname, '../../dist/index.html'); +const workerEntry = path.join(__dirname, '../worker/runtime-worker.js'); + +let mainWindow: BrowserWindow | null = null; +let runtimeWorker: ChildProcess | null = null; +let debugLogger: DebugLogger | null = null; +let providerConfigState: ProviderConfigState = createDefaultProviderConfigState(); +let lastRequestedMode: RuntimeMode = 'local'; +let lastRequestedProvider: TranslationProviderId = 'auto'; +let expectedWorkerExit = false; + +type MicrophoneAccessStatus = ReturnType; + +function getMicrophonePermissionContext() { + const appPath = app.getAppPath(); + const execPath = process.execPath; + const resourcesPath = process.resourcesPath; + const looksLikePackagedApp = execPath.endsWith('.app/Contents/MacOS/Electron') || execPath.includes('.app/Contents/MacOS/'); + + return { + isPackaged: app.isPackaged, + defaultApp: (app as typeof app & { defaultApp?: boolean }).defaultApp ?? false, + appName: app.getName(), + appPath, + execPath, + resourcesPath, + looksLikePackagedApp, + electronRunAsNode: process.env.ELECTRON_RUN_AS_NODE ?? null, + argv: process.argv, + pid: process.pid + }; +} + +async function readMacInfoPlistUsageDescription() { + if (process.platform !== 'darwin') { + return null; + } + + const candidates = [ + path.join(process.execPath, '..', '../Info.plist'), + path.join(process.resourcesPath, '../Info.plist') + ]; + + for (const candidate of candidates) { + try { + const content = await fs.readFile(candidate, 'utf8'); + const match = content.match(/NSMicrophoneUsageDescription<\/key>\s*([\s\S]*?)<\/string>/); + return { + path: candidate, + found: Boolean(match), + value: match?.[1] ?? null + }; + } catch { + continue; + } + } + + return { + path: null, + found: false, + value: null + }; +} + +async function debug(event: string, payload: Record = {}) { + if (!debugLogger) { + return; + } + await debugLogger.log(event, payload); +} + +async function openMacMicrophonePrivacySettings() { + if (process.platform !== 'darwin') { + return false; + } + + try { + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'); + return true; + } catch { + const child = spawn('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'], { + detached: true, + stdio: 'ignore' + }); + child.unref(); + return true; + } +} + +async function openMacScreenCapturePrivacySettings() { + if (process.platform !== 'darwin') { + return false; + } + + try { + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); + return true; + } catch { + const child = spawn('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'], { + detached: true, + stdio: 'ignore' + }); + child.unref(); + return true; + } +} + +async function resolveCodexCliForLogin() { + const candidates = process.platform === 'win32' + ? [ + path.join(app.getPath('home'), '.codex', 'bin', 'codex.exe'), + path.join(app.getPath('home'), 'AppData', 'Local', 'OpenAI', 'Codex', 'bin', 'codex.exe') + ] + : []; + + const lookupCommand = process.platform === 'win32' ? 'where' : 'which'; + const lookup = await new Promise((resolve) => { + execFile(lookupCommand, ['codex'], { windowsHide: true }, (_error, stdout) => { + resolve(stdout.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean)); + }); + }).catch(() => []); + + candidates.push(...lookup); + + const normalizedCandidates = [...new Set(candidates)] + .filter((candidate) => process.platform !== 'win32' || path.extname(candidate).toLowerCase() === '.exe') + .sort((left, right) => { + const leftWindowsApps = process.platform === 'win32' && left.includes(`${path.sep}WindowsApps${path.sep}`); + const rightWindowsApps = process.platform === 'win32' && right.includes(`${path.sep}WindowsApps${path.sep}`); + return Number(leftWindowsApps) - Number(rightWindowsApps); + }); + + for (const candidate of normalizedCandidates) { + if (process.platform === 'win32' && candidate.includes(`${path.sep}WindowsApps${path.sep}`)) { + const copiedPath = path.join(app.getPath('home'), '.codex', 'bin', 'codex.exe'); + try { + await fs.mkdir(path.dirname(copiedPath), { recursive: true }); + await fs.copyFile(candidate, copiedPath); + await new Promise((resolve, reject) => { + execFile(copiedPath, ['--version'], { windowsHide: true }, (error) => { + if (error) reject(error); + else resolve(); + }); + }); + return copiedPath; + } catch { + continue; + } + } + + try { + await fs.access(candidate); + await new Promise((resolve, reject) => { + execFile(candidate, ['--version'], { windowsHide: true }, (error) => { + if (error) reject(error); + else resolve(); + }); + }); + return candidate; + } catch { + continue; + } + } + + return null; +} + +function launchCodexLoginTerminal(codexCliPath: string) { + const codexHome = path.join(app.getPath('home'), '.codex'); + const command = [ + `$env:CODEX_HOME = ${JSON.stringify(codexHome)}`, + 'Write-Host "Codex ChatGPT device login"', + 'Write-Host "1. A browser page will open at https://auth.openai.com/codex/device"', + 'Write-Host "2. Enter the one-time code shown below"', + 'Write-Host ""', + `& ${JSON.stringify(codexCliPath)} login --device-auth`, + 'Write-Host ""', + 'Write-Host "Codex login command finished. You can close this window after the browser login is complete."' + ].join('; '); + const encodedCommand = Buffer.from(command, 'utf16le').toString('base64'); + const child = spawn('powershell.exe', ['-NoExit', '-EncodedCommand', encodedCommand], { + detached: true, + stdio: 'ignore', + windowsHide: false + }); + child.unref(); +} + +async function getCodexLoginStatus(codexCliPath: string) { + const codexHome = path.join(app.getPath('home'), '.codex'); + return new Promise<{ loggedIn: boolean; detail: string }>((resolve) => { + execFile(codexCliPath, ['login', 'status'], { + windowsHide: true, + env: { + ...process.env, + CODEX_HOME: codexHome + } + }, (error, stdout, stderr) => { + const output = `${stdout}${stderr}`.trim(); + if (!error && /logged in/i.test(output)) { + resolve({ loggedIn: true, detail: output }); + return; + } + resolve({ loggedIn: false, detail: output || (error ? error.message : 'Codex login status is unknown.') }); + }); + }); +} + +function buildMicrophonePermissionDetail(status: MicrophoneAccessStatus, promptAttempted: boolean) { + switch (status) { + case 'granted': + return '마이크 권한이 허용되었습니다. 앱을 다시 시작하면 장치 확인이 바로 이어집니다.'; + case 'denied': + return '마이크 권한이 거부되어 있습니다. 시스템 설정의 개인정보 보호 > 마이크에서 이 앱을 직접 허용해야 합니다.'; + case 'restricted': + return '이 Mac 정책 때문에 마이크 권한을 바꿀 수 없습니다. 시스템 설정 또는 관리자 정책을 확인해야 합니다.'; + case 'not-determined': + return promptAttempted + ? '마이크 권한 요청을 보냈지만 macOS 승인 창이 나타나지 않았습니다. 개발 실행 상태이거나 앱 번들의 권한 설명이 없으면 이런 현상이 날 수 있습니다.' + : '마이크 권한을 아직 요청하지 않았습니다.'; + default: + return `마이크 권한 상태: ${status}`; + } +} + +let workerSnapshot: WorkerSnapshot = { + phase: 'idle', + mode: 'local', + sourceLanguage: 'ja', + targetLanguage: 'ko', + transcriptSource: process.platform === 'win32' || process.platform === 'darwin' ? 'live-capture' : 'fixture', + transcriptSourceLabel: process.platform === 'win32' || process.platform === 'darwin' ? '실시간 오디오 입력' : '샘플 입력', + liveCaptureImplemented: process.platform === 'win32' || process.platform === 'darwin', + providerSummary: { + stt: process.platform === 'win32' || process.platform === 'darwin' ? 'live-capture.ffmpeg+whisper' : 'fixture.transcript-feed', + translation: 'local.fixture-check-only' + }, + translationRoute: { + selected: 'auto', + active: 'local-fixture', + fallbackUsed: false, + selectedLabel: '자동 선택', + activeLabel: '기본 확인', + detail: '설정된 번역 provider가 있으면 자동 선택하고, 없으면 기본 확인으로 대기합니다.', + providerStatuses: [] + }, + modeDescriptor: { + label: '기본 확인', + description: '실오디오 입력을 로컬 캡처와 로컬 전사로 받아 빠르게 확인하는 모드입니다.', + localFirst: true, + cloudEnabled: false, + requiresCredentials: false + }, + capture: createCaptureSnapshot(process.platform), + startedAt: null, + lastError: null, + retryCount: 0, + healthMessage: '실행 대기 중입니다.', + logPath: null, + debugLogPath: null, + logDirectory: null, + logPathDetail: null, + routeLabel: '실오디오 입력 + 기본 확인', + lines: [] +}; + +function broadcastSnapshot() { + if (mainWindow) { + mainWindow.webContents.send('worker:status', workerSnapshot); + } + void debug('main.snapshot.broadcast', { + phase: workerSnapshot.phase, + mode: workerSnapshot.mode, + retryCount: workerSnapshot.retryCount, + capturePhase: workerSnapshot.capture.phase, + lineCount: workerSnapshot.lines.length, + lastError: workerSnapshot.lastError + }); +} + +function attachWorkerListeners(worker: ChildProcess) { + worker.on('message', (message: { type?: string; payload?: WorkerSnapshot }) => { + void debug('main.worker.message', { + messageType: message.type ?? 'unknown', + hasPayload: Boolean(message.payload) + }); + if (message.type === 'status' && message.payload) { + workerSnapshot = message.payload; + broadcastSnapshot(); + } + }); + + worker.on('disconnect', () => { + void debug('main.worker.disconnect', { + pid: worker.pid ?? null, + expectedWorkerExit + }); + }); + + worker.on('spawn', () => { + void debug('main.worker.spawn', { + pid: worker.pid ?? null + }); + }); + + worker.on('exit', (code, signal) => { + const shouldRespawn = + !expectedWorkerExit && Boolean(mainWindow) && (workerSnapshot.phase === 'starting' || workerSnapshot.phase === 'running'); + + void debug('main.worker.exit', { + code, + signal, + expectedWorkerExit, + shouldRespawn, + phase: workerSnapshot.phase, + mode: workerSnapshot.mode, + provider: lastRequestedProvider + }); + + runtimeWorker = null; + + if (shouldRespawn) { + workerSnapshot = { + ...workerSnapshot, + phase: 'starting', + lastError: signal + ? `Worker exited unexpectedly via ${signal}; restarting runtime.` + : `Worker exited unexpectedly with code ${code}; restarting runtime.` + }; + broadcastSnapshot(); + void ensureWorker(lastRequestedMode, lastRequestedProvider); + return; + } + + workerSnapshot = { + ...workerSnapshot, + phase: code === 0 || expectedWorkerExit ? 'idle' : 'error', + lastError: + code === 0 || expectedWorkerExit + ? null + : signal + ? `Worker exited via ${signal}` + : `Worker exited with code ${code}` + }; + expectedWorkerExit = false; + broadcastSnapshot(); + }); + + worker.on('error', (error) => { + void debug('main.worker.error', { error }); + workerSnapshot = { + ...workerSnapshot, + phase: 'error', + lastError: error.message + }; + broadcastSnapshot(); + }); +} + +function spawnRuntimeWorker() { + expectedWorkerExit = false; + const worker = spawn(process.execPath, [workerEntry], { + env: { + ...process.env, + ...buildWorkerEnv(providerConfigState), + ...(process.platform === 'win32' ? { TRANSLATOR_STT_BACKEND: 'whisper-stream' } : {}), + ELECTRON_RUN_AS_NODE: '1' + }, + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }); + + worker.stdout?.on('data', (chunk) => { + void debug('main.worker.stdout', { chunk: String(chunk).trim() }); + }); + worker.stderr?.on('data', (chunk) => { + void debug('main.worker.stderr', { chunk: String(chunk).trim() }); + }); + + attachWorkerListeners(worker); + void debug('main.worker.spawned', { + pid: worker.pid ?? null, + workerEntry, + execPath: process.execPath, + electronRunAsNode: '1' + }); + + return worker; +} + +async function ensureWorker(mode: RuntimeMode, provider: TranslationProviderId) { + lastRequestedMode = mode; + lastRequestedProvider = provider; + + if (!runtimeWorker || runtimeWorker.killed) { + runtimeWorker = spawnRuntimeWorker(); + } + + workerSnapshot = { + ...workerSnapshot, + phase: 'starting', + mode, + translationRoute: { + ...workerSnapshot.translationRoute, + selected: provider, + active: provider + }, + lastError: null + }; + broadcastSnapshot(); + + const { path: logDirectory, reason: logReason } = await resolveLogDirectory(); + runtimeWorker.send({ + type: 'start', + mode, + provider, + logDirectory, + debugLogDirectory: logDirectory + }); + + await debug('main.worker.start.sent', { + mode, + provider, + logDirectory, + logReason + }); + + return workerSnapshot; +} + +async function stopWorker() { + expectedWorkerExit = true; + + if (!runtimeWorker || runtimeWorker.killed) { + workerSnapshot = { + ...workerSnapshot, + phase: 'idle', + startedAt: null + }; + broadcastSnapshot(); + await debug('main.worker.stop.skip', { + hasWorker: false + }); + return workerSnapshot; + } + + await debug('main.worker.stop.request', { + phase: workerSnapshot.phase, + mode: workerSnapshot.mode + }); + + runtimeWorker.send({ type: 'stop' }); + return workerSnapshot; +} + +async function resolveLogDirectory() { + const portableCandidates = [ + path.dirname(process.execPath), + process.cwd() + ].filter((value, index, array) => Boolean(value) && array.indexOf(value) === index); + + for (const candidate of portableCandidates) { + try { + const target = path.join(candidate, 'logs'); + await fs.mkdir(target, { recursive: true }); + await fs.access(target); + return { path: target, reason: 'executable-near' as const }; + } catch { + continue; + } + } + + const fallback = path.join(app.getPath('userData'), 'logs'); + await fs.mkdir(fallback, { recursive: true }); + return { path: fallback, reason: 'userData-fallback' as const }; +} + +function configureWindowsLoopbackCapture() { + if (process.platform !== 'win32') { + return; + } + + session.defaultSession.setDisplayMediaRequestHandler((_request, callback) => { + void desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { width: 0, height: 0 } + }).then((sources) => { + const source = sources[0]; + if (!source) { + void debug('main.display-media.no-source', {}); + callback({}); + return; + } + + void debug('main.display-media.grant-loopback', { + sourceId: source.id, + sourceName: source.name + }); + callback({ + video: { + id: source.id, + name: source.name + }, + audio: 'loopback' + }); + }).catch((error: Error) => { + void debug('main.display-media.error', { error: error.message }); + callback({}); + }); + }, { useSystemPicker: false }); +} + +async function createWindow() { + mainWindow = new BrowserWindow({ + width: 1180, + height: 760, + minWidth: 900, + minHeight: 560, + backgroundColor: '#f4f1e8', + title: 'JP -> KO Realtime Translator', + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + trafficLightPosition: process.platform === 'darwin' ? { x: 18, y: 20 } : undefined, + webPreferences: { + preload: path.join(__dirname, '../preload.cjs') + } + }); + + mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { + void debug('main.window.did-fail-load', { + errorCode, + errorDescription, + validatedURL + }); + }); + + mainWindow.webContents.on('console-message', (_event, level, message, line, sourceId) => { + void debug('main.window.console', { + level, + message, + line, + sourceId + }); + }); + + await mainWindow.loadFile(rendererIndex); + + await debug('main.window.did-load-file', { + rendererIndex + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(async () => { + const userDataPath = app.getPath('userData'); + const { path: logDirectory, reason: logReason } = await resolveLogDirectory(); + providerConfigState = await loadProviderConfigState(userDataPath); + debugLogger = createDebugLogger({ + logDirectory, + scope: 'main' + }); + await debug('main.app.ready', { + platform: process.platform, + logDirectory, + logReason, + rendererIndex, + workerEntry + }); + configureWindowsLoopbackCapture(); + + ipcMain.handle('worker:get-status', async () => { + await debug('main.ipc.get-status', { + phase: workerSnapshot.phase, + mode: workerSnapshot.mode + }); + return workerSnapshot; + }); + ipcMain.handle( + 'worker:start', + async (_event, args: { mode?: RuntimeMode; provider?: TranslationProviderId } | RuntimeMode) => { + const mode = typeof args === 'string' ? args : args.mode ?? 'local'; + const provider = + typeof args === 'string' ? 'auto' : args.provider ?? (mode === 'local' ? 'local-fixture' : 'auto'); + await debug('main.ipc.start', { mode, provider }); + return ensureWorker(mode, provider); + } + ); + ipcMain.handle('worker:stop', async () => { + await debug('main.ipc.stop'); + return stopWorker(); + }); + ipcMain.on('renderer-audio:chunk', (_event, payload: { data?: number[]; mimeType?: string }) => { + if (!runtimeWorker || runtimeWorker.killed || workerSnapshot.phase !== 'running') { + return; + } + runtimeWorker.send({ + type: 'renderer-audio-chunk', + data: payload.data ?? [], + mimeType: payload.mimeType ?? 'audio/webm' + }); + }); + ipcMain.handle('provider-config:get', async () => providerConfigState); + ipcMain.handle('provider-config:save', async (_event, payload: ProviderConfigState) => { + providerConfigState = await saveProviderConfigState(userDataPath, payload); + if (runtimeWorker && !runtimeWorker.killed) { + expectedWorkerExit = true; + runtimeWorker.kill(); + runtimeWorker = null; + expectedWorkerExit = false; + } + return providerConfigState; + }); + ipcMain.handle('provider-config:launch-codex-auth', async () => { + const codexLoginUrl = 'https://chatgpt.com'; + if (process.platform === 'win32') { + const codexCliPath = await resolveCodexCliForLogin(); + const message = [ + codexCliPath + ? 'Codex CLI 로그인 창을 열었습니다. 브라우저 인증을 끝내면 앱이 ~/.codex/auth.json 세션을 사용합니다.' + : 'Codex CLI를 찾지 못했습니다. Codex 앱/CLI를 설치한 뒤 다시 시도하세요.', + '', + '1. 열린 터미널에서 Codex 로그인 진행', + '2. 브라우저 인증 완료', + '3. 완료 후 앱에서 연결 확인 다시 실행' + ].join('\n'); + + dialog.showMessageBox({ + type: 'info', + title: 'Codex 로그인 안내', + message: 'Codex CLI 로그인 후 앱이 세션을 재사용합니다.', + detail: message, + buttons: ['확인'] + }).catch(() => undefined); + + if (!codexCliPath) { + await shell.openExternal(codexLoginUrl); + return { ok: false, detail: 'Codex CLI를 찾지 못했습니다. Codex CLI 설치 후 다시 시도하세요.' }; + } + + const status = await getCodexLoginStatus(codexCliPath); + if (status.loggedIn) { + return { ok: true, detail: `Codex auth는 이미 로그인되어 있습니다. (${status.detail}) 연결 테스트를 눌러 번역까지 확인하세요.` }; + } + + launchCodexLoginTerminal(codexCliPath); + await shell.openExternal('https://auth.openai.com/codex/device'); + return { ok: true, detail: 'Codex 장치 인증 페이지와 PowerShell 로그인 창을 열었습니다. PowerShell에 표시된 1회용 코드를 웹페이지에 입력한 뒤 연결 테스트를 눌러 주세요.' }; + } + + await shell.openExternal(codexLoginUrl); + return { ok: true, detail: '브라우저에서 ChatGPT를 열었습니다. Codex CLI 로그인 후 다시 연결 확인을 하세요.' }; + }); + ipcMain.handle('provider-config:probe-translation', async (_event, args: { provider?: TranslationProviderId; text?: string } | undefined) => { + const provider = args?.provider ?? 'auto'; + const text = args?.text?.trim() || 'これは日本語のテスト音声です。'; + const previousEnv = { ...process.env }; + Object.assign(process.env, buildWorkerEnv(providerConfigState)); + const result = await runTranslationProbe(provider, text); + process.env = previousEnv; + + if (result.success) { + return { + ok: true, + detail: `${result.providerLabel} 연결 확인이 끝났습니다.`, + translatedText: result.translatedText + }; + } + + return { + ok: false, + detail: result.error ?? `${result.providerLabel} 연결 확인에 실패했습니다.`, + translatedText: null + }; + }); + ipcMain.handle('shell:open-external', async (_event, url: string) => { + await shell.openExternal(url); + return true; + }); + ipcMain.on('debug:permission-trace', (_event, payload: Record) => { + void debug('trace.permission', payload); + }); + ipcMain.handle('permissions:request-microphone', async () => { + if (process.platform !== 'darwin') { + return { ok: false, detail: '이 권한 요청 흐름은 현재 macOS에서만 지원됩니다.', status: 'unsupported' }; + } + + const permissionContext = getMicrophonePermissionContext(); + const plistInfo = await readMacInfoPlistUsageDescription(); + const currentStatus = systemPreferences.getMediaAccessStatus('microphone'); + await debug('main.ipc.permissions.request-microphone.enter', { + ...permissionContext, + plistInfo, + currentStatus + }); + if (currentStatus === 'granted') { + await debug('main.ipc.permissions.request-microphone.short-circuit', { + reason: 'already-granted', + currentStatus + }); + return { ok: true, detail: '마이크 권한이 이미 허용되어 있습니다.', status: currentStatus }; + } + + let granted = false; + let askError: unknown = null; + try { + await debug('main.ipc.permissions.request-microphone.ask.before', { + statusBeforeAsk: currentStatus + }); + granted = await systemPreferences.askForMediaAccess('microphone'); + await debug('main.ipc.permissions.request-microphone.ask.after', { + granted + }); + } catch (error) { + askError = error; + await debug('main.ipc.permissions.request-microphone.ask.error', { + error + }); + } + + const nextStatus = systemPreferences.getMediaAccessStatus('microphone'); + const fallbackTriggered = nextStatus === 'denied' || nextStatus === 'restricted'; + await debug('main.ipc.permissions.request-microphone.result', { + granted, + nextStatus, + askError, + fallbackTriggered + }); + if ((granted || nextStatus === 'granted') && !askError) { + return { ok: true, detail: '마이크 권한 요청을 마쳤습니다. 앱을 다시 시작하면 장치 확인이 바로 이어집니다.', status: nextStatus }; + } + + if (fallbackTriggered) { + const openedSettings = await openMacMicrophonePrivacySettings(); + await debug('main.ipc.permissions.request-microphone.fallback.settings', { + openedSettings, + nextStatus + }); + } else { + await debug('main.ipc.permissions.request-microphone.fallback.skipped', { + nextStatus + }); + } + + return { + ok: false, + detail: askError instanceof Error + ? `마이크 권한 요청 호출이 실패했습니다: ${askError.message}` + : buildMicrophonePermissionDetail(nextStatus, true), + status: nextStatus + }; + }); + ipcMain.handle('permissions:open-microphone-settings', async () => { + const opened = await openMacMicrophonePrivacySettings(); + return { + ok: opened, + detail: opened + ? '시스템 설정의 개인정보 보호 > 마이크를 열었습니다.' + : '이 동작은 현재 macOS에서만 지원됩니다.' + }; + }); + ipcMain.handle('permissions:open-screen-capture-settings', async () => { + const opened = await openMacScreenCapturePrivacySettings(); + return { + ok: opened, + detail: opened + ? '시스템 설정의 개인정보 보호 > 화면 및 시스템 오디오 녹음을 열었습니다.' + : '이 동작은 현재 macOS에서만 지원됩니다.' + }; + }); + + await createWindow(); + + app.on('activate', async () => { + if (BrowserWindow.getAllWindows().length === 0) { + await createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('before-quit', () => { + expectedWorkerExit = true; + if (runtimeWorker && !runtimeWorker.killed) { + runtimeWorker.kill(); + } +}); diff --git a/windows-jp-ko-translator-transfer-20260504/electron/native/macos-screen-audio-capture.swift b/windows-jp-ko-translator-transfer-20260504/electron/native/macos-screen-audio-capture.swift new file mode 100644 index 0000000..febde9e --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/native/macos-screen-audio-capture.swift @@ -0,0 +1,250 @@ +import Foundation +import AVFoundation +import CoreMedia +import ScreenCaptureKit + +struct HelperError: Error { + let message: String +} + +func emit(_ payload: [String: Any]) { + guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else { + return + } + FileHandle.standardOutput.write(data) + FileHandle.standardOutput.write(Data([0x0a])) +} + +final class SegmentWriter: @unchecked Sendable { + let fileURL: URL + let startedAt: CMTime + private let writer: AVAssetWriter + private let input: AVAssetWriterInput + private var sessionStarted = false + private var finishContinuation: CheckedContinuation? + + init(fileURL: URL, startedAt: CMTime, formatDescription: CMAudioFormatDescription) throws { + self.fileURL = fileURL + self.startedAt = startedAt + self.writer = try AVAssetWriter(outputURL: fileURL, fileType: .m4a) + self.input = AVAssetWriterInput(mediaType: .audio, outputSettings: [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: 16_000, + AVEncoderBitRateKey: 64_000 + ], sourceFormatHint: formatDescription) + self.input.expectsMediaDataInRealTime = true + + guard self.writer.canAdd(self.input) else { + throw HelperError(message: "AVAssetWriter cannot add audio input") + } + + self.writer.add(self.input) + guard self.writer.startWriting() else { + throw HelperError(message: self.writer.error?.localizedDescription ?? "AVAssetWriter failed to start") + } + } + + func append(_ sampleBuffer: CMSampleBuffer) throws { + if !sessionStarted { + writer.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) + sessionStarted = true + } + + guard input.isReadyForMoreMediaData else { + return + } + + guard input.append(sampleBuffer) else { + throw HelperError(message: writer.error?.localizedDescription ?? "Failed to append audio sample") + } + } + + func finish() async { + input.markAsFinished() + await withCheckedContinuation { continuation in + finishContinuation = continuation + writer.finishWriting { [weak self] in + guard let self else { + continuation.resume() + return + } + self.finishContinuation?.resume() + self.finishContinuation = nil + } + } + } +} + +final class ScreenAudioCapture: NSObject, SCStreamOutput { + private let outputDirectory: URL + private let segmentDuration: Double + private var stream: SCStream? + private var writer: SegmentWriter? + private let queue = DispatchQueue(label: "com.mingu.jpkotranslator.screen-audio") + private var segmentIndex = 0 + + init(outputDirectory: URL, segmentDuration: Double) { + self.outputDirectory = outputDirectory + self.segmentDuration = segmentDuration + super.init() + } + + func start() async throws { + if #available(macOS 13.0, *) { + let shareable = try await SCShareableContent.current + guard let display = shareable.displays.first else { + throw HelperError(message: "No display available for ScreenCaptureKit") + } + + let filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: []) + let configuration = SCStreamConfiguration() + configuration.width = 2 + configuration.height = 2 + configuration.minimumFrameInterval = CMTime(value: 1, timescale: 1) + configuration.queueDepth = 3 + configuration.capturesAudio = true + configuration.captureMicrophone = false + configuration.excludesCurrentProcessAudio = false + configuration.sampleRate = 16_000 + configuration.channelCount = 1 + + let stream = SCStream(filter: filter, configuration: configuration, delegate: nil) + self.stream = stream + try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: queue) + try await stream.startCapture() + + emit([ + "event": "ready", + "backend": "macos-screencapturekit-native", + "backendLabel": "macOS ScreenCaptureKit Native", + "deviceLabel": "Display \(display.displayID)", + "outputDirectory": outputDirectory.path + ]) + } else { + throw HelperError(message: "macOS 13.0 or newer is required for ScreenCaptureKit audio capture") + } + } + + func stop() async { + if let stream { + try? await stream.stopCapture() + } + if let writer { + await writer.finish() + self.writer = nil + } + } + + func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { + guard outputType == .audio, CMSampleBufferIsValid(sampleBuffer), CMSampleBufferDataIsReady(sampleBuffer) else { + return + } + + do { + try rotateWriterIfNeeded(for: sampleBuffer) + try writer?.append(sampleBuffer) + } catch { + emit([ + "event": "error", + "message": error.localizedDescription + ]) + } + } + + private func rotateWriterIfNeeded(for sampleBuffer: CMSampleBuffer) throws { + let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + if let writer, CMTimeGetSeconds(pts - writer.startedAt) < segmentDuration { + return + } + + let previousWriter = writer + writer = nil + if let previousWriter { + Task { + await previousWriter.finish() + } + } + + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) else { + throw HelperError(message: "Missing audio format description") + } + + let fileURL = outputDirectory.appendingPathComponent(String(format: "chunk-%05d.m4a", segmentIndex)) + segmentIndex += 1 + writer = try SegmentWriter(fileURL: fileURL, startedAt: pts, formatDescription: formatDescription) + } +} + +func parseArguments() throws -> (URL, Double) { + let arguments = Array(CommandLine.arguments.dropFirst()) + var outputDirectory: String? + var segmentDuration = 4.0 + var index = 0 + + while index < arguments.count { + let argument = arguments[index] + switch argument { + case "--output-dir": + index += 1 + guard index < arguments.count else { + throw HelperError(message: "Missing value for --output-dir") + } + outputDirectory = arguments[index] + case "--segment-seconds": + index += 1 + guard index < arguments.count, let value = Double(arguments[index]), value > 0 else { + throw HelperError(message: "Invalid value for --segment-seconds") + } + segmentDuration = value + default: + throw HelperError(message: "Unknown argument: \(argument)") + } + index += 1 + } + + guard let outputDirectory else { + throw HelperError(message: "--output-dir is required") + } + + return (URL(fileURLWithPath: outputDirectory, isDirectory: true), segmentDuration) +} + +Task { + do { + let (outputDirectory, segmentDuration) = try parseArguments() + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + let capture = ScreenAudioCapture(outputDirectory: outputDirectory, segmentDuration: segmentDuration) + try await capture.start() + + signal(SIGINT, SIG_IGN) + signal(SIGTERM, SIG_IGN) + + let signalStream = AsyncStream { continuation in + let sigint = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main) + sigint.setEventHandler { continuation.yield(()) } + sigint.resume() + + let sigterm = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main) + sigterm.setEventHandler { continuation.yield(()) } + sigterm.resume() + + continuation.onTermination = { _ in + sigint.cancel() + sigterm.cancel() + } + } + + _ = await signalStream.first(where: { _ in true }) + await capture.stop() + exit(0) + } catch let error as HelperError { + emit(["event": "error", "message": error.message]) + exit(1) + } catch { + emit(["event": "error", "message": error.localizedDescription]) + exit(1) + } +} + +RunLoop.main.run() diff --git a/windows-jp-ko-translator-transfer-20260504/electron/native/macos-system-audio-tap.swift b/windows-jp-ko-translator-transfer-20260504/electron/native/macos-system-audio-tap.swift new file mode 100644 index 0000000..104bf83 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/native/macos-system-audio-tap.swift @@ -0,0 +1,170 @@ +import Foundation +import CoreAudio + +@available(macOS 14.2, *) +final class TapSession { + private var tapID: AudioObjectID = kAudioObjectUnknown + private var aggregateDeviceID: AudioObjectID = kAudioObjectUnknown + private let sessionUUID = UUID().uuidString.lowercased() + private let outputName: String + private let outputUID: String + private let aggregateName: String + private let aggregateUID: String + + init(outputName: String, outputUID: String) { + self.outputName = outputName + self.outputUID = outputUID + self.aggregateName = "JP-KO Translator System Audio \(sessionUUID.prefix(8))" + self.aggregateUID = "com.mingu.jpkotranslator.system-audio.\(sessionUUID)" + } + + func start() throws { + let description = CATapDescription(__excludingProcesses: [], andDeviceUID: outputUID, withStream: 0) + description.name = aggregateName + description.isPrivate = false + description.muteBehavior = CATapMuteBehavior(rawValue: 0)! + + try check(AudioHardwareCreateProcessTap(description, &tapID), context: "AudioHardwareCreateProcessTap") + + let tapDictionary: [String: Any] = [ + kAudioSubTapUIDKey: description.uuid.uuidString, + kAudioSubTapDriftCompensationKey: false + ] + let aggregateDictionary: [String: Any] = [ + kAudioAggregateDeviceNameKey: aggregateName, + kAudioAggregateDeviceUIDKey: aggregateUID, + kAudioAggregateDeviceMainSubDeviceKey: outputUID, + kAudioAggregateDeviceIsPrivateKey: false, + kAudioAggregateDeviceIsStackedKey: false, + kAudioAggregateDeviceTapAutoStartKey: true, + kAudioAggregateDeviceTapListKey: [tapDictionary] + ] + + do { + try check(AudioHardwareCreateAggregateDevice(aggregateDictionary as CFDictionary, &aggregateDeviceID), context: "AudioHardwareCreateAggregateDevice") + } catch { + _ = AudioHardwareDestroyProcessTap(tapID) + tapID = kAudioObjectUnknown + throw error + } + + emit([ + "event": "ready", + "backend": "macos-coreaudio-tap", + "backendLabel": "macOS CoreAudio Tap", + "deviceLabel": aggregateName, + "aggregateDeviceUID": aggregateUID, + "aggregateDeviceName": aggregateName, + "outputDeviceUID": outputUID, + "outputDeviceName": outputName + ]) + } + + func stop() { + if aggregateDeviceID != kAudioObjectUnknown { + _ = AudioHardwareDestroyAggregateDevice(aggregateDeviceID) + aggregateDeviceID = kAudioObjectUnknown + } + if tapID != kAudioObjectUnknown { + _ = AudioHardwareDestroyProcessTap(tapID) + tapID = kAudioObjectUnknown + } + } +} + +struct HelperError: Error { + let message: String +} + +@available(macOS 14.2, *) +func emit(_ payload: [String: Any]) { + guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else { + return + } + FileHandle.standardOutput.write(data) + FileHandle.standardOutput.write("\n".data(using: .utf8)!) +} + +func emitError(_ message: String) { + let payload: [String: Any] = [ + "event": "error", + "message": message + ] + guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else { + fputs(message + "\n", stderr) + return + } + FileHandle.standardOutput.write(data) + FileHandle.standardOutput.write("\n".data(using: .utf8)!) +} + +func check(_ status: OSStatus, context: String) throws { + guard status == noErr else { + throw HelperError(message: "\(context) failed with OSStatus \(status)") + } +} + +func readStringProperty(deviceID: AudioObjectID, selector: AudioObjectPropertySelector) throws -> String { + var address = AudioObjectPropertyAddress( + mSelector: selector, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + var cfValue: CFString = "" as CFString + var size = UInt32(MemoryLayout.size) + try check(AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &cfValue), context: "AudioObjectGetPropertyData(\(selector))") + return cfValue as String +} + +func defaultOutputDevice() throws -> AudioObjectID { + var address = AudioObjectPropertyAddress( + mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + var deviceID = AudioObjectID(kAudioObjectUnknown) + var size = UInt32(MemoryLayout.size) + try check(AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &deviceID), context: "kAudioHardwarePropertyDefaultOutputDevice") + guard deviceID != kAudioObjectUnknown else { + throw HelperError(message: "No default output device is available") + } + return deviceID +} + +if #available(macOS 14.2, *) { + do { + let outputDeviceID = try defaultOutputDevice() + let outputUID = try readStringProperty(deviceID: outputDeviceID, selector: kAudioDevicePropertyDeviceUID) + let outputName = try readStringProperty(deviceID: outputDeviceID, selector: kAudioObjectPropertyName) + let session = TapSession(outputName: outputName, outputUID: outputUID) + try session.start() + + signal(SIGINT, SIG_IGN) + signal(SIGTERM, SIG_IGN) + + let sigint = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main) + sigint.setEventHandler { + session.stop() + exit(0) + } + sigint.resume() + + let sigterm = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main) + sigterm.setEventHandler { + session.stop() + exit(0) + } + sigterm.resume() + + RunLoop.main.run() + } catch let error as HelperError { + emitError(error.message) + exit(1) + } catch { + emitError(error.localizedDescription) + exit(1) + } +} else { + emitError("macOS 14.2 or newer is required for CoreAudio process taps") + exit(1) +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/preload.cts b/windows-jp-ko-translator-transfer-20260504/electron/preload.cts new file mode 100644 index 0000000..54f419b --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/preload.cts @@ -0,0 +1,95 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import type { RuntimeMode, TranslationProviderId, WorkerSnapshot } from './shared/runtime-contract.js'; +import type { ProviderConfigState } from './main/config-store.js'; + +function tracePermission(event: string, payload: Record = {}) { + const enriched = { + source: 'preload', + event, + timestamp: new Date().toISOString(), + ...payload + }; + console.log('[permission-trace]', enriched); + ipcRenderer.send('debug:permission-trace', enriched); +} + +const bridge = { + getWorkerStatus: (): Promise => ipcRenderer.invoke('worker:get-status'), + startWorker: (mode: RuntimeMode, provider?: TranslationProviderId): Promise => + ipcRenderer.invoke('worker:start', { mode, provider }), + stopWorker: (): Promise => ipcRenderer.invoke('worker:stop'), + getProviderConfig: (): Promise => ipcRenderer.invoke('provider-config:get'), + saveProviderConfig: (payload: ProviderConfigState): Promise => ipcRenderer.invoke('provider-config:save', payload), + launchCodexAuth: (): Promise<{ ok: boolean; detail: string }> => ipcRenderer.invoke('provider-config:launch-codex-auth'), + probeTranslation: (provider: TranslationProviderId, text?: string): Promise<{ ok: boolean; detail: string; translatedText?: string | null }> => + ipcRenderer.invoke('provider-config:probe-translation', { provider, text }), + pushRendererAudioChunk: (payload: { data: number[]; mimeType: string }): void => { + ipcRenderer.send('renderer-audio:chunk', payload); + }, + requestMicrophonePermission: async (): Promise<{ ok: boolean; detail: string; status: string }> => { + tracePermission('preload.requestMicrophonePermission.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:request-microphone'); + tracePermission('preload.requestMicrophonePermission.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.requestMicrophonePermission.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openMicrophoneSettings: async (): Promise<{ ok: boolean; detail: string }> => { + tracePermission('preload.openMicrophoneSettings.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:open-microphone-settings'); + tracePermission('preload.openMicrophoneSettings.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.openMicrophoneSettings.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openScreenCaptureSettings: async (): Promise<{ ok: boolean; detail: string }> => { + tracePermission('preload.openScreenCaptureSettings.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:open-screen-capture-settings'); + tracePermission('preload.openScreenCaptureSettings.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.openScreenCaptureSettings.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openExternalUrl: (url: string): Promise => ipcRenderer.invoke('shell:open-external', url), + onWorkerStatus: (listener: (snapshot: WorkerSnapshot) => void) => { + const wrapped = (_event: Electron.IpcRendererEvent, snapshot: WorkerSnapshot) => { + listener(snapshot); + }; + ipcRenderer.on('worker:status', wrapped); + return () => ipcRenderer.removeListener('worker:status', wrapped); + } +}; + +contextBridge.exposeInMainWorld('translatorRuntime', bridge); + +declare global { + interface Window { + translatorRuntime: typeof bridge; + } +} + +export type { ProviderConfigState } from './main/config-store.js'; +export type { + CaptureBackendOption, + CaptureSnapshot, + RuntimeMode, + TranslationProviderId, + TranslationProviderStatus, + TranslationRouteStatus, + WorkerSnapshot +} from './shared/runtime-contract.js'; diff --git a/windows-jp-ko-translator-transfer-20260504/electron/preload.ts b/windows-jp-ko-translator-transfer-20260504/electron/preload.ts new file mode 100644 index 0000000..54f419b --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/preload.ts @@ -0,0 +1,95 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import type { RuntimeMode, TranslationProviderId, WorkerSnapshot } from './shared/runtime-contract.js'; +import type { ProviderConfigState } from './main/config-store.js'; + +function tracePermission(event: string, payload: Record = {}) { + const enriched = { + source: 'preload', + event, + timestamp: new Date().toISOString(), + ...payload + }; + console.log('[permission-trace]', enriched); + ipcRenderer.send('debug:permission-trace', enriched); +} + +const bridge = { + getWorkerStatus: (): Promise => ipcRenderer.invoke('worker:get-status'), + startWorker: (mode: RuntimeMode, provider?: TranslationProviderId): Promise => + ipcRenderer.invoke('worker:start', { mode, provider }), + stopWorker: (): Promise => ipcRenderer.invoke('worker:stop'), + getProviderConfig: (): Promise => ipcRenderer.invoke('provider-config:get'), + saveProviderConfig: (payload: ProviderConfigState): Promise => ipcRenderer.invoke('provider-config:save', payload), + launchCodexAuth: (): Promise<{ ok: boolean; detail: string }> => ipcRenderer.invoke('provider-config:launch-codex-auth'), + probeTranslation: (provider: TranslationProviderId, text?: string): Promise<{ ok: boolean; detail: string; translatedText?: string | null }> => + ipcRenderer.invoke('provider-config:probe-translation', { provider, text }), + pushRendererAudioChunk: (payload: { data: number[]; mimeType: string }): void => { + ipcRenderer.send('renderer-audio:chunk', payload); + }, + requestMicrophonePermission: async (): Promise<{ ok: boolean; detail: string; status: string }> => { + tracePermission('preload.requestMicrophonePermission.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:request-microphone'); + tracePermission('preload.requestMicrophonePermission.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.requestMicrophonePermission.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openMicrophoneSettings: async (): Promise<{ ok: boolean; detail: string }> => { + tracePermission('preload.openMicrophoneSettings.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:open-microphone-settings'); + tracePermission('preload.openMicrophoneSettings.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.openMicrophoneSettings.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openScreenCaptureSettings: async (): Promise<{ ok: boolean; detail: string }> => { + tracePermission('preload.openScreenCaptureSettings.invoke'); + try { + const result = await ipcRenderer.invoke('permissions:open-screen-capture-settings'); + tracePermission('preload.openScreenCaptureSettings.result', result as Record); + return result; + } catch (error) { + tracePermission('preload.openScreenCaptureSettings.error', { + error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error) + }); + throw error; + } + }, + openExternalUrl: (url: string): Promise => ipcRenderer.invoke('shell:open-external', url), + onWorkerStatus: (listener: (snapshot: WorkerSnapshot) => void) => { + const wrapped = (_event: Electron.IpcRendererEvent, snapshot: WorkerSnapshot) => { + listener(snapshot); + }; + ipcRenderer.on('worker:status', wrapped); + return () => ipcRenderer.removeListener('worker:status', wrapped); + } +}; + +contextBridge.exposeInMainWorld('translatorRuntime', bridge); + +declare global { + interface Window { + translatorRuntime: typeof bridge; + } +} + +export type { ProviderConfigState } from './main/config-store.js'; +export type { + CaptureBackendOption, + CaptureSnapshot, + RuntimeMode, + TranslationProviderId, + TranslationProviderStatus, + TranslationRouteStatus, + WorkerSnapshot +} from './shared/runtime-contract.js'; diff --git a/windows-jp-ko-translator-transfer-20260504/electron/shared/capture-backends.ts b/windows-jp-ko-translator-transfer-20260504/electron/shared/capture-backends.ts new file mode 100644 index 0000000..f301988 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/shared/capture-backends.ts @@ -0,0 +1,173 @@ +import type { + CaptureBackend, + CaptureBackendOption, + CaptureSnapshot, + HostPlatform +} from './runtime-contract.js'; + +const WINDOWS_BACKENDS: CaptureBackendOption[] = [ + { + key: 'windows-wasapi-loopback', + label: 'Windows WASAPI Loopback', + availability: 'native-ready', + selectable: true, + recommended: true, + description: 'Windows 데스크톱 재생 오디오를 위한 기본 시스템 캡처 경로입니다.', + setupHint: '추가 가상 장치가 필요하지 않습니다. 런타임은 기본 재생 장치를 대상으로 합니다.' + } +]; + +const MACOS_BACKENDS: CaptureBackendOption[] = [ + { + key: 'macos-screencapturekit-native', + label: 'macOS ScreenCaptureKit Native', + availability: 'native-ready', + selectable: true, + recommended: true, + description: '네이티브 Swift helper가 ScreenCaptureKit 오디오 스트림을 직접 받아 segment 파일로 기록하는 기본 경로입니다.', + setupHint: '추가 가상 장치나 수동 라우팅이 필요하지 않습니다. macOS 13+와 화면 기록 권한이 필요합니다.' + }, + { + key: 'macos-coreaudio-tap', + label: 'macOS CoreAudio Tap', + availability: 'planned', + selectable: false, + recommended: false, + description: '이전 CoreAudio process tap 실험 경로입니다. 현재 기본 경로에서 제외했습니다.', + setupHint: '현재는 사용하지 않습니다.' + }, + { + key: 'macos-screencapturekit', + label: 'macOS ScreenCaptureKit Audio', + availability: 'planned', + selectable: false, + recommended: false, + description: '이전 Electron helper 기반 ScreenCaptureKit 경로입니다. 이 호스트에서는 브라우저 capture API 제약으로 막혀 있어 기본값에서 제외했습니다.', + setupHint: '현재는 사용하지 않습니다.' + }, + { + key: 'macos-avfoundation-input', + label: 'macOS AVFoundation Input', + availability: 'manual-setup', + selectable: true, + recommended: false, + description: '입력 장치를 직접 캡처하는 레거시 경로입니다. 시스템 오디오는 가상 장치 라우팅이 필요합니다.', + setupHint: '직접 시스템 오디오가 막힐 때만 예비 경로로 사용하세요. 기본값은 ScreenCaptureKit입니다.' + }, + { + key: 'macos-blackhole', + label: 'macOS BlackHole', + availability: 'manual-setup', + selectable: false, + recommended: false, + description: '시스템 출력을 BlackHole 가상 장치로 보낸 뒤 캡처하는 수동 우회 경로입니다.', + setupHint: 'ScreenCaptureKit 권한 또는 호환성 문제가 있을 때만 사용하세요.' + }, + { + key: 'manual-audio-routing', + label: 'Manual Audio Routing', + availability: 'manual-setup', + selectable: true, + recommended: false, + description: '네이티브 macOS 캡처가 완전히 붙기 전, 수동 라우팅 또는 외부 미러링 입력으로 점검하는 모드입니다.', + setupHint: '네이티브 macOS 캡처 브리지가 없더라도 앱 셸과 worker 흐름을 먼저 점검하고 싶을 때 사용합니다.' + } +]; + +const FALLBACK_BACKENDS: CaptureBackendOption[] = [ + { + key: 'unavailable', + label: 'No Native Capture Backend', + availability: 'unsupported', + selectable: false, + recommended: true, + description: '현재 빌드에는 이 호스트용 1급 시스템 오디오 백엔드가 없습니다.', + setupHint: '런타임은 fixture transcript 이벤트 기반으로 UI와 흐름 점검에 계속 사용할 수 있습니다.' + } +]; + +function resolveBackends(platform: HostPlatform) { + if (platform === 'win32') { + return WINDOWS_BACKENDS; + } + + if (platform === 'darwin') { + return MACOS_BACKENDS; + } + + return FALLBACK_BACKENDS; +} + +function preferredBackend(platform: HostPlatform, backends: CaptureBackendOption[]) { + if (platform === 'win32') { + return backends[0]; + } + + if (platform === 'darwin') { + return backends.find((backend) => backend.recommended) ?? backends[0]; + } + + return backends[0]; +} + +export function createCaptureSnapshot(platform: HostPlatform): CaptureSnapshot { + const availableBackends = resolveBackends(platform); + const selected = preferredBackend(platform, availableBackends); + + if (platform === 'win32') { + return { + phase: 'ready', + backend: selected.key, + target: 'system-audio', + platform, + deviceLabel: 'Default Windows Output (auto loopback)', + supportsSystemAudio: true, + recoveryAttempts: 0, + guidance: 'Windows 재생 오디오 캡처용으로 WASAPI loopback 백엔드가 선택되어 있습니다. 별도 가상 장치 없이 현재 기본 출력 장치를 자동으로 따라갑니다.', + lastError: null, + backendLabel: selected.label, + backendAvailability: selected.availability, + availableBackends + }; + } + + if (platform === 'darwin') { + return { + phase: 'ready', + backend: selected.key, + target: 'system-audio', + platform, + deviceLabel: '자동 선택 대기 중', + supportsSystemAudio: true, + recoveryAttempts: 0, + guidance: + 'macOS ScreenCaptureKit native helper 경로를 사용합니다. 추가 가상 장치 없이 시스템 오디오를 직접 segment 파일로 기록합니다.', + lastError: null, + backendLabel: selected.label, + backendAvailability: selected.availability, + availableBackends + }; + } + + return { + phase: 'unsupported', + backend: 'unavailable', + target: 'system-audio', + platform, + deviceLabel: null, + supportsSystemAudio: false, + recoveryAttempts: 0, + guidance: '현재 빌드 기준으로 이 호스트는 Windows/macOS 캡처 지원 범위 밖입니다.', + lastError: null, + backendLabel: availableBackends[0].label, + backendAvailability: availableBackends[0].availability, + availableBackends + }; +} + +export function resolveCaptureBackendOption( + backend: CaptureBackend, + platform: HostPlatform +): CaptureBackendOption | undefined { + return resolveBackends(platform).find((option) => option.key === backend); +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/shared/debug-log.ts b/windows-jp-ko-translator-transfer-20260504/electron/shared/debug-log.ts new file mode 100644 index 0000000..80f4801 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/shared/debug-log.ts @@ -0,0 +1,79 @@ +import { appendFile, mkdir } from 'node:fs/promises'; +import path from 'node:path'; + +interface DebugLoggerOptions { + logDirectory: string; + scope: string; +} + +export interface DebugLogger { + readonly textPath: string; + readonly jsonlPath: string; + log(event: string, payload?: Record): Promise; +} + +function serializeError(error: unknown) { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack ?? null + }; + } + + return { + message: String(error) + }; +} + +function stringifyValue(value: unknown): string { + if (value instanceof Error) { + return JSON.stringify(serializeError(value)); + } + + if (typeof value === 'string') { + return value; + } + + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function createDebugLogger(options: DebugLoggerOptions): DebugLogger { + const baseName = `${options.scope}-debug`; + const textPath = path.join(options.logDirectory, `${baseName}.log`); + const jsonlPath = path.join(options.logDirectory, `${baseName}.jsonl`); + + return { + textPath, + jsonlPath, + async log(event: string, payload: Record = {}) { + await mkdir(options.logDirectory, { recursive: true }); + const timestamp = new Date().toISOString(); + const serializedPayload = Object.fromEntries( + Object.entries(payload).map(([key, value]) => [ + key, + value instanceof Error ? serializeError(value) : value + ]) + ); + + const jsonlLine = `${JSON.stringify({ + timestamp, + scope: options.scope, + event, + payload: serializedPayload + })}\n`; + const textLine = `[${timestamp}] [${options.scope}] ${event} ${Object.entries(serializedPayload) + .map(([key, value]) => `${key}=${stringifyValue(value)}`) + .join(' ')}\n`; + + await Promise.all([ + appendFile(jsonlPath, jsonlLine, 'utf8'), + appendFile(textPath, textLine, 'utf8') + ]); + } + }; +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/shared/runtime-contract.ts b/windows-jp-ko-translator-transfer-20260504/electron/shared/runtime-contract.ts new file mode 100644 index 0000000..45ce0cd --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/shared/runtime-contract.ts @@ -0,0 +1,131 @@ +export type RuntimeMode = 'local' | 'hybrid' | 'cloud'; +export type TranslationProviderId = 'auto' | 'local-fixture' | 'codex-auth' | 'chatgpt-api' | 'gemini-api' | 'deepl-fallback'; +export type WorkerPhase = 'idle' | 'starting' | 'running' | 'stopping' | 'error'; +export type TranscriptSource = 'fixture' | 'live-capture'; +export type HostPlatform = + | 'aix' + | 'android' + | 'darwin' + | 'freebsd' + | 'haiku' + | 'linux' + | 'netbsd' + | 'openbsd' + | 'sunos' + | 'win32' + | 'cygwin'; +export type CapturePhase = + | 'idle' + | 'probing' + | 'ready' + | 'capturing' + | 'recovering' + | 'unsupported' + | 'error'; +export type CaptureBackend = + | 'windows-wasapi-loopback' + | 'macos-avfoundation-input' + | 'macos-blackhole' + | 'macos-coreaudio-tap' + | 'macos-screencapturekit-native' + | 'macos-screencapturekit' + | 'manual-audio-routing' + | 'unavailable'; + +export interface CaptureBackendOption { + key: CaptureBackend; + label: string; + availability: 'native-ready' | 'manual-setup' | 'planned' | 'unsupported'; + selectable: boolean; + recommended: boolean; + description: string; + setupHint: string; +} + +export interface CaptureSnapshot { + phase: CapturePhase; + backend: CaptureBackend; + target: 'system-audio'; + platform: HostPlatform; + deviceLabel: string | null; + supportsSystemAudio: boolean; + recoveryAttempts: number; + guidance: string; + lastError: string | null; + backendLabel: string; + backendAvailability: CaptureBackendOption['availability']; + availableBackends: CaptureBackendOption[]; +} + +export interface ProviderSummary { + stt: string; + translation: string; +} + +export interface TranslationProviderStatus { + id: TranslationProviderId; + label: string; + configured: boolean; + available: boolean; + state: 'ready' | 'missing-config' | 'manual-auth' | 'unavailable' | 'active' | 'fallback-active'; + detail: string; +} + +export interface TranslationRouteStatus { + selected: TranslationProviderId; + active: TranslationProviderId; + fallbackUsed: boolean; + selectedLabel: string; + activeLabel: string; + detail: string; + providerStatuses: TranslationProviderStatus[]; +} + +export interface ModeDescriptor { + label: string; + description: string; + localFirst: boolean; + cloudEnabled: boolean; + requiresCredentials: boolean; +} + +export interface TranscriptLine { + id: string; + sourceText: string; + translatedText: string; + replySuggestions: ReplySuggestion[]; + sourceFinal: boolean; + translatedFinal: boolean; + createdAt: string; +} + +export interface ReplySuggestion { + id: string; + korean: string; + japanese: string; + reading: string; +} + +export interface WorkerSnapshot { + phase: WorkerPhase; + mode: RuntimeMode; + sourceLanguage: 'ja'; + targetLanguage: 'ko'; + transcriptSource: TranscriptSource; + transcriptSourceLabel: string; + liveCaptureImplemented: boolean; + providerSummary: ProviderSummary; + translationRoute: TranslationRouteStatus; + modeDescriptor: ModeDescriptor; + capture: CaptureSnapshot; + startedAt: string | null; + lastError: string | null; + retryCount: number; + healthMessage: string; + logPath: string | null; + debugLogPath: string | null; + logDirectory: string | null; + logPathDetail: string | null; + routeLabel: string; + lines: TranscriptLine[]; +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/verify-runtime.ts b/windows-jp-ko-translator-transfer-20260504/electron/verify-runtime.ts new file mode 100644 index 0000000..cea0b25 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/verify-runtime.ts @@ -0,0 +1,193 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import type { HostPlatform, RuntimeMode, TranslationProviderId, WorkerSnapshot } from './shared/runtime-contract.js'; +import { runLiveCaptureProbe } from './worker/live-capture.js'; +import { runTranslationProbe, TranslatorRuntime } from './worker/runtime-core.js'; + +interface ParsedArgs { + mode: RuntimeMode; + provider: TranslationProviderId; + sampleText: string; + timeoutMs: number; + reportFile: string | null; +} + +function parseArgs(argv: string[]): ParsedArgs { + const values = new Map(); + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) { + continue; + } + const key = token.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + values.set(key, '1'); + continue; + } + values.set(key, next); + index += 1; + } + + const mode = (values.get('mode') as RuntimeMode | undefined) ?? 'hybrid'; + const provider = (values.get('provider') as TranslationProviderId | undefined) ?? 'auto'; + const sampleText = + values.get('text') + ?? '音声入力と翻訳経路を実機で確認しています。'; + const timeoutMs = Number.parseInt(values.get('timeout-ms') ?? '', 10); + const reportFile = values.get('report-file')?.trim() || null; + + return { + mode, + provider, + sampleText, + timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 12_000, + reportFile + }; +} + +async function waitForRuntimeResult( + runtime: TranslatorRuntime, + timeoutMs: number +): Promise<{ + status: 'translated' | 'capture-ready-only' | 'failed'; + snapshot: WorkerSnapshot; +}> { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + const snapshot = runtime.getSnapshot(); + const translatedLine = snapshot.lines.find((line) => line.translatedText.trim()); + if (translatedLine) { + return { + status: 'translated', + snapshot + }; + } + if (snapshot.phase === 'error') { + return { + status: 'failed', + snapshot + }; + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + const snapshot = runtime.getSnapshot(); + return { + status: snapshot.capture.phase === 'capturing' ? 'capture-ready-only' : 'failed', + snapshot + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const platform = process.platform as HostPlatform; + const providerOrder: TranslationProviderId[] = ['codex-auth', 'chatgpt-api', 'gemini-api', 'deepl-fallback']; + + const translationResults = []; + for (const provider of providerOrder) { + translationResults.push(await runTranslationProbe(provider, args.sampleText)); + } + + let captureResult: + | Awaited> + | null = null; + let captureProbeError: string | null = null; + + try { + captureResult = await runLiveCaptureProbe(platform, { + timeoutMs: Math.min(args.timeoutMs, 8_000) + }); + } catch (error) { + captureProbeError = error instanceof Error ? error.message : String(error); + } + + const runtime = new TranslatorRuntime({ + onSnapshot: () => undefined + }); + + let e2eStatus: 'translated' | 'capture-ready-only' | 'failed' = 'failed'; + let e2eSnapshot = runtime.getSnapshot(); + let e2eError: string | null = null; + + try { + await runtime.handleCommand({ + type: 'start', + mode: args.mode, + provider: args.provider, + logDirectory: path.join(os.tmpdir(), 'jp-ko-runtime-probe-logs'), + debugLogDirectory: path.join(os.tmpdir(), 'jp-ko-runtime-probe-logs') + }); + const result = await waitForRuntimeResult(runtime, args.timeoutMs); + e2eStatus = result.status; + e2eSnapshot = result.snapshot; + } catch (error) { + e2eError = error instanceof Error ? error.message : String(error); + e2eSnapshot = runtime.getSnapshot(); + } finally { + await runtime.handleCommand({ type: 'stop' }).catch(() => undefined); + } + + const summary = { + timestamp: new Date().toISOString(), + platform, + requested: { + mode: args.mode, + provider: args.provider, + sampleText: args.sampleText, + timeoutMs: args.timeoutMs + }, + translationResults, + captureProbe: captureResult + ? { + success: captureResult.ready, + ...captureResult + } + : { + success: false, + error: captureProbeError + }, + e2e: { + status: e2eStatus, + error: e2eError, + phase: e2eSnapshot.phase, + capturePhase: e2eSnapshot.capture.phase, + lastError: e2eSnapshot.lastError, + routeLabel: e2eSnapshot.routeLabel, + activeProvider: e2eSnapshot.translationRoute.active, + fallbackUsed: e2eSnapshot.translationRoute.fallbackUsed, + lineCount: e2eSnapshot.lines.length, + translatedLines: e2eSnapshot.lines.filter((line) => line.translatedText.trim()).length, + transcriptPreview: e2eSnapshot.lines[0]?.sourceText ?? null, + translationPreview: e2eSnapshot.lines[0]?.translatedText ?? null + } + }; + + if (args.reportFile) { + await mkdir(path.dirname(args.reportFile), { recursive: true }); + await writeFile(args.reportFile, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); + } + + console.log(JSON.stringify(summary, null, 2)); + + const hasSuccessfulProvider = translationResults.some((result) => result.success); + const e2eOkay = e2eStatus === 'translated'; + process.exitCode = hasSuccessfulProvider && captureResult?.ready && e2eOkay ? 0 : 1; +} + +void main().catch((error) => { + console.error( + JSON.stringify( + { + timestamp: new Date().toISOString(), + fatal: error instanceof Error ? error.message : String(error) + }, + null, + 2 + ) + ); + process.exitCode = 1; +}); diff --git a/windows-jp-ko-translator-transfer-20260504/electron/worker/live-capture.ts b/windows-jp-ko-translator-transfer-20260504/electron/worker/live-capture.ts new file mode 100644 index 0000000..8b9440c --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/worker/live-capture.ts @@ -0,0 +1,1567 @@ +import { execFile, spawn, type ChildProcess } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +import type { CaptureSnapshot, HostPlatform } from '../shared/runtime-contract.js'; + +const execFileAsync = promisify(execFile); + +export interface LiveTranscriptChunk { + id: string; + text: string; + finalized: boolean; +} + +export interface LiveCaptureHooks { + onCaptureReady: (detail: { deviceLabel: string; backendLabel: string; command: string }) => Promise | void; + onCaptureExit: (message: string) => Promise | void; + onCaptureError: (message: string) => Promise | void; +} + +export interface LiveAudioSttProviderOptions { + externalInput?: boolean; +} + +function stripTerminalControl(value: string) { + return value + .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '') + .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '') + .replace(/\r/g, '\n'); +} + +function getWhisperStreamExecutablePath() { + const explicit = process.env.WHISPER_STREAM_PATH?.trim(); + if (explicit && existsSync(explicit)) { + return explicit; + } + + const candidates = [ + ...getResourceRoots().map((root) => path.join(root, 'bin', process.platform, process.arch, 'stream.exe')), + ...getResourceRoots().map((root) => path.join(root, 'bin', process.platform, 'x64', 'stream.exe')), + ...getResourceRoots().map((root) => path.join(root, 'b', 'wx', 'stream.exe')), + path.join(process.cwd(), 'vendor', 'bin', process.platform, process.arch, 'stream.exe'), + path.join(process.cwd(), 'vendor', 'bin', process.platform, 'x64', 'stream.exe'), + path.join(process.cwd(), 'vendor', 'bin', 'win32', 'x64', 'stream.exe') + ]; + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +function summarizeFfmpegWindowsError(stderr: string) { + const compact = stderr.replace(/\s+/g, ' ').trim(); + const lower = compact.toLowerCase(); + const excerpt = compact.length > 320 ? `${compact.slice(0, 317)}...` : compact; + + if (!compact) { + return { classification: 'unknown', summary: 'ffmpeg가 이유를 남기지 않고 종료됐습니다.', excerpt: null }; + } + if (lower.includes('permission denied') || lower.includes('access is denied')) { + return { classification: 'permission-denied', summary: '오디오 캡처 접근이 거부됐습니다.', excerpt }; + } + if (lower.includes('device not found') || lower.includes('could not find audio device') || lower.includes('no such device')) { + return { classification: 'device-not-found', summary: 'Windows 오디오 출력 장치를 찾지 못했습니다.', excerpt }; + } + if (lower.includes('invalid argument') || lower.includes('[in#0') || lower.includes('could not open input') || lower.includes('i/o error')) { + return { classification: 'input-open-failed', summary: 'ffmpeg가 Windows 오디오 입력을 열지 못했습니다.', excerpt }; + } + return { classification: 'unknown', summary: 'Windows 오디오 캡처가 실패했습니다.', excerpt }; +} + +interface CaptureCommand { + readonly backendLabel: string; + readonly deviceLabel: string; + readonly command: string; + readonly args: string[]; +} + +interface MacSystemAudioHelperCommand { + readonly backendLabel: string; + readonly deviceLabel: string; + readonly command: string; + readonly args: string[]; + readonly env: NodeJS.ProcessEnv; +} + +interface MacCaptureHelperReadyPayload { + event?: string; + message?: string; + name?: string; + backend?: string; + backendLabel?: string; + deviceLabel?: string; + aggregateDeviceUID?: string; + aggregateDeviceName?: string; + outputDeviceUID?: string; + outputDeviceName?: string; + outputDirectory?: string; +} + +interface MacAudioDevice { + index: string; + label: string; +} + +interface MacAudioDiagnostics { + devices: MacAudioDevice[]; + selectedDevice: MacAudioDevice | null; + requestedDevice: string | null; + inputDeviceCount: number; + virtualDeviceCount: number; +} + +interface WindowsAudioDevice { + id: string; + label: string; + kind: 'render' | 'capture' | 'unknown'; + isLoopback: boolean; + isDefault: boolean; +} + +interface WindowsAudioDiagnostics { + devices: WindowsAudioDevice[]; + selectedDevice: WindowsAudioDevice | null; + requestedDevice: string | null; +} + +export interface LiveCaptureProbeResult { + platform: HostPlatform; + toolchain: { + ffmpeg: string; + whisper: string; + }; + requestedDevice: string | null; + devices: Array<{ + index: string; + label: string; + }>; + ready: boolean; + readyDetail: { + deviceLabel: string; + backendLabel: string; + command: string; + } | null; + chunkCount: number; + transcriptPreview: string | null; + errors: string[]; + exitedUnexpectedly: boolean; +} + +interface WhisperCommand { + readonly command: string; + readonly args: string[]; +} + +interface ResolvedToolchain { + ffmpeg: string; + whisper: string; + whisperModel: string | null; +} + +interface PythonRuntime { + command: string; + args: string[]; +} + +interface DistilBackend { + runtime: PythonRuntime; + modelPath: string; + scriptPath: string; +} + +function getCommandLookupBinary() { + return process.platform === 'win32' ? 'where' : 'which'; +} + +async function resolveExecutableFromPath(name: string) { + const result = await execFileAsync(getCommandLookupBinary(), [name], { + timeout: 5_000, + windowsHide: true + }).catch(() => null); + + return result?.stdout + ?.split(/\r?\n/g) + .map((line) => line.trim()) + .find(Boolean) + ?? null; +} + +function quoteArg(value: string) { + if (!/[^\w./:-]/.test(value)) { + return value; + } + return JSON.stringify(value); +} + +function getSegmentSeconds() { + const raw = Number.parseInt(process.env.TRANSLATOR_CAPTURE_SEGMENT_SECONDS ?? '', 10); + if (!Number.isFinite(raw) || raw < 2 || raw > 15) { + return 4; + } + return raw; +} + +function getWhisperModel() { + return process.env.WHISPER_MODEL?.trim() || 'turbo'; +} + +function getExplicitWhisperModelPath() { + return process.env.WHISPER_MODEL_PATH?.trim() || null; +} + +function isWhisperCppBinary(command: string) { + return /(?:whisper(?:-cli)?|main)(?:\.exe)?$/i.test(path.basename(command)); +} + +function getWhisperThreads() { + const raw = Number.parseInt(process.env.WHISPER_THREADS ?? '', 10); + if (!Number.isFinite(raw) || raw <= 0) { + return Math.max(1, Math.min(4, os.cpus().length || 1)); + } + return raw; +} + +function getWhisperLanguage() { + return process.env.WHISPER_LANGUAGE?.trim() || 'Japanese'; +} + +function getPreferredSttBackend() { + return process.env.TRANSLATOR_STT_BACKEND?.trim() || 'auto'; +} + +function getMacAudioDevice() { + return process.env.MACOS_TRANSLATOR_CAPTURE_DEVICE?.trim() || ':0'; +} + +function normalizeMacDeviceLabel(label: string) { + return label + .replace(/\s*\(.*?\)\s*$/g, '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +} + +function isLikelyVirtualMacInput(label: string) { + return /(blackhole|loopback|background music|soundflower|vb-?cable|multi-output|aggregate|rogue amoeba|macfuse)/i.test(label); +} + +function rankMacAudioDevice(device: MacAudioDevice) { + const label = device.label; + let score = 0; + if (isLikelyVirtualMacInput(label)) score += 100; + if (/blackhole/i.test(label)) score += 30; + if (/loopback/i.test(label)) score += 20; + if (/background music/i.test(label)) score += 15; + if (/built-?in|internal|microphone|mic|macbook/i.test(label)) score -= 5; + return score; +} + +function resolveMacAudioDevice(devices: MacAudioDevice[], requestedDevice: string | null) { + if (devices.length === 0) { + return null; + } + + if (requestedDevice && requestedDevice !== ':0') { + const exact = devices.find((device) => `:${device.index}` === requestedDevice || device.label === requestedDevice); + if (exact) { + return exact; + } + + const normalizedRequested = normalizeMacDeviceLabel(requestedDevice.replace(/^:/, '')); + return devices.find((device) => normalizeMacDeviceLabel(device.label) === normalizedRequested) ?? null; + } + + const ranked = [...devices].sort((left, right) => rankMacAudioDevice(right) - rankMacAudioDevice(left)); + return ranked[0] ?? devices[0]; +} + +function formatMacAudioSelectionError(diagnostics: MacAudioDiagnostics) { + const { devices, requestedDevice, virtualDeviceCount } = diagnostics; + if (devices.length === 0) { + return 'macOS가 현재 이 앱에 노출한 오디오 입력 장치가 없습니다. 권한 문제라기보다 입력 장치 목록이 비어 있는 상태입니다.'; + } + + if (requestedDevice && requestedDevice !== ':0') { + return `설정한 mac 오디오 입력(${requestedDevice})을 찾지 못했습니다. 현재 입력 ${devices.length}개만 확인됐습니다.`; + } + + if (virtualDeviceCount === 0) { + return '현재 구현은 mac 시스템 내부 소리를 직접 못 받고, 입력 장치만 읽고 있습니다. 이 경로 자체를 바꿔야 합니다.'; + } + + return '현재 구현은 mac 입력 장치 선택까지만 되어 있고, 내부 오디오 직접 캡처 경로는 아직 없습니다.'; +} + +function getWindowsAudioDevice() { + return process.env.WINDOWS_TRANSLATOR_CAPTURE_DEVICE?.trim() || 'default'; +} + +function normalizeWindowsDeviceToken(value: string) { + return value + .replace(/^audio=/i, '') + .replace(/^"|"$/g, '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +} + +function isLikelyManualWindowsVirtualDevice(label: string) { + return /(stereo mix|vb-?cable|virtual|loopback cable|cable output|voicemeeter|line \d)/i.test(label); +} + +export async function probeWindowsAudioDevices(ffmpegPath?: string) { + const command = ffmpegPath ?? await resolveExecutable('ffmpeg'); + if (!command) { + throw new Error(formatToolchainError('ffmpeg')); + } + + const result = await execFileAsync(command, [ + '-hide_banner', + '-f', + 'wasapi', + '-list_devices', + 'true', + '-i', + 'dummy' + ], { + timeout: 10_000, + windowsHide: true + }).catch((error: { stdout?: string; stderr?: string } | null) => error ?? null); + + const rawOutput = [result?.stdout ?? '', result?.stderr ?? ''].join('\n'); + const devices: WindowsAudioDevice[] = []; + const seen = new Set(); + + for (const line of rawOutput.split(/\r?\n/g)) { + const quoted = [...line.matchAll(/"([^"]+)"/g)].map((match) => match[1]?.trim()).filter(Boolean) as string[]; + if (quoted.length === 0) { + continue; + } + + const lower = line.toLowerCase(); + const kind: WindowsAudioDevice['kind'] = lower.includes('loopback') || lower.includes('render') || lower.includes('output') + ? 'render' + : lower.includes('capture') || lower.includes('input') + ? 'capture' + : 'unknown'; + + for (const label of quoted) { + const key = normalizeWindowsDeviceToken(label); + if (!key || seen.has(key)) { + continue; + } + seen.add(key); + devices.push({ + id: label, + label, + kind, + isLoopback: /loopback/i.test(line) || /\(loopback\)/i.test(label), + isDefault: /default/i.test(line) + }); + } + } + + return devices; +} + +export async function probeWindowsDshowAudioDevices(ffmpegPath?: string) { + const command = ffmpegPath ?? await resolveExecutable('ffmpeg'); + if (!command) { + throw new Error(formatToolchainError('ffmpeg')); + } + + const result = await execFileAsync(command, [ + '-hide_banner', + '-f', + 'dshow', + '-list_devices', + 'true', + '-i', + 'dummy' + ], { + timeout: 10_000, + windowsHide: true + }).catch((error: { stdout?: string; stderr?: string } | null) => error ?? null); + + const rawOutput = [result?.stdout ?? '', result?.stderr ?? ''].join('\n'); + const devices: WindowsAudioDevice[] = []; + let inAudioSection = false; + + for (const line of rawOutput.split(/\r?\n/g)) { + if (/DirectShow audio devices/i.test(line)) { + inAudioSection = true; + continue; + } + if (/DirectShow video devices/i.test(line)) { + inAudioSection = false; + continue; + } + if (!inAudioSection) { + const inlineAudioMatch = line.match(/"([^"]+)"\s+\(audio\)/i); + if (!inlineAudioMatch?.[1]) { + continue; + } + const label = inlineAudioMatch[1].trim(); + devices.push({ + id: label, + label, + kind: isLikelyManualWindowsVirtualDevice(label) || /stereo mix|what u hear|wave out/i.test(label) ? 'render' : 'capture', + isLoopback: /stereo mix|what u hear|wave out|loopback|virtual-audio-capturer/i.test(label), + isDefault: false + }); + continue; + } + const match = line.match(/"([^"]+)"/); + if (!match?.[1]) { + continue; + } + const label = match[1].trim(); + devices.push({ + id: label, + label, + kind: isLikelyManualWindowsVirtualDevice(label) || /stereo mix|what u hear|wave out/i.test(label) ? 'render' : 'capture', + isLoopback: /stereo mix|what u hear|wave out|loopback|virtual-audio-capturer/i.test(label), + isDefault: false + }); + } + + return devices; +} + +function rankWindowsAudioDevice(device: WindowsAudioDevice) { + let score = 0; + if (device.kind === 'render') score += 100; + if (device.isLoopback) score += 80; + if (device.isDefault) score += 40; + if (isLikelyManualWindowsVirtualDevice(device.label)) score -= 30; + if (/speaker|headphone|headset|realtek|output/i.test(device.label)) score += 10; + if (/microphone|mic|input/i.test(device.label)) score -= 50; + return score; +} + +function resolveWindowsAudioDevice(devices: WindowsAudioDevice[], requestedDevice: string | null) { + if (requestedDevice && requestedDevice !== 'default') { + const normalizedRequested = normalizeWindowsDeviceToken(requestedDevice); + return devices.find((device) => normalizeWindowsDeviceToken(device.id) === normalizedRequested || normalizeWindowsDeviceToken(device.label) === normalizedRequested) ?? null; + } + + if (devices.length === 0) { + return null; + } + + const ranked = [...devices].sort((left, right) => rankWindowsAudioDevice(right) - rankWindowsAudioDevice(left)); + return ranked.find((device) => device.kind === 'render' || device.isLoopback) ?? ranked[0] ?? null; +} + +function formatWindowsAudioSelectionError(diagnostics: WindowsAudioDiagnostics) { + const { devices, requestedDevice } = diagnostics; + if (devices.length === 0) { + return 'Windows 재생 장치 목록을 읽지 못했습니다. 기본 출력 장치가 정상인지 확인한 뒤 다시 시도하세요.'; + } + + if (requestedDevice && requestedDevice !== 'default') { + return `설정한 Windows 캡처 장치(${requestedDevice})를 찾지 못했습니다. 기본값으로 비워 두면 현재 기본 출력 장치를 자동으로 따라갑니다.`; + } + + return 'Windows 기본 출력 장치를 loopback 대상으로 고르지 못했습니다. 가상 장치 대신 기본 스피커/헤드폰 출력을 확인하세요.'; +} + +function formatWindowsDshowLoopbackMissingError(devices: WindowsAudioDevice[]) { + const names = devices.map((device) => device.label).join(', ') || '없음'; + return [ + '현재 번들 ffmpeg는 WASAPI loopback을 지원하지 않아 DirectShow 캡처로 대체했습니다.', + '하지만 DirectShow에서 Stereo Mix, What U Hear, virtual-audio-capturer 같은 재생 loopback 장치를 찾지 못했습니다.', + `감지된 DirectShow 오디오 장치: ${names}`, + 'Windows 시스템 소리를 직접 받으려면 WASAPI 지원 캡처 경로가 필요하거나, Stereo Mix/가상 오디오 캡처 장치를 활성화해야 합니다.' + ].join(' '); +} + +function getExecutableCandidates(name: 'ffmpeg' | 'whisper') { + if (name === 'ffmpeg') { + return process.platform === 'win32' ? ['ffmpeg.exe'] : ['ffmpeg']; + } + return process.platform === 'win32' ? ['whisper-cli.exe', 'whisper.exe', 'main.exe'] : ['whisper', 'whisper-cli', 'main']; +} + +function getExplicitExecutablePath(name: 'ffmpeg' | 'whisper') { + const envKey = name === 'ffmpeg' ? 'FFMPEG_PATH' : 'WHISPER_PATH'; + return process.env[envKey]?.trim() || null; +} + +function getResourceRoots() { + const roots = [ + process.resourcesPath, + process.cwd(), + path.join(process.cwd(), 'resources'), + path.dirname(process.execPath) + ].filter((value): value is string => Boolean(value)); + return [...new Set(roots)]; +} + +function getBundledDistilModelPath() { + const explicit = process.env.DISTIL_WHISPER_MODEL_PATH?.trim(); + if (explicit && existsSync(explicit)) { + return explicit; + } + + const candidates = [ + ...getResourceRoots().map((root) => path.join(root, 'models', process.platform, process.arch, 'distil-large-v3-ct2')), + path.join(process.cwd(), 'vendor', 'models', process.platform, process.arch, 'distil-large-v3-ct2') + ]; + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +function getBundledTranscribeScriptPath() { + const candidates = [ + ...getResourceRoots().map((root) => path.join(root, 'python', 'transcribe_distil_faster_whisper.py')), + path.join(process.cwd(), 'scripts', 'transcribe_distil_faster_whisper.py') + ]; + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +function getBundledPythonCommand() { + const candidates = process.platform === 'win32' + ? [ + ...getResourceRoots().map((root) => path.join(root, 'python', process.platform, process.arch, 'Scripts', 'python.exe')), + path.join(process.cwd(), 'vendor', 'python', process.platform, process.arch, 'Scripts', 'python.exe') + ] + : [ + ...getResourceRoots().map((root) => path.join(root, 'python', process.platform, process.arch, 'bin', 'python3')), + ...getResourceRoots().map((root) => path.join(root, 'python', process.platform, process.arch, 'bin', 'python')), + path.join(process.cwd(), 'vendor', 'python', process.platform, process.arch, 'bin', 'python3'), + path.join(process.cwd(), 'vendor', 'python', process.platform, process.arch, 'bin', 'python') + ]; + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +async function resolvePythonCommand() { + const bundled = getBundledPythonCommand(); + if (bundled) { + return { command: bundled, args: [] }; + } + + const fromPath = await resolveExecutableFromPath(process.platform === 'win32' ? 'python.exe' : 'python3'); + if (fromPath) { + return { command: fromPath, args: [] }; + } + + if (process.platform !== 'win32') { + const alt = await resolveExecutableFromPath('python'); + if (alt) { + return { command: alt, args: [] }; + } + } + + return null; +} + +async function resolveDistilBackend(): Promise { + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return null; + } + + const modelPath = getBundledDistilModelPath(); + const scriptPath = getBundledTranscribeScriptPath(); + const runtime = await resolvePythonCommand(); + if (!modelPath || !scriptPath || !runtime) { + return null; + } + + return { + runtime, + modelPath, + scriptPath + }; +} + +function getBundledExecutableCandidatePaths(name: 'ffmpeg' | 'whisper') { + const executables = getExecutableCandidates(name); + const archCandidates = [...new Set([process.arch, 'arm64', 'x64'])]; + const candidates = executables.flatMap((executable) => [ + ...getResourceRoots().flatMap((root) => [ + ...archCandidates.map((arch) => path.join(root, 'bin', process.platform, arch, executable)), + path.join(root, 'bin', executable), + path.join(root, 'b', 'wx', executable) + ]), + ...archCandidates.map((arch) => path.join(process.cwd(), 'vendor', 'bin', process.platform, arch, executable)), + ...archCandidates.map((arch) => path.join(process.cwd(), 'bin', process.platform, arch, executable)), + path.join(process.cwd(), 'vendor', 'bin', 'win32', 'x64', executable) + ]); + + return [...new Set(candidates)]; +} + +async function resolveExecutable(name: 'ffmpeg' | 'whisper') { + const explicit = getExplicitExecutablePath(name); + if (explicit && existsSync(explicit)) { + return explicit; + } + + for (const candidate of getBundledExecutableCandidatePaths(name)) { + if (existsSync(candidate)) { + return candidate; + } + } + + for (const executable of getExecutableCandidates(name)) { + const resolved = await resolveExecutableFromPath(executable); + if (resolved) { + return resolved; + } + } + + return null; +} + +async function ffmpegSupportsInputDevice(command: string, inputDevice: string) { + const result = await execFileAsync(command, ['-hide_banner', '-devices'], { + timeout: 5_000, + windowsHide: true + }).catch((error: { stdout?: string; stderr?: string } | null) => error ?? null); + const output = [result?.stdout ?? '', result?.stderr ?? ''].join('\n'); + return new RegExp(`\\b${inputDevice}\\b`, 'i').test(output); +} + +function resolveBundledModelPath() { + const explicit = getExplicitWhisperModelPath(); + if (explicit && existsSync(explicit)) { + return explicit; + } + + const names = ['ggml-base.bin', 'ggml-turbo.bin']; + const candidates = names.flatMap((name) => [ + ...getResourceRoots().map((root) => path.join(root, 'bin', process.platform, process.arch, name)), + ...getResourceRoots().map((root) => path.join(root, 'bin', process.platform, 'x64', name)), + ...getResourceRoots().map((root) => path.join(root, 'bin', process.platform, 'arm64', name)), + ...getResourceRoots().map((root) => path.join(root, 'bin', name)), + path.join(process.cwd(), 'vendor', 'bin', process.platform, process.arch, name) + ]); + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +function hasBundledModelForPlatform(platform: HostPlatform) { + const names = ['ggml-base.bin', 'ggml-turbo.bin']; + const archCandidates = platform === 'win32' ? ['x64', process.arch] : [process.arch, 'arm64', 'x64']; + const candidates = names.flatMap((name) => [ + ...getResourceRoots().flatMap((root) => archCandidates.map((arch) => path.join(root, 'bin', platform, arch, name))), + ...archCandidates.map((arch) => path.join(process.cwd(), 'vendor', 'bin', platform, arch, name)) + ]); + return candidates.some((candidate) => existsSync(candidate)); +} + +function formatMissingModelError(platform: HostPlatform) { + if (getExplicitWhisperModelPath()) { + return '지정한 음성 인식 모델 파일을 찾지 못했습니다. 설정의 모델 경로를 확인한 뒤 다시 시도하세요.'; + } + if (platform === 'darwin' && hasBundledModelForPlatform('win32') && !hasBundledModelForPlatform('darwin')) { + return '현재 mac 호스트에는 실오디오 검증용 ggml 모델이 없습니다. mac용 모델 경로를 설정하거나 mac용 번들 리소스를 추가하세요.'; + } + if (platform === 'win32' && hasBundledModelForPlatform('darwin') && !hasBundledModelForPlatform('win32')) { + return '현재 Windows 호스트에는 실오디오 검증용 ggml 모델이 없습니다. Windows용 모델 경로를 설정하거나 Windows용 번들 리소스를 추가하세요.'; + } + return '음성 인식 모델 파일(ggml-base.bin)을 찾지 못했습니다. 압축을 다시 풀거나 모델 경로를 설정하세요.'; +} + +function formatToolchainError(name: 'ffmpeg' | 'whisper') { + if (name === 'ffmpeg') { + if (process.platform === 'win32') { + return '오디오 캡처 도구(ffmpeg)를 열지 못했습니다. 앱을 다시 압축 해제한 뒤 다시 실행하세요.'; + } + return '오디오 캡처 도구(ffmpeg)를 찾지 못했습니다. ffmpeg를 설치한 뒤 다시 실행하세요.'; + } + + if (process.platform === 'win32') { + return '음성 인식 도구(whisper.cpp)를 찾지 못했습니다. 압축을 다시 풀고 실행하세요.'; + } + + return '음성 인식 도구(whisper)를 찾지 못했습니다. whisper를 설치한 뒤 다시 실행하세요.'; +} + +export async function probeMacAudioDevices(ffmpegPath?: string) { + const command = ffmpegPath ?? await resolveExecutable('ffmpeg'); + if (!command) { + throw new Error(formatToolchainError('ffmpeg')); + } + + const result = await execFileAsync(command, [ + '-hide_banner', + '-f', + 'avfoundation', + '-list_devices', + 'true', + '-i', + '' + ], { + timeout: 10_000, + windowsHide: true + }).catch((error: { stdout?: string; stderr?: string } | null) => error ?? null); + + const rawOutput = [result?.stdout ?? '', result?.stderr ?? ''].join('\n'); + const devices: MacAudioDevice[] = []; + const lines = rawOutput.split(/\r?\n/g); + + let inAudioSection = false; + + for (const line of lines) { + if (line.includes('AVFoundation audio devices')) { + inAudioSection = true; + continue; + } + if (inAudioSection && line.includes('AVFoundation video devices')) { + continue; + } + if (!inAudioSection) { + continue; + } + const match = line.match(/\[([0-9]+)\]\s+(.+)$/); + if (!match) { + continue; + } + devices.push({ + index: match[1], + label: match[2].trim() + }); + } + + return devices; +} + +async function createCaptureCommand(platform: HostPlatform, outputPattern: string, toolchain: ResolvedToolchain): Promise { + if (platform === 'win32') { + const requestedDevice = getWindowsAudioDevice(); + const supportsWasapi = await ffmpegSupportsInputDevice(toolchain.ffmpeg, 'wasapi'); + const devices = supportsWasapi + ? await probeWindowsAudioDevices(toolchain.ffmpeg).catch(() => []) + : await probeWindowsDshowAudioDevices(toolchain.ffmpeg).catch(() => []); + const selectedDevice = resolveWindowsAudioDevice(devices, requestedDevice); + if (!supportsWasapi && !selectedDevice?.isLoopback) { + throw new Error(formatWindowsDshowLoopbackMissingError(devices)); + } + if (!selectedDevice && devices.length > 0 && requestedDevice !== 'default') { + throw new Error(formatWindowsAudioSelectionError({ + devices, + selectedDevice, + requestedDevice + })); + } + + const inputDevice = selectedDevice?.id ?? requestedDevice; + const normalizedInputDevice = inputDevice.toLowerCase() === 'default' ? 'default' : `audio=${inputDevice}`; + const dshowInputDevice = selectedDevice?.id ? `audio=${selectedDevice.id}` : 'audio=virtual-audio-capturer'; + const args = [ + '-hide_banner', + '-loglevel', + 'level+info', + '-fflags', + '+genpts', + '-thread_queue_size', + '4096', + '-f', + supportsWasapi ? 'wasapi' : 'dshow', + ...(supportsWasapi ? ['-loopback', '1'] : []), + '-i', + supportsWasapi ? normalizedInputDevice : dshowInputDevice, + '-vn', + '-ac', + '1', + '-ar', + '16000', + '-c:a', + 'pcm_s16le', + '-f', + 'segment', + '-segment_time', + String(getSegmentSeconds()), + '-reset_timestamps', + '1', + outputPattern + ]; + + return { + backendLabel: supportsWasapi ? 'Windows WASAPI Loopback' : 'Windows DirectShow Audio', + deviceLabel: selectedDevice?.label ?? (supportsWasapi && requestedDevice === 'default' ? 'Default Windows Output (WASAPI loopback)' : requestedDevice), + command: toolchain.ffmpeg, + args + }; + } + + if (platform === 'darwin') { + const requestedDevice = process.env.MACOS_TRANSLATOR_CAPTURE_DEVICE?.trim(); + if (!requestedDevice) { + throw new Error('macOS CoreAudio tap aggregate 장치 이름을 아직 받지 못했습니다.'); + } + + return { + backendLabel: 'macOS CoreAudio Tap', + deviceLabel: requestedDevice, + command: toolchain.ffmpeg, + args: [ + '-hide_banner', + '-loglevel', + 'warning', + '-f', + 'avfoundation', + '-i', + `:${requestedDevice}`, + '-vn', + '-ac', + '1', + '-ar', + '16000', + '-c:a', + 'pcm_s16le', + '-f', + 'segment', + '-segment_time', + String(getSegmentSeconds()), + '-reset_timestamps', + '1', + outputPattern + ] + }; + } + + throw new Error(`Live capture is not supported on platform ${platform}.`); +} + +function getMacScreenAudioHelperSourcePath() { + const resourcePath = typeof process.resourcesPath === 'string' && process.resourcesPath.length > 0 + ? process.resourcesPath + : null; + + const candidates = [ + path.join(process.cwd(), 'electron', 'native', 'macos-screen-audio-capture.swift'), + resourcePath ? path.join(resourcePath, 'app.asar', 'electron', 'native', 'macos-screen-audio-capture.swift') : null, + resourcePath ? path.join(resourcePath, 'electron', 'native', 'macos-screen-audio-capture.swift') : null + ].filter((candidate): candidate is string => Boolean(candidate)); + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +async function resolveMacScreenAudioHelperBinary() { + const sourcePath = getMacScreenAudioHelperSourcePath(); + if (!sourcePath) { + throw new Error('macOS ScreenCaptureKit helper 소스 파일을 찾지 못했습니다.'); + } + + const compiler = await resolveExecutableFromPath('swiftc') ?? await resolveExecutableFromPath('xcrun'); + if (!compiler) { + throw new Error('macOS ScreenCaptureKit helper를 컴파일할 swiftc를 찾지 못했습니다. Xcode Command Line Tools를 설치하세요.'); + } + + const helperDirectory = path.join(os.tmpdir(), 'jp-ko-translator-native'); + await mkdir(helperDirectory, { recursive: true }); + const binaryPath = path.join(helperDirectory, 'macos-screen-audio-capture-helper'); + const args = path.basename(compiler) === 'xcrun' + ? ['swiftc', sourcePath, '-o', binaryPath] + : [sourcePath, '-o', binaryPath]; + + await execFileAsync(compiler, args, { + timeout: 120_000, + maxBuffer: 1024 * 1024 * 8, + windowsHide: true + }).catch((error: { stderr?: string; message?: string }) => { + throw new Error(`macOS ScreenCaptureKit helper를 컴파일하지 못했습니다. ${error?.stderr?.trim() || error?.message || 'swiftc failed'}`); + }); + + return binaryPath; +} + +async function createMacScreenAudioHelperCommand(outputDirectory: string): Promise { + const helperBinary = await resolveMacScreenAudioHelperBinary(); + return { + backendLabel: 'macOS ScreenCaptureKit Native', + deviceLabel: 'System Audio (ScreenCaptureKit)', + command: helperBinary, + args: ['--output-dir', outputDirectory, '--segment-seconds', String(getSegmentSeconds())], + env: { + ...process.env + } + }; +} + +async function convertAudioForTranscription(sourcePath: string, toolchain: ResolvedToolchain, tempDirectory: string) { + if (sourcePath.endsWith('.wav')) { + return sourcePath; + } + + const convertedPath = path.join(tempDirectory, `${path.basename(sourcePath, path.extname(sourcePath))}.wav`); + await execFileAsync(toolchain.ffmpeg, [ + '-hide_banner', + '-loglevel', + 'warning', + '-y', + '-i', + sourcePath, + '-ac', + '1', + '-ar', + '16000', + '-c:a', + 'pcm_s16le', + convertedPath + ], { + timeout: 120_000, + maxBuffer: 1024 * 1024 * 8, + windowsHide: true + }); + + return convertedPath; +} + +function createWhisperCommand(audioPath: string, outputDirectory: string, toolchain: ResolvedToolchain): WhisperCommand { + if (isWhisperCppBinary(toolchain.whisper)) { + const modelPath = toolchain.whisperModel ?? getWhisperModel(); + return { + command: toolchain.whisper, + args: [ + '-m', + modelPath, + '-f', + audioPath, + '-l', + 'ja', + '-oj', + '-of', + path.join(outputDirectory, path.basename(audioPath, path.extname(audioPath))), + '-t', + String(getWhisperThreads()) + ] + }; + } + + return { + command: toolchain.whisper, + args: [ + '--model', + getWhisperModel(), + '--device', + process.env.WHISPER_DEVICE?.trim() || 'cpu', + '--task', + 'transcribe', + '--language', + getWhisperLanguage(), + '--output_format', + 'json', + '--output_dir', + outputDirectory, + '--threads', + String(getWhisperThreads()), + '--verbose', + 'False', + audioPath + ] + }; +} + +async function readWhisperText(outputDirectory: string, audioPath: string) { + const jsonPath = path.join(outputDirectory, `${path.basename(audioPath, path.extname(audioPath))}.json`); + const raw = await readFile(jsonPath, 'utf8'); + const parsed = JSON.parse(raw) as { text?: string }; + return parsed.text?.trim() || null; +} + +export async function verifyLiveCaptureToolchain(platform: HostPlatform): Promise { + if (platform !== 'win32' && platform !== 'darwin') { + throw new Error(`Live capture is not supported on ${platform}.`); + } + + const ffmpeg = await resolveExecutable('ffmpeg'); + if (!ffmpeg) { + throw new Error(formatToolchainError('ffmpeg')); + } + if (platform === 'win32' + && !(await ffmpegSupportsInputDevice(ffmpeg, 'wasapi')) + && !(await ffmpegSupportsInputDevice(ffmpeg, 'dshow'))) { + throw new Error('번들된 ffmpeg가 Windows 오디오 입력 장치를 지원하지 않습니다. ffmpeg Windows 빌드를 다시 준비하세요.'); + } + + const preferredBackend = getPreferredSttBackend(); + if ((preferredBackend === 'auto' || preferredBackend === 'distil-faster-whisper') && platform === 'darwin' && process.arch === 'arm64') { + const distilBackend = await resolveDistilBackend(); + if (distilBackend) { + return { + ffmpeg, + whisper: `${distilBackend.runtime.command} ${distilBackend.runtime.args.join(' ')} :: distil-faster-whisper`, + whisperModel: distilBackend.modelPath + }; + } + if (preferredBackend === 'distil-faster-whisper') { + throw new Error('distil-large-v3 백엔드를 강제했지만, 번들 Python 런타임 또는 모델 디렉터리를 찾지 못했습니다.'); + } + } + + const whisper = await resolveExecutable('whisper'); + if (!whisper) { + throw new Error(formatToolchainError('whisper')); + } + + const whisperModel = resolveBundledModelPath(); + if (isWhisperCppBinary(whisper) && !whisperModel) { + throw new Error(formatMissingModelError(platform)); + } + + return { + ffmpeg, + whisper, + whisperModel + }; +} + +export async function runLiveCaptureProbe( + platform: HostPlatform, + options: { + timeoutMs?: number; + } = {} +): Promise { + const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 8_000; + const toolchain = await verifyLiveCaptureToolchain(platform); + const devices = platform === 'darwin' ? [] : []; + let ready = false; + let exitedUnexpectedly = false; + let transcriptPreview: string | null = null; + let chunkCount = 0; + let readyDetail: LiveCaptureProbeResult['readyDetail'] = null; + const errors: string[] = []; + const hooks: LiveCaptureHooks = { + onCaptureReady: async () => undefined, + onCaptureExit: async () => undefined, + onCaptureError: async () => undefined + }; + const provider = new LiveAudioSttProvider(platform, hooks); + + hooks.onCaptureReady = async (detail) => { + ready = true; + readyDetail = detail; + }; + hooks.onCaptureExit = async (message) => { + exitedUnexpectedly = true; + errors.push(message); + }; + hooks.onCaptureError = async (message) => { + errors.push(message); + }; + + try { + await provider.start(async (chunk) => { + chunkCount += 1; + if (!transcriptPreview && chunk.text.trim()) { + transcriptPreview = chunk.text.trim(); + } + }); + + await new Promise((resolve) => setTimeout(resolve, timeoutMs)); + } finally { + await provider.stop().catch(() => undefined); + } + + return { + platform, + toolchain, + requestedDevice: platform === 'darwin' ? getMacAudioDevice() : platform === 'win32' ? getWindowsAudioDevice() : null, + devices, + ready, + readyDetail, + chunkCount, + transcriptPreview, + errors, + exitedUnexpectedly + }; +} + +export function applyLiveCaptureReadySnapshot( + snapshot: CaptureSnapshot, + detail: { + backendLabel: string; + deviceLabel: string; + backend: CaptureSnapshot['backend']; + } +): CaptureSnapshot { + return { + ...snapshot, + phase: 'capturing', + deviceLabel: detail.deviceLabel, + backendLabel: detail.backendLabel, + backend: detail.backend, + lastError: null, + guidance: `${detail.backendLabel}가 실제 입력선을 열었습니다. 현재 장치: ${detail.deviceLabel}` + }; +} + +export class StreamingWhisperSttProvider { + readonly key = 'live-capture.win32.whisper-stream'; + private readonly platform: HostPlatform; + private readonly hooks: LiveCaptureHooks; + private streamProcess: ChildProcess | null = null; + private cancelled = false; + private sequence = 0; + private lastText = ''; + private stdoutBuffer = ''; + + constructor(platform: HostPlatform, hooks: LiveCaptureHooks) { + this.platform = platform; + this.hooks = hooks; + } + + async start(onChunk: (chunk: LiveTranscriptChunk) => Promise) { + if (this.platform !== 'win32') { + throw new Error('whisper.cpp stream.exe STT is currently supported on Windows only.'); + } + + const toolchain = await verifyLiveCaptureToolchain(this.platform); + const streamPath = getWhisperStreamExecutablePath(); + if (!streamPath) { + throw new Error('whisper.cpp stream.exe를 찾지 못했습니다. Windows STT 준비 스크립트를 다시 실행하세요.'); + } + + const modelPath = toolchain.whisperModel ?? getWhisperModel(); + const args = [ + '-m', + modelPath, + '-l', + 'ja', + '--step', + process.env.WHISPER_STREAM_STEP_MS?.trim() || '1000', + '--length', + process.env.WHISPER_STREAM_LENGTH_MS?.trim() || '6000', + '--keep', + process.env.WHISPER_STREAM_KEEP_MS?.trim() || '1000', + '-t', + String(getWhisperThreads()), + '-kc' + ]; + + const requestedDevice = process.env.WHISPER_STREAM_CAPTURE_DEVICE?.trim(); + if (requestedDevice) { + args.push('-c', requestedDevice); + } + + this.cancelled = false; + this.streamProcess = spawn(streamPath, args, { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + env: { + ...process.env + } + }); + + await this.hooks.onCaptureReady({ + deviceLabel: requestedDevice ? `SDL capture device #${requestedDevice}` : 'Default SDL capture device', + backendLabel: 'Whisper.cpp Streaming STT', + command: [streamPath, ...args].map(quoteArg).join(' ') + }); + + this.streamProcess.stdout?.on('data', (chunk) => { + this.consumeStdout(String(chunk), onChunk); + }); + + this.streamProcess.stderr?.on('data', (chunk) => { + const text = stripTerminalControl(String(chunk)).trim(); + if (/found \d+ capture devices|Capture device|obtained spec/i.test(text)) { + void this.hooks.onCaptureReady({ + deviceLabel: requestedDevice ? `SDL capture device #${requestedDevice}` : 'Default SDL capture device', + backendLabel: 'Whisper.cpp Streaming STT', + command: [streamPath, ...args].map(quoteArg).join(' ') + }); + } + }); + + this.streamProcess.once('error', (error) => { + void this.hooks.onCaptureError(`whisper.cpp stream.exe를 시작하지 못했습니다. ${error.message}`); + }); + + this.streamProcess.once('exit', (code, signal) => { + if (this.cancelled) { + return; + } + void this.hooks.onCaptureExit(signal + ? `whisper.cpp stream.exe가 ${signal}로 종료되었습니다.` + : `whisper.cpp stream.exe가 code ${code ?? 'unknown'}로 종료되었습니다.`); + }); + } + + async stop() { + this.cancelled = true; + if (this.streamProcess && !this.streamProcess.killed) { + this.streamProcess.kill('SIGTERM'); + } + this.streamProcess = null; + this.stdoutBuffer = ''; + this.lastText = ''; + } + + private consumeStdout(raw: string, onChunk: (chunk: LiveTranscriptChunk) => Promise) { + this.stdoutBuffer += stripTerminalControl(raw); + if (this.stdoutBuffer.length > 8000) { + this.stdoutBuffer = this.stdoutBuffer.slice(-8000); + } + + const parts = this.stdoutBuffer.split(/\n/g); + this.stdoutBuffer = parts.pop() ?? ''; + for (const part of parts) { + const text = part + .replace(/\[[^\]]*\]/g, '') + .replace(/\s+/g, ' ') + .trim(); + if (!text || /^start speaking$/i.test(text) || text === this.lastText) { + continue; + } + this.lastText = text; + this.sequence += 1; + void onChunk({ + id: `stream-${Date.now()}-${this.sequence}`, + text, + finalized: false + }); + } + } +} + +export class LiveAudioSttProvider { + readonly key: string; + private readonly platform: HostPlatform; + private readonly hooks: LiveCaptureHooks; + private readonly externalInput: boolean; + private captureProcess: ChildProcess | null = null; + private processingTimer: NodeJS.Timeout | null = null; + private tempDirectory: string | null = null; + private segmentDirectory: string | null = null; + private processedFiles = new Set(); + private cancelled = false; + private sequence = 0; + private isPolling = false; + private toolchain: ResolvedToolchain | null = null; + private distilBackend: DistilBackend | null = null; + private macHelperProcess: ChildProcess | null = null; + private onChunk: ((chunk: LiveTranscriptChunk) => Promise) | null = null; + private rollingTranscriptId: string | null = null; + private lastExternalTranscriptText = ''; + + constructor(platform: HostPlatform, hooks: LiveCaptureHooks, options: LiveAudioSttProviderOptions = {}) { + this.platform = platform; + this.hooks = hooks; + this.externalInput = options.externalInput ?? false; + this.key = this.externalInput ? `live-capture.${platform}.electron-mediarecorder+whisper` : `live-capture.${platform}.ffmpeg+whisper`; + } + + async start(onChunk: (chunk: LiveTranscriptChunk) => Promise) { + this.toolchain = await verifyLiveCaptureToolchain(this.platform); + this.distilBackend = await resolveDistilBackend(); + this.onChunk = onChunk; + + this.cancelled = false; + this.tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'jp-ko-live-capture-')); + this.segmentDirectory = path.join(this.tempDirectory, 'segments'); + await mkdir(this.segmentDirectory, { recursive: true }); + const outputPattern = path.join(this.segmentDirectory, 'chunk-%05d.wav'); + + if (this.externalInput) { + await this.hooks.onCaptureReady({ + deviceLabel: 'Electron system audio stream', + backendLabel: 'Electron Desktop Audio Bridge', + command: 'renderer MediaRecorder -> worker whisper' + }); + return; + } + + if (this.platform === 'darwin') { + const helper = await createMacScreenAudioHelperCommand(this.segmentDirectory); + this.macHelperProcess = spawn(helper.command, helper.args, { + env: helper.env, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }); + + let stdoutBuffer = ''; + let stderrBuffer = ''; + let captureStarted = false; + + this.macHelperProcess.stdout?.on('data', (chunk) => { + stdoutBuffer += String(chunk); + const lines = stdoutBuffer.split(/\r?\n/g); + stdoutBuffer = lines.pop() ?? ''; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith('{')) { + continue; + } + try { + const payload = JSON.parse(trimmed) as MacCaptureHelperReadyPayload; + if (payload.event === 'ready' && !captureStarted) { + captureStarted = true; + const deviceLabel = typeof payload.deviceLabel === 'string' ? payload.deviceLabel : helper.deviceLabel; + void this.hooks.onCaptureReady({ + deviceLabel, + backendLabel: typeof payload.backendLabel === 'string' ? payload.backendLabel : helper.backendLabel, + command: [helper.command, ...helper.args].map(quoteArg).join(' ') + }); + } + if (payload.event === 'error') { + const detail = [payload.message, payload.name].filter((value, index, array) => typeof value === 'string' && value.length > 0 && array.indexOf(value) === index).join(' '); + void this.hooks.onCaptureError(`macOS ScreenCaptureKit native 시스템 오디오 캡처를 시작하지 못했습니다. ${detail || 'ScreenCaptureKit 상태를 확인하세요.'}`); + } + } catch { + // ignore helper noise + } + } + }); + + this.macHelperProcess.stderr?.on('data', (chunk) => { + stderrBuffer += String(chunk); + }); + + this.macHelperProcess.once('error', (error) => { + void this.hooks.onCaptureError(`macOS ScreenCaptureKit helper를 시작하지 못했습니다. ${error.message}`); + }); + + this.macHelperProcess.once('exit', (code, signal) => { + if (this.cancelled) { + return; + } + const reason = stderrBuffer.trim() || (signal ? `signal ${signal}` : `exit code ${code ?? 'unknown'}`); + void this.hooks.onCaptureExit(`macOS ScreenCaptureKit helper가 중단됐습니다. ${reason}`); + }); + } else { + const capture = await createCaptureCommand(this.platform, outputPattern, this.toolchain); + this.captureProcess = spawn(capture.command, capture.args, { + stdio: ['ignore', 'ignore', 'pipe'], + windowsHide: true + }); + + let stderrBuffer = ''; + this.captureProcess.stderr?.on('data', (chunk) => { + stderrBuffer += String(chunk); + if (stderrBuffer.length > 12000) { + stderrBuffer = stderrBuffer.slice(-12000); + } + }); + + this.captureProcess.once('spawn', () => { + // wait for first real segment before declaring capture ready + }); + + this.captureProcess.once('error', (error) => { + void this.hooks.onCaptureError(`캡처를 시작하지 못했습니다. ${error.message}`); + }); + + this.captureProcess.once('exit', (code, signal) => { + if (this.cancelled) { + return; + } + const fallbackReason = signal ? `signal ${signal}` : `exit code ${code ?? 'unknown'}`; + if (this.platform === 'win32') { + const parsed = summarizeFfmpegWindowsError(stderrBuffer); + const reason = parsed.excerpt ? `${parsed.summary} ${parsed.excerpt}` : `${parsed.summary} ${fallbackReason}`; + void this.hooks.onCaptureExit(`오디오 캡처가 중단됐습니다. [${parsed.classification}] ${reason}`); + return; + } + const reason = stderrBuffer.trim() || fallbackReason; + void this.hooks.onCaptureExit(`오디오 캡처가 중단됐습니다. ${reason}`); + }); + } + + this.processingTimer = setInterval(() => { + void this.pollSegments(onChunk); + }, 1_000); + } + + async ingestExternalChunk(data: Uint8Array, mimeType: string) { + if (this.cancelled || !this.segmentDirectory || !this.onChunk) { + return; + } + + const extension = mimeType.includes('wav') + ? 'wav' + : mimeType.includes('mp4') || mimeType.includes('m4a') + ? 'm4a' + : 'webm'; + const filePath = path.join(this.segmentDirectory, `renderer-${Date.now()}-${this.sequence}.${extension}`); + await writeFile(filePath, data); + await this.pollSegments(this.onChunk); + } + + async stop() { + this.cancelled = true; + if (this.processingTimer) { + clearInterval(this.processingTimer); + this.processingTimer = null; + } + if (this.captureProcess && !this.captureProcess.killed) { + this.captureProcess.kill('SIGTERM'); + } + if (this.macHelperProcess && !this.macHelperProcess.killed) { + this.macHelperProcess.kill('SIGTERM'); + } + this.captureProcess = null; + this.macHelperProcess = null; + this.onChunk = null; + this.rollingTranscriptId = null; + this.lastExternalTranscriptText = ''; + if (this.tempDirectory) { + await rm(this.tempDirectory, { recursive: true, force: true }).catch(() => undefined); + } + this.tempDirectory = null; + this.segmentDirectory = null; + this.processedFiles.clear(); + } + + private async pollSegments(onChunk: (chunk: LiveTranscriptChunk) => Promise) { + if (this.cancelled || this.isPolling || !this.segmentDirectory) { + return; + } + + this.isPolling = true; + try { + let files = (await readdir(this.segmentDirectory)) + .filter((file) => (file.endsWith('.wav') || file.endsWith('.webm') || file.endsWith('.m4a')) && !file.endsWith('.part')) + .sort(); + + if (this.externalInput) { + const pendingFiles = files.filter((file) => !this.processedFiles.has(path.join(this.segmentDirectory!, file))); + const staleFiles = pendingFiles.slice(0, -1); + for (const staleFile of staleFiles) { + const stalePath = path.join(this.segmentDirectory, staleFile); + this.processedFiles.add(stalePath); + await rm(stalePath, { force: true }).catch(() => undefined); + } + files = pendingFiles.slice(-1); + } + + for (const file of files) { + const fullPath = path.join(this.segmentDirectory, file); + if (this.processedFiles.has(fullPath)) { + continue; + } + this.processedFiles.add(fullPath); + if (this.sequence === 0 && !this.externalInput) { + await this.hooks.onCaptureReady({ + deviceLabel: this.platform === 'win32' ? 'Default Windows Output (WASAPI loopback)' : 'Live Audio Input', + backendLabel: this.platform === 'win32' ? 'Windows WASAPI Loopback' : 'Live Audio Capture', + command: this.captureProcess ? [this.captureProcess.spawnfile, ...(this.captureProcess.spawnargs?.slice(1) ?? [])].map(quoteArg).join(' ') : 'capture-process' + }); + } + const text = await this.transcribe(fullPath); + if (!text) { + continue; + } + if (this.externalInput && text === this.lastExternalTranscriptText) { + continue; + } + if (this.externalInput) { + this.lastExternalTranscriptText = text; + this.rollingTranscriptId ??= `live-rolling-${Date.now()}`; + } + this.sequence += 1; + await onChunk({ + id: this.externalInput ? this.rollingTranscriptId! : `live-${Date.now()}-${this.sequence}`, + text, + finalized: !this.externalInput + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await this.hooks.onCaptureError(`오디오 처리 중 문제가 생겼습니다. ${message}`); + } finally { + this.isPolling = false; + } + } + + private async transcribe(audioPath: string) { + if (!this.tempDirectory || !this.toolchain) { + return null; + } + + let normalizedAudioPath = audioPath; + + try { + normalizedAudioPath = await convertAudioForTranscription(audioPath, this.toolchain, this.tempDirectory); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await this.hooks.onCaptureError(`오디오 형식 변환에 실패했습니다. ${message}`); + await rm(audioPath, { force: true }).catch(() => undefined); + return null; + } + + if (this.distilBackend && this.toolchain.whisper.includes('distil-faster-whisper')) { + return this.transcribeWithDistilBackend(normalizedAudioPath, audioPath); + } + + const outputDirectory = path.join(this.tempDirectory, `whisper-${path.basename(normalizedAudioPath, path.extname(normalizedAudioPath))}`); + await mkdir(outputDirectory, { recursive: true }); + const whisper = createWhisperCommand(normalizedAudioPath, outputDirectory, this.toolchain); + + try { + await execFileAsync(whisper.command, whisper.args, { + timeout: 120_000, + maxBuffer: 1024 * 1024 * 4, + windowsHide: true, + env: { + ...process.env, + OMP_NUM_THREADS: String(getWhisperThreads()) + } + }); + return await readWhisperText(outputDirectory, normalizedAudioPath); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await this.hooks.onCaptureError(`음성 인식에 실패했습니다. ${message}`); + return null; + } finally { + await rm(outputDirectory, { recursive: true, force: true }).catch(() => undefined); + if (normalizedAudioPath !== audioPath) { + await rm(normalizedAudioPath, { force: true }).catch(() => undefined); + } + await rm(audioPath, { force: true }).catch(() => undefined); + } + } + + private async transcribeWithDistilBackend(audioPath: string, originalAudioPath = audioPath) { + if (!this.tempDirectory || !this.distilBackend) { + return null; + } + + const outputPath = path.join(this.tempDirectory, `${path.basename(audioPath, path.extname(audioPath))}.distil.json`); + const runtimeArgs = [ + ...this.distilBackend.runtime.args, + this.distilBackend.scriptPath, + '--audio', + audioPath, + '--model-dir', + this.distilBackend.modelPath, + '--output', + outputPath, + '--language', + 'ja' + ]; + + try { + await execFileAsync(this.distilBackend.runtime.command, runtimeArgs, { + timeout: 120_000, + maxBuffer: 1024 * 1024 * 16, + windowsHide: true, + env: { + ...process.env, + OMP_NUM_THREADS: String(getWhisperThreads()) + } + }); + + const raw = await readFile(outputPath, 'utf8'); + const parsed = JSON.parse(raw) as { text?: string }; + return parsed.text?.trim() || null; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await this.hooks.onCaptureError(`distil-large-v3 음성 인식에 실패했습니다. ${message}`); + return null; + } finally { + await rm(outputPath, { force: true }).catch(() => undefined); + if (audioPath !== originalAudioPath) { + await rm(audioPath, { force: true }).catch(() => undefined); + } + await rm(originalAudioPath, { force: true }).catch(() => undefined); + } + } +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-core.ts b/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-core.ts new file mode 100644 index 0000000..e0e5d69 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-core.ts @@ -0,0 +1,1753 @@ +import { execFile, spawn } from 'node:child_process'; +import { appendFile, copyFile, mkdtemp, mkdir, readFile, rm } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +import { createCaptureSnapshot } from '../shared/capture-backends.js'; +import { createDebugLogger, type DebugLogger } from '../shared/debug-log.js'; +import { + applyLiveCaptureReadySnapshot, + LiveAudioSttProvider, + StreamingWhisperSttProvider, + verifyLiveCaptureToolchain +} from './live-capture.js'; +import type { + CaptureSnapshot, + ModeDescriptor, + ProviderSummary, + ReplySuggestion, + RuntimeMode, + TranscriptLine, + TranslationProviderId, + TranslationProviderStatus, + TranslationRouteStatus, + WorkerSnapshot +} from '../shared/runtime-contract.js'; + +interface RuntimeStartCommand { + type: 'start'; + mode?: RuntimeMode; + provider?: TranslationProviderId; + logDirectory?: string; + debugLogDirectory?: string; +} + +interface RuntimeStopCommand { + type: 'stop'; +} + +interface RuntimeRendererAudioChunkCommand { + type: 'renderer-audio-chunk'; + data: number[]; + mimeType: string; +} + +export type RuntimeCommand = RuntimeStartCommand | RuntimeStopCommand | RuntimeRendererAudioChunkCommand; + +interface TranscriptChunk { + id: string; + text: string; + finalized: boolean; +} + +interface TranslationChunk { + text: string; + finalized: boolean; +} + +interface SttProvider { + readonly key: string; + start(onChunk: (chunk: TranscriptChunk) => Promise): Promise; + stop(): Promise; + ingestExternalChunk?(data: Uint8Array, mimeType: string): Promise; +} + +interface TranslationProvider { + readonly id: TranslationProviderId; + readonly key: string; + readonly label: string; + translate(chunk: TranscriptChunk): Promise; +} + +interface RuntimeModeConfig { + providerSummary: ProviderSummary; + modeDescriptor: ModeDescriptor; + translationDelayMs: number; + translatorKind: 'local' | 'remote'; +} + +interface RuntimeEvents { + onSnapshot: (snapshot: WorkerSnapshot) => void; +} + +interface RuntimeLogEntry { + type: + | 'session.started' + | 'session.stopped' + | 'capture.status' + | 'transcript.chunk' + | 'translation.chunk' + | 'runtime.retry'; + timestamp: string; + payload: Record; +} + +interface ChatGptApiCredentials { + apiKey: string; + source: 'env.CHATGPT_API_KEY' | 'env.OPENAI_API_KEY'; +} + +interface GeminiApiCredentials { + apiKey: string; + source: 'env.GEMINI_API_KEY'; +} + +interface DeepLCredentials { + apiKey: string; + source: 'env.DEEPL_API_KEY'; +} + +interface CodexCliCredentials { + source: 'codex.chatgpt-auth'; + command: string; + codexHome: string; +} + +interface ResponsesApiOutputItem { + type?: string; + content?: Array<{ + type?: string; + text?: string; + }>; +} + +interface ResponsesApiResponse { + output_text?: string; + output?: ResponsesApiOutputItem[]; + error?: { + message?: string; + }; +} + +interface GeminiApiResponse { + candidates?: Array<{ + content?: { + parts?: Array<{ + text?: string; + }>; + }; + finishReason?: string; + }>; + error?: { + message?: string; + }; +} + +interface DeepLApiResponse { + translations?: Array<{ + text?: string; + }>; + message?: string; +} + +interface ResolvedTranslationRoute { + providerSummary: string; + routeLabel: string; + healthMessage: string; + translationProvider: TranslationProvider; + translationRoute: TranslationRouteStatus; +} + +export interface TranslationProbeResult { + provider: TranslationProviderId; + providerLabel: string; + providerKey: string | null; + success: boolean; + translatedText: string | null; + finalized: boolean; + error: string | null; +} + +const TRANSCRIPT_FIXTURES = [ + '音声ループバックの準備を始めます。', + '軽量な字幕向けの翻訳を優先します。', + '設定画面から翻訳 제공자를 고를 수 있습니다。', + '通信이 불안정하면 DeepL fallback을 다시 시도합니다。', + '実時間に近い表示を安定して続けます。' +]; + +const TRANSLATION_FIXTURES = new Map([ + ['音声ループバックの準備を始めます。', '오디오 루프백 준비를 시작합니다.'], + ['軽量な字幕向けの翻訳を優先します。', '가벼운 자막용 번역을 우선합니다.'], + ['設定画面から翻訳 제공자를 고를 수 있습니다。', '설정 화면에서 번역 제공자를 고를 수 있습니다.'], + ['通信이 불안정하면 DeepL fallback을 다시 시도합니다。', '통신이 불안정하면 DeepL fallback을 다시 시도합니다.'], + ['実時間に近い表示を安定して続けます。', '실시간에 가까운 표시를 안정적으로 이어갑니다.'] +]); + +const PROVIDER_LABELS: Record = { + auto: '자동 선택', + 'chatgpt-api': 'GPT API', + 'gemini-api': 'Gemini', + 'deepl-fallback': 'DeepL', + 'codex-auth': 'Codex auth', + 'local-fixture': '기본 확인' +}; + +const MODE_CONFIG: Record = { + local: { + providerSummary: { + stt: 'live-capture.ffmpeg+whisper', + translation: 'local.fixture-ja-ko' + }, + modeDescriptor: { + label: '기본 확인', + description: '실오디오 입력을 로컬 캡처와 로컬 전사로 받아 빠르게 확인하는 모드입니다.', + localFirst: true, + cloudEnabled: false, + requiresCredentials: false + }, + translationDelayMs: 160, + translatorKind: 'local' + }, + hybrid: { + providerSummary: { + stt: 'live-capture.ffmpeg+whisper', + translation: 'remote.selected-provider-ja-ko' + }, + modeDescriptor: { + label: '권장 모드', + description: '실오디오 입력을 받아 로컬 전사 후, 번역은 설정에서 고른 provider로 진행합니다.', + localFirst: true, + cloudEnabled: true, + requiresCredentials: true + }, + translationDelayMs: 110, + translatorKind: 'remote' + }, + cloud: { + providerSummary: { + stt: 'live-capture.ffmpeg+whisper', + translation: 'remote.selected-provider-ja-ko' + }, + modeDescriptor: { + label: '원격 번역 모드', + description: '실오디오 입력을 받아 로컬 전사 후, 선택한 provider와 DeepL 대체 경로를 검증하는 원격 번역 모드입니다.', + localFirst: false, + cloudEnabled: true, + requiresCredentials: true + }, + translationDelayMs: 70, + translatorKind: 'remote' + } +}; + +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function normalizeBaseUrl(rawValue: string | undefined, fallback: string) { + if (!rawValue?.trim()) { + return fallback; + } + return rawValue.replace(/\/+$/, ''); +} + +function getChatGptModel() { + return process.env.CHATGPT_TRANSLATION_MODEL?.trim() + || process.env.OPENAI_TRANSLATION_MODEL?.trim() + || process.env.OPENAI_MODEL?.trim() + || 'gpt-4.1-mini'; +} + +function getGeminiModel() { + return process.env.GEMINI_TRANSLATION_MODEL?.trim() || process.env.GEMINI_MODEL?.trim() || 'gemini-2.5-flash'; +} + +function getDeepLModel() { + return 'deepl-api'; +} + +function getCodexCliModel() { + return process.env.CODEX_TRANSLATION_MODEL?.trim() || process.env.CODEX_MODEL?.trim() || null; +} + +function getTranslatorContextPrompt() { + const context = process.env.TRANSLATOR_USER_CONTEXT?.trim(); + const glossary = process.env.TRANSLATOR_GLOSSARY?.trim(); + const parts = []; + if (context) { + parts.push(`User context: ${context}`); + } + if (glossary) { + parts.push(`Glossary and preferred terms: ${glossary}`); + } + return parts.length > 0 + ? `Use this private context to disambiguate technical terms, devices, roles, and domain vocabulary.\n${parts.join('\n')}` + : ''; +} + +function buildReplySuggestions(_sourceText: string, translatedText: string): ReplySuggestion[] { + const topicHint = translatedText.trim() ? `방금 말한 내용(${translatedText.trim()}) 기준으로` : '방금 말한 내용 기준으로'; + return [ + { + id: 'acknowledge', + korean: `${topicHint} 이해했습니다. 바로 확인해보겠습니다.`, + japanese: '今の内容は理解しました。すぐ確認します。', + reading: '이마노 나이요오와 리카이 시마시타. 스구 카쿠닌 시마스.' + }, + { + id: 'clarify', + korean: '한 가지 더 확인하고 싶은데, 구체적인 조건을 다시 말해주실 수 있을까요?', + japanese: '一つ確認したいのですが、具体的な条件をもう一度教えていただけますか。', + reading: '히토츠 카쿠닌 시타이노데스가, 구타이테키나 조오켄오 모오 이치도 오시에테 이타다케마스카.' + }, + { + id: 'next-step', + korean: '그 방향으로 진행하겠습니다. 결과가 나오면 바로 공유드리겠습니다.', + japanese: 'その方向で進めます。結果が出たらすぐ共有します。', + reading: '소노 호오코오데 스스메마스. 켓카가 데타라 스구 쿄오유우 시마스.' + } + ]; +} + +function getRemoteTimeoutMs() { + const raw = + Number.parseInt(process.env.TRANSLATION_TIMEOUT_MS ?? '', 10) + || Number.parseInt(process.env.OPENAI_TRANSLATION_TIMEOUT_MS ?? '', 10) + || Number.parseInt(process.env.GEMINI_TRANSLATION_TIMEOUT_MS ?? '', 10) + || Number.parseInt(process.env.DEEPL_TRANSLATION_TIMEOUT_MS ?? '', 10); + if (!Number.isFinite(raw) || raw <= 0) { + return 15_000; + } + return raw; +} + +const execFileAsync = promisify(execFile); + +function execFileWithStdin(command: string, args: string[], input: string, options: { + timeout: number; + env?: NodeJS.ProcessEnv; + windowsHide?: boolean; +}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + env: options.env, + windowsHide: options.windowsHide, + stdio: ['pipe', 'pipe', 'pipe'] + }); + let stderr = ''; + let stdout = ''; + const timer = setTimeout(() => { + child.kill('SIGTERM'); + reject(new Error(`Command timed out after ${options.timeout}ms.`)); + }, options.timeout); + + child.stdout?.on('data', (chunk) => { + stdout += String(chunk); + }); + child.stderr?.on('data', (chunk) => { + stderr += String(chunk); + }); + child.on('error', (error) => { + clearTimeout(timer); + reject(error); + }); + child.on('exit', (code, signal) => { + clearTimeout(timer); + if (code === 0) { + resolve(); + return; + } + reject(new Error(stderr.trim() || stdout.trim() || `Command exited with ${signal ?? code}.`)); + }); + child.stdin.end(input); + }); +} + +async function resolveCodexCliPath() { + const candidates = process.platform === 'win32' + ? [ + path.join(os.homedir(), '.codex', 'bin', 'codex.exe'), + path.join(os.homedir(), 'AppData', 'Local', 'OpenAI', 'Codex', 'bin', 'codex.exe') + ] + : []; + + const command = process.platform === 'win32' ? 'where' : 'which'; + const result = await execFileAsync(command, ['codex'], { + timeout: 5_000, + windowsHide: true + }).catch(() => null); + + candidates.push(...(result?.stdout + ?.split(/\r?\n/g) + .map((line) => line.trim()) + .filter(Boolean) ?? [])); + + const uniqueCandidates = [...new Set(candidates)]; + const runnableCandidates = uniqueCandidates + .filter((candidate) => process.platform !== 'win32' || path.extname(candidate).toLowerCase() === '.exe') + .sort((left, right) => { + const leftWindowsApps = process.platform === 'win32' && left.includes(`${path.sep}WindowsApps${path.sep}`); + const rightWindowsApps = process.platform === 'win32' && right.includes(`${path.sep}WindowsApps${path.sep}`); + return Number(leftWindowsApps) - Number(rightWindowsApps); + }); + + const tryRunCodexVersion = async (candidate: string) => { + try { + await execFileAsync(candidate, ['--version'], { + timeout: 5_000, + windowsHide: true, + env: { + ...process.env, + NO_COLOR: '1' + } + }); + return candidate; + } catch { + return null; + } + }; + + for (const candidate of runnableCandidates) { + const runnable = await tryRunCodexVersion(candidate); + if (runnable) { + return runnable; + } + + if (process.platform === 'win32' && candidate.includes(`${path.sep}WindowsApps${path.sep}`)) { + const copiedPath = path.join(os.homedir(), '.codex', 'bin', 'codex.exe'); + try { + await mkdir(path.dirname(copiedPath), { recursive: true }); + await copyFile(candidate, copiedPath); + const copiedRunnable = await tryRunCodexVersion(copiedPath); + if (copiedRunnable) { + return copiedRunnable; + } + } catch { + continue; + } + } + } + + return null; +} + +async function loadCodexCliCredentials(): Promise { + const authPath = process.env.CODEX_AUTH_PATH?.trim() || path.join(os.homedir(), '.codex', 'auth.json'); + try { + const raw = await readFile(authPath, 'utf8'); + const parsed = JSON.parse(raw) as { auth_mode?: string; tokens?: { access_token?: string } }; + if (parsed.auth_mode !== 'chatgpt' || !parsed.tokens?.access_token?.trim()) { + return null; + } + const codexPath = await resolveCodexCliPath(); + if (!codexPath) { + return null; + } + return { + source: 'codex.chatgpt-auth', + command: codexPath, + codexHome: path.dirname(authPath) + }; + } catch { + return null; + } +} + +function loadChatGptApiCredentials(): ChatGptApiCredentials | null { + const direct = process.env.CHATGPT_API_KEY?.trim(); + if (direct) { + return { + apiKey: direct, + source: 'env.CHATGPT_API_KEY' + }; + } + + const openai = process.env.OPENAI_API_KEY?.trim(); + if (openai) { + return { + apiKey: openai, + source: 'env.OPENAI_API_KEY' + }; + } + + return null; +} + +function loadGeminiApiCredentials(): GeminiApiCredentials | null { + const apiKey = process.env.GEMINI_API_KEY?.trim(); + return apiKey ? { apiKey, source: 'env.GEMINI_API_KEY' } : null; +} + +function loadDeepLCredentials(): DeepLCredentials | null { + const apiKey = process.env.DEEPL_API_KEY?.trim(); + return apiKey ? { apiKey, source: 'env.DEEPL_API_KEY' } : null; +} + +async function detectProviderStatuses(selected: TranslationProviderId): Promise { + const chatGpt = loadChatGptApiCredentials(); + const gemini = loadGeminiApiCredentials(); + const deepl = loadDeepLCredentials(); + const codex = await loadCodexCliCredentials(); + const autoReadyProvider = chatGpt ? 'chatgpt-api' : gemini ? 'gemini-api' : deepl ? 'deepl-fallback' : codex ? 'codex-auth' : null; + + const statuses: TranslationProviderStatus[] = [ + { + id: 'auto', + label: PROVIDER_LABELS.auto, + configured: Boolean(autoReadyProvider), + available: Boolean(autoReadyProvider), + state: selected === 'auto' ? (autoReadyProvider ? 'active' : 'missing-config') : (autoReadyProvider ? 'ready' : 'missing-config'), + detail: autoReadyProvider + ? `${PROVIDER_LABELS[autoReadyProvider]}가 자동 기본값으로 선택됩니다.` + : 'GPT API, Gemini, DeepL 중 하나를 저장하면 자동 선택이 바로 활성화됩니다.' + }, + { + id: 'chatgpt-api', + label: PROVIDER_LABELS['chatgpt-api'], + configured: Boolean(chatGpt), + available: Boolean(chatGpt), + state: selected === 'chatgpt-api' + ? (chatGpt ? 'active' : 'missing-config') + : (chatGpt ? 'ready' : 'missing-config'), + detail: chatGpt + ? `${chatGpt.source} 기준으로 설정이 확인되었습니다.` + : '앱 설정에서 GPT API 키를 입력하세요.' + }, + { + id: 'gemini-api', + label: PROVIDER_LABELS['gemini-api'], + configured: Boolean(gemini), + available: Boolean(gemini), + state: selected === 'gemini-api' + ? (gemini ? 'active' : 'missing-config') + : (gemini ? 'ready' : 'missing-config'), + detail: gemini + ? 'Gemini API 키가 확인되었습니다.' + : '앱 설정에서 Gemini API 키를 입력하세요.' + }, + { + id: 'deepl-fallback', + label: PROVIDER_LABELS['deepl-fallback'], + configured: Boolean(deepl), + available: Boolean(deepl), + state: selected === 'deepl-fallback' + ? (deepl ? 'active' : 'missing-config') + : (deepl ? 'ready' : 'missing-config'), + detail: deepl + ? 'DeepL 키가 확인되어 선택 경로 또는 대체 경로로 사용할 수 있습니다.' + : '앱 설정에서 DeepL API 키를 입력하세요.' + }, + { + id: 'codex-auth', + label: PROVIDER_LABELS['codex-auth'], + configured: Boolean(codex), + available: Boolean(codex), + state: selected === 'codex-auth' + ? (codex ? 'active' : 'manual-auth') + : (codex ? 'ready' : 'manual-auth'), + detail: codex + ? '고급 옵션이 준비되었습니다. Codex CLI 로그인 상태가 확인되어 별도 API 키 없이 사용할 수 있습니다.' + : '고급 옵션입니다. 이 경로를 쓰려면 Codex CLI 설치와 ChatGPT 로그인이 둘 다 필요합니다.' + }, + { + id: 'local-fixture', + label: PROVIDER_LABELS['local-fixture'], + configured: true, + available: true, + state: selected === 'local-fixture' ? 'active' : 'ready', + detail: '실오디오/STT 흐름을 점검하는 내장 확인 경로입니다. 실사용 번역은 GPT API, Gemini, DeepL, Codex auth 중 하나를 설정해야 합니다.' + } + ]; + + return statuses; +} + +function setRouteState( + statuses: TranslationProviderStatus[], + providerId: TranslationProviderId, + state: TranslationProviderStatus['state'], + detail?: string +) { + return statuses.map((status) => ( + status.id === providerId + ? { + ...status, + state, + detail: detail ?? status.detail + } + : status + )); +} + +function extractResponseText(payload: ResponsesApiResponse) { + if (typeof payload.output_text === 'string' && payload.output_text.trim()) { + return payload.output_text.trim(); + } + + const contentTexts = payload.output + ?.flatMap((item) => item.content ?? []) + .map((item) => item.text?.trim()) + .filter((value): value is string => Boolean(value)); + + if (contentTexts?.length) { + return contentTexts.join('\n').trim(); + } + + return null; +} + +function extractGeminiResponseText(payload: GeminiApiResponse) { + const text = payload.candidates + ?.flatMap((candidate) => candidate.content?.parts ?? []) + .map((part) => part.text?.trim()) + .filter((value): value is string => Boolean(value)) + .join('\n') + .trim(); + + return text || null; +} + +class FixtureSttProvider implements SttProvider { + readonly key: string; + private timer: NodeJS.Timeout | null = null; + private cancelled = false; + private fixtureIndex = 0; + + constructor(key: string) { + this.key = key; + } + + async start(onChunk: (chunk: TranscriptChunk) => Promise) { + this.cancelled = false; + this.fixtureIndex = 0; + + const emitNext = async () => { + if (this.cancelled) { + return; + } + + const text = TRANSCRIPT_FIXTURES[this.fixtureIndex % TRANSCRIPT_FIXTURES.length]; + const chunk: TranscriptChunk = { + id: `chunk-${Date.now()}-${this.fixtureIndex}`, + text, + finalized: true + }; + + this.fixtureIndex += 1; + await onChunk(chunk); + this.timer = setTimeout(() => { + void emitNext(); + }, 900); + }; + + this.timer = setTimeout(() => { + void emitNext(); + }, 260); + } + + async stop() { + this.cancelled = true; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } +} + +class FixtureTranslationProvider implements TranslationProvider { + readonly id: TranslationProviderId = 'local-fixture'; + readonly key: string; + readonly label = PROVIDER_LABELS['local-fixture']; + private readonly delayMs: number; + + constructor(key: string, delayMs: number) { + this.key = key; + this.delayMs = delayMs; + } + + async translate(chunk: TranscriptChunk) { + await wait(this.delayMs); + return { + text: TRANSLATION_FIXTURES.get(chunk.text) ?? `확인용 출력: ${chunk.text}`, + finalized: chunk.finalized + }; + } +} + +class ChatGptApiTranslationProvider implements TranslationProvider { + readonly id: TranslationProviderId = 'chatgpt-api'; + readonly key: string; + readonly label = PROVIDER_LABELS['chatgpt-api']; + readonly credentialSource: ChatGptApiCredentials['source']; + private readonly apiKey: string; + private readonly model: string; + private readonly baseUrl: string; + private readonly timeoutMs: number; + + constructor(credentials: ChatGptApiCredentials) { + this.key = `chatgpt-api.${credentials.source}`; + this.apiKey = credentials.apiKey; + this.credentialSource = credentials.source; + this.model = getChatGptModel(); + this.baseUrl = normalizeBaseUrl(process.env.OPENAI_BASE_URL, 'https://api.openai.com/v1'); + this.timeoutMs = getRemoteTimeoutMs(); + } + + async translate(chunk: TranscriptChunk) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + const contextPrompt = getTranslatorContextPrompt(); + + try { + const response = await fetch(`${this.baseUrl}/responses`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${this.apiKey}` + }, + signal: controller.signal, + body: JSON.stringify({ + model: this.model, + input: [ + { + role: 'system', + content: + [ + 'Translate Japanese speech transcripts into natural Korean subtitle lines. Keep it concise. Output Korean only.', + contextPrompt + ].filter(Boolean).join('\n\n') + }, + { + role: 'user', + content: chunk.text + } + ], + text: { + format: { + type: 'text' + } + } + }) + }); + + const payload = (await response.json().catch(() => ({}))) as ResponsesApiResponse; + if (!response.ok) { + if (response.status === 401) { + throw new Error(`ChatGPT API authentication failed (HTTP 401). Credential source: ${this.credentialSource}.`); + } + if (response.status === 429) { + throw new Error('ChatGPT API rate-limited the request (HTTP 429).'); + } + throw new Error(payload.error?.message?.trim() || `ChatGPT API request failed with HTTP ${response.status}.`); + } + + const translatedText = extractResponseText(payload); + if (!translatedText) { + throw new Error('ChatGPT API returned no usable text.'); + } + + return { + text: translatedText, + finalized: chunk.finalized + }; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`ChatGPT API timed out after ${this.timeoutMs}ms.`); + } + if (error instanceof TypeError) { + throw new Error('ChatGPT API network request failed. Check internet access or OPENAI_BASE_URL.'); + } + throw error; + } finally { + clearTimeout(timeout); + } + } +} + +class GeminiApiTranslationProvider implements TranslationProvider { + readonly id: TranslationProviderId = 'gemini-api'; + readonly key: string; + readonly label = PROVIDER_LABELS['gemini-api']; + private readonly apiKey: string; + private readonly model: string; + private readonly baseUrl: string; + private readonly timeoutMs: number; + + constructor(credentials: GeminiApiCredentials) { + this.key = `gemini-api.${credentials.source}`; + this.apiKey = credentials.apiKey; + this.model = getGeminiModel(); + this.baseUrl = normalizeBaseUrl( + process.env.GEMINI_BASE_URL, + 'https://generativelanguage.googleapis.com/v1beta' + ); + this.timeoutMs = getRemoteTimeoutMs(); + } + + async translate(chunk: TranscriptChunk) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + const contextPrompt = getTranslatorContextPrompt(); + + try { + const response = await fetch( + `${this.baseUrl}/models/${encodeURIComponent(this.model)}:generateContent?key=${encodeURIComponent(this.apiKey)}`, + { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + signal: controller.signal, + body: JSON.stringify({ + systemInstruction: { + parts: [ + { + text: [ + 'Translate Japanese speech transcripts into natural Korean subtitle lines. Keep it concise. Output Korean only.', + contextPrompt + ].filter(Boolean).join('\n\n') + } + ] + }, + contents: [ + { + parts: [ + { + text: chunk.text + } + ] + } + ] + }) + } + ); + + const payload = (await response.json().catch(() => ({}))) as GeminiApiResponse; + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + throw new Error('Gemini API authentication failed.'); + } + if (response.status === 429) { + throw new Error('Gemini API rate-limited the request (HTTP 429).'); + } + throw new Error(payload.error?.message?.trim() || `Gemini API request failed with HTTP ${response.status}.`); + } + + const translatedText = extractGeminiResponseText(payload); + if (!translatedText) { + throw new Error('Gemini API returned no usable text.'); + } + + return { + text: translatedText, + finalized: chunk.finalized + }; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Gemini API timed out after ${this.timeoutMs}ms.`); + } + if (error instanceof TypeError) { + throw new Error('Gemini API network request failed. Check internet access or GEMINI_BASE_URL.'); + } + throw error; + } finally { + clearTimeout(timeout); + } + } +} + +class DeepLTranslationProvider implements TranslationProvider { + readonly id: TranslationProviderId = 'deepl-fallback'; + readonly key: string; + readonly label = PROVIDER_LABELS['deepl-fallback']; + private readonly apiKey: string; + private readonly baseUrl: string; + private readonly timeoutMs: number; + + constructor(credentials: DeepLCredentials) { + this.key = `deepl-fallback.${credentials.source}`; + this.apiKey = credentials.apiKey; + this.baseUrl = normalizeBaseUrl(process.env.DEEPL_BASE_URL, 'https://api-free.deepl.com'); + this.timeoutMs = getRemoteTimeoutMs(); + } + + async translate(chunk: TranscriptChunk) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const response = await fetch(`${this.baseUrl}/v2/translate`, { + method: 'POST', + headers: { + authorization: `DeepL-Auth-Key ${this.apiKey}`, + 'content-type': 'application/x-www-form-urlencoded' + }, + signal: controller.signal, + body: new URLSearchParams({ + text: chunk.text, + source_lang: 'JA', + target_lang: 'KO' + }).toString() + }); + + const payload = (await response.json().catch(() => ({}))) as DeepLApiResponse; + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + throw new Error('DeepL authentication failed.'); + } + if (response.status === 429) { + throw new Error('DeepL rate-limited the request (HTTP 429).'); + } + throw new Error(payload.message?.trim() || `DeepL request failed with HTTP ${response.status}.`); + } + + const translatedText = payload.translations?.[0]?.text?.trim(); + if (!translatedText) { + throw new Error('DeepL returned no usable text.'); + } + + return { + text: translatedText, + finalized: chunk.finalized + }; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`DeepL timed out after ${this.timeoutMs}ms.`); + } + if (error instanceof TypeError) { + throw new Error('DeepL network request failed. Check internet access or DEEPL_BASE_URL.'); + } + throw error; + } finally { + clearTimeout(timeout); + } + } +} + +class CodexCliTranslationProvider implements TranslationProvider { + readonly id: TranslationProviderId = 'codex-auth'; + readonly key: string; + readonly label = PROVIDER_LABELS['codex-auth']; + private readonly command: string; + private readonly codexHome: string; + private readonly timeoutMs: number; + private readonly model: string | null; + + constructor(credentials: CodexCliCredentials) { + this.key = `codex-auth.${credentials.source}`; + this.command = credentials.command; + this.codexHome = credentials.codexHome; + this.timeoutMs = Math.max(getRemoteTimeoutMs(), 30_000); + this.model = getCodexCliModel(); + } + + async translate(chunk: TranscriptChunk) { + const contextPrompt = getTranslatorContextPrompt(); + const prompt = [ + 'Translate this Japanese speech transcript into natural Korean subtitle style.', + 'Keep it concise.', + 'Output Korean only.', + contextPrompt, + '', + chunk.text + ].filter(Boolean).join('\n'); + + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'jp-ko-codex-translation-')); + const outputPath = path.join(tempDir, 'last-message.txt'); + + try { + const args = [ + 'exec', + '--skip-git-repo-check', + '--dangerously-bypass-approvals-and-sandbox', + '--output-last-message', + outputPath, + '-' + ]; + if (this.model) { + args.push('--model', this.model); + } + + await execFileWithStdin(this.command, args, prompt, { + timeout: this.timeoutMs, + windowsHide: true, + env: { + ...process.env, + CODEX_HOME: this.codexHome, + NO_COLOR: '1' + } + }); + + const translatedText = (await readFile(outputPath, 'utf8')).trim(); + if (!translatedText) { + throw new Error('Codex auth translation returned no usable text.'); + } + + return { + text: translatedText, + finalized: chunk.finalized + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes('timed out')) { + throw new Error(`Codex auth translation timed out after ${this.timeoutMs}ms.`); + } + throw new Error(`Codex auth translation failed. ${message}`); + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => undefined); + } + } +} + +class FallbackTranslationProvider implements TranslationProvider { + readonly key: string; + readonly label: string; + readonly id: TranslationProviderId; + private readonly primary: TranslationProvider; + private readonly fallback: TranslationProvider | null; + private readonly onFallback: (detail: string) => void; + + constructor(primary: TranslationProvider, fallback: TranslationProvider | null, onFallback: (detail: string) => void) { + this.primary = primary; + this.fallback = fallback; + this.onFallback = onFallback; + this.id = primary.id; + this.label = primary.label; + this.key = fallback ? `${primary.key}->${fallback.key}` : primary.key; + } + + async translate(chunk: TranscriptChunk) { + try { + return await this.primary.translate(chunk); + } catch (error) { + if (!this.fallback) { + throw error; + } + const message = error instanceof Error ? error.message : String(error); + this.onFallback(message); + return this.fallback.translate(chunk); + } + } +} + +async function resolveAutoProviderId(): Promise> { + if (loadChatGptApiCredentials()) return 'chatgpt-api'; + if (loadGeminiApiCredentials()) return 'gemini-api'; + if (loadDeepLCredentials()) return 'deepl-fallback'; + if (await loadCodexCliCredentials()) return 'codex-auth'; + throw new Error('자동 선택에 사용할 provider가 없습니다. GPT API, Gemini, DeepL 중 하나를 먼저 저장하세요.'); +} + +async function createSelectedProvider(provider: TranslationProviderId): Promise { + switch (provider) { + case 'auto': + return createSelectedProvider(await resolveAutoProviderId()); + case 'local-fixture': + return new FixtureTranslationProvider('local.fixture-ja-ko', MODE_CONFIG.local.translationDelayMs); + case 'codex-auth': { + const credentials = await loadCodexCliCredentials(); + if (!credentials) { + throw new Error('Codex auth is not ready. Sign in through Codex CLI first.'); + } + return new CodexCliTranslationProvider(credentials); + } + case 'chatgpt-api': { + const credentials = loadChatGptApiCredentials(); + if (!credentials) { + throw new Error('ChatGPT API가 설정되지 않았습니다. CHATGPT_API_KEY 또는 OPENAI_API_KEY를 설정하세요.'); + } + return new ChatGptApiTranslationProvider(credentials); + } + case 'gemini-api': { + const credentials = loadGeminiApiCredentials(); + if (!credentials) { + throw new Error('Gemini API가 설정되지 않았습니다. GEMINI_API_KEY를 설정하세요.'); + } + return new GeminiApiTranslationProvider(credentials); + } + case 'deepl-fallback': { + const credentials = loadDeepLCredentials(); + if (!credentials) { + throw new Error('DeepL이 설정되지 않았습니다. DEEPL_API_KEY를 설정하세요.'); + } + return new DeepLTranslationProvider(credentials); + } + } +} + +export async function runTranslationProbe( + provider: TranslationProviderId, + text: string +): Promise { + try { + const translationProvider = await createSelectedProvider(provider); + const translated = await translationProvider.translate({ + id: `probe-${Date.now()}`, + text, + finalized: true + }); + return { + provider, + providerLabel: PROVIDER_LABELS[provider], + providerKey: translationProvider.key, + success: true, + translatedText: translated.text, + finalized: translated.finalized, + error: null + }; + } catch (error) { + return { + provider, + providerLabel: PROVIDER_LABELS[provider], + providerKey: null, + success: false, + translatedText: null, + finalized: false, + error: error instanceof Error ? error.message : String(error) + }; + } +} + +async function createProviders( + mode: RuntimeMode, + selectedProvider: TranslationProviderId, + capture: CaptureSnapshot, + onCaptureEvent: { + ready: (detail: { deviceLabel: string; backendLabel: string; command: string }) => Promise; + error: (message: string) => Promise; + exit: (message: string) => Promise; + }, + onFallback: (detail: string) => void +): Promise<{ sttProvider: SttProvider; route: ResolvedTranslationRoute }> { + const config = MODE_CONFIG[mode]; + const sttProvider = await createSttProvider(capture, onCaptureEvent); + const providerStatuses = await detectProviderStatuses(selectedProvider); + + if (config.translatorKind === 'local' || selectedProvider === 'local-fixture') { + const translationProvider = new FixtureTranslationProvider(config.providerSummary.translation, config.translationDelayMs); + return { + sttProvider, + route: { + providerSummary: translationProvider.key, + routeLabel: '실오디오 입력 + 기본 확인', + healthMessage: '실오디오 입력과 로컬 전사가 활성화되어 있으며, 번역은 기본 내장 경로로 처리합니다.', + translationProvider, + translationRoute: { + selected: 'local-fixture', + active: 'local-fixture', + fallbackUsed: false, + selectedLabel: PROVIDER_LABELS['local-fixture'], + activeLabel: PROVIDER_LABELS['local-fixture'], + detail: '기본 내장 번역이 선택된 상태입니다.', + providerStatuses: setRouteState(providerStatuses, 'local-fixture', 'active') + } + } + }; + } + + const resolvedSelectedProvider = selectedProvider === 'auto' ? await resolveAutoProviderId() : selectedProvider; + let selected = await createSelectedProvider(resolvedSelectedProvider); + let activeProvider = selected; + let fallbackUsed = false; + let routeDetail = selectedProvider === 'auto' + ? `${PROVIDER_LABELS[resolvedSelectedProvider]}가 자동 기본값으로 선택되었습니다.` + : `${PROVIDER_LABELS[selectedProvider]} 경로가 선택되었습니다.`; + let providerStatusesNext = setRouteState(providerStatuses, selectedProvider, 'active'); + if (selectedProvider === 'auto') { + providerStatusesNext = setRouteState(providerStatusesNext, resolvedSelectedProvider, 'active', `${PROVIDER_LABELS[resolvedSelectedProvider]}가 자동 기본값으로 선택되었습니다.`); + } else { + providerStatusesNext = setRouteState(providerStatusesNext, resolvedSelectedProvider, 'active'); + } + + const fallbackCandidate = + resolvedSelectedProvider === 'deepl-fallback' + ? null + : await createSelectedProvider('deepl-fallback').catch(() => null); + + if (fallbackCandidate) { + selected = new FallbackTranslationProvider(selected, fallbackCandidate, (detail) => { + fallbackUsed = true; + activeProvider = fallbackCandidate; + routeDetail = `${PROVIDER_LABELS[resolvedSelectedProvider]}가 실패하여 DeepL 대체 경로로 전환되었습니다.`; + providerStatusesNext = setRouteState(providerStatusesNext, selectedProvider, 'ready', detail); + providerStatusesNext = setRouteState(providerStatusesNext, resolvedSelectedProvider, 'ready', detail); + providerStatusesNext = setRouteState(providerStatusesNext, 'deepl-fallback', 'fallback-active', '현재 DeepL 대체 경로가 번역을 처리하고 있습니다.'); + onFallback(detail); + }); + } + + return { + sttProvider, + route: { + providerSummary: selected.key, + routeLabel: `실오디오 입력 + ${PROVIDER_LABELS[resolvedSelectedProvider]} 번역`, + healthMessage: + fallbackCandidate + ? `실오디오 입력과 ${PROVIDER_LABELS[resolvedSelectedProvider]} 번역 경로가 활성화되어 있으며, 실패 시 DeepL 대체 경로가 자동으로 이어받습니다.` + : `실오디오 입력과 ${PROVIDER_LABELS[resolvedSelectedProvider]} 번역 경로가 활성화되어 있으며, 현재 대체 경로는 설정되어 있지 않습니다.`, + translationProvider: selected, + translationRoute: { + selected: selectedProvider, + active: activeProvider.id, + fallbackUsed, + selectedLabel: PROVIDER_LABELS[selectedProvider], + activeLabel: activeProvider.label, + detail: routeDetail, + providerStatuses: providerStatusesNext + } + } + }; +} + +async function createSttProvider( + capture: CaptureSnapshot, + onCaptureEvent: { + ready: (detail: { deviceLabel: string; backendLabel: string; command: string }) => Promise; + error: (message: string) => Promise; + exit: (message: string) => Promise; + } +) { + if (process.env.TRANSLATOR_FORCE_FIXTURE === '1') { + return new FixtureSttProvider('fixture.transcript-feed'); + } + + if (process.env.TRANSLATOR_STT_BACKEND === 'whisper-stream') { + return new StreamingWhisperSttProvider(capture.platform, { + onCaptureReady: onCaptureEvent.ready, + onCaptureError: onCaptureEvent.error, + onCaptureExit: onCaptureEvent.exit + }); + } + + if (process.env.TRANSLATOR_CAPTURE_BACKEND === 'renderer-audio') { + await verifyLiveCaptureToolchain(capture.platform); + return new LiveAudioSttProvider(capture.platform, { + onCaptureReady: onCaptureEvent.ready, + onCaptureError: onCaptureEvent.error, + onCaptureExit: onCaptureEvent.exit + }, { externalInput: true }); + } + + if (capture.platform !== 'win32' && capture.platform !== 'darwin') { + return new FixtureSttProvider('fixture.transcript-feed'); + } + + await verifyLiveCaptureToolchain(capture.platform); + return new LiveAudioSttProvider(capture.platform, { + onCaptureReady: onCaptureEvent.ready, + onCaptureError: onCaptureEvent.error, + onCaptureExit: onCaptureEvent.exit + }); +} + +export class TranslatorRuntime { + private snapshot: WorkerSnapshot; + private readonly events: RuntimeEvents; + private sttProvider: SttProvider | null = null; + private translationProvider: TranslationProvider | null = null; + private logPath: string | null = null; + private activeRunId = 0; + private debugLogger: DebugLogger | null = null; + + constructor(events: RuntimeEvents) { + this.events = events; + this.snapshot = this.createSnapshot('local', 'local-fixture'); + } + + getSnapshot() { + return this.snapshot; + } + + async handleCommand(command: RuntimeCommand) { + if (command.type === 'start') { + await this.start( + command.mode ?? 'local', + command.provider ?? (command.mode === 'local' ? 'local-fixture' : 'chatgpt-api'), + command.logDirectory ?? path.join(process.cwd(), 'logs'), + command.debugLogDirectory ?? path.join(process.cwd(), 'logs') + ); + return this.snapshot; + } + + if (command.type === 'renderer-audio-chunk') { + await this.ingestRendererAudioChunk(command.data, command.mimeType); + return this.snapshot; + } + + await this.debug('worker.command.stop', { + phase: this.snapshot.phase, + mode: this.snapshot.mode, + retryCount: this.snapshot.retryCount + }); + await this.stop(); + return this.snapshot; + } + + private async ingestRendererAudioChunk(data: number[], mimeType: string) { + if (!this.sttProvider?.ingestExternalChunk) { + await this.debug('worker.renderer-audio.skip', { + reason: 'provider-does-not-accept-external-audio', + phase: this.snapshot.phase + }); + return; + } + await this.sttProvider.ingestExternalChunk(Uint8Array.from(data), mimeType); + } + + async reportHostIssue(event: string, error?: unknown) { + await this.debug(event, { + phase: this.snapshot.phase, + mode: this.snapshot.mode, + retryCount: this.snapshot.retryCount, + error: error instanceof Error ? error : error ? { message: String(error) } : null + }); + } + + private createSnapshot(mode: RuntimeMode, selectedProvider: TranslationProviderId): WorkerSnapshot { + const config = MODE_CONFIG[mode]; + const capture = createCaptureSnapshot(process.platform); + const liveCaptureCapable = capture.platform === 'win32' || capture.platform === 'darwin'; + return { + phase: 'idle', + mode, + sourceLanguage: 'ja', + targetLanguage: 'ko', + transcriptSource: liveCaptureCapable ? 'live-capture' : 'fixture', + transcriptSourceLabel: liveCaptureCapable ? '실시간 오디오 입력' : '데모 자막 입력', + liveCaptureImplemented: liveCaptureCapable, + providerSummary: config.providerSummary, + translationRoute: { + selected: selectedProvider, + active: selectedProvider, + fallbackUsed: false, + selectedLabel: PROVIDER_LABELS[selectedProvider], + activeLabel: PROVIDER_LABELS[selectedProvider], + detail: `${PROVIDER_LABELS[selectedProvider]} 경로가 선택된 상태입니다.`, + providerStatuses: [] + }, + modeDescriptor: config.modeDescriptor, + capture, + startedAt: null, + lastError: null, + retryCount: 0, + healthMessage: '실행 대기 중입니다.', + logPath: null, + debugLogPath: null, + logDirectory: null, + logPathDetail: null, + routeLabel: + config.translatorKind === 'remote' + ? `실오디오 입력 + ${PROVIDER_LABELS[selectedProvider]} 번역` + : '실오디오 입력 + 기본 확인', + lines: [] + }; + } + + private emitSnapshot() { + this.events.onSnapshot(this.snapshot); + } + + private async debug(event: string, payload: Record) { + if (!this.debugLogger) { + return; + } + await this.debugLogger.log(event, payload); + } + + private updateSnapshot(next: Partial) { + this.snapshot = { + ...this.snapshot, + ...next + }; + this.emitSnapshot(); + void this.debug('worker.snapshot.update', { + phase: this.snapshot.phase, + mode: this.snapshot.mode, + retryCount: this.snapshot.retryCount, + capturePhase: this.snapshot.capture.phase, + lineCount: this.snapshot.lines.length, + lastError: this.snapshot.lastError, + routeLabel: this.snapshot.routeLabel, + activeProvider: this.snapshot.translationRoute.active + }); + } + + private upsertLine(line: TranscriptLine) { + const nextLines = [...this.snapshot.lines.filter((item) => item.id !== line.id), line].slice(-6); + this.updateSnapshot({ + lines: nextLines + }); + } + + private async writeLog(entry: RuntimeLogEntry) { + if (!this.logPath) { + return; + } + + await mkdir(path.dirname(this.logPath), { recursive: true }); + await appendFile(this.logPath, `${JSON.stringify(entry)}\n`, 'utf8'); + } + + private async start(mode: RuntimeMode, selectedProvider: TranslationProviderId, logDirectory: string, debugLogDirectory: string) { + await this.sttProvider?.stop(); + this.sttProvider = null; + this.translationProvider = null; + + this.activeRunId += 1; + const runId = this.activeRunId; + const baseSnapshot = this.createSnapshot(mode, selectedProvider); + const startedAt = new Date().toISOString(); + this.logPath = path.join(logDirectory, `session-${startedAt.replaceAll(':', '-')}.jsonl`); + + this.debugLogger = createDebugLogger({ + logDirectory: debugLogDirectory, + scope: `worker-${runId}` + }); + + const unsupportedCapture = !baseSnapshot.capture.supportsSystemAudio; + const manualCaptureSetup = baseSnapshot.capture.backendAvailability === 'manual-setup'; + + await this.debug('worker.session.starting', { + runId, + mode, + selectedProvider, + logPath: this.logPath, + debugLogPath: this.debugLogger.textPath, + unsupportedCapture, + translatorKind: MODE_CONFIG[mode].translatorKind + }); + + this.snapshot = { + ...baseSnapshot, + phase: 'starting', + startedAt, + logPath: this.logPath, + debugLogPath: this.debugLogger.textPath, + logDirectory, + logPathDetail: `세션 로그: ${this.logPath} | 디버그 로그: ${this.debugLogger.textPath}`, + healthMessage: + MODE_CONFIG[mode].translatorKind === 'remote' + ? '실오디오 캡처와 선택한 provider 준비 상태를 점검하는 중입니다.' + : '실오디오 캡처와 로컬 번역 경로를 초기화하는 중입니다.' + }; + this.emitSnapshot(); + + await this.writeLog({ + type: 'session.started', + timestamp: startedAt, + payload: { + mode, + selectedProvider, + providerSummary: this.snapshot.providerSummary, + routeLabel: this.snapshot.routeLabel + } + }); + + let routeDetailMessage = ''; + try { + const providers = await createProviders(mode, selectedProvider, this.snapshot.capture, { + ready: async (detail) => { + this.updateSnapshot({ + transcriptSource: 'live-capture', + transcriptSourceLabel: `실시간 오디오 입력 (${detail.deviceLabel})`, + liveCaptureImplemented: true, + capture: applyLiveCaptureReadySnapshot(this.snapshot.capture, { + backend: process.platform === 'win32' ? 'windows-wasapi-loopback' : 'macos-screencapturekit-native', + backendLabel: detail.backendLabel, + deviceLabel: detail.deviceLabel + }), + healthMessage: `${detail.backendLabel}가 실제 입력선을 열었습니다.`, + lastError: null + }); + await this.debug('worker.capture.ready', { + runId, + deviceLabel: detail.deviceLabel, + backendLabel: detail.backendLabel, + command: detail.command + }); + }, + error: async (message) => { + this.updateSnapshot({ + capture: { + ...this.snapshot.capture, + phase: 'error', + lastError: message, + guidance: message + }, + lastError: message, + healthMessage: '실오디오 캡처 또는 전사 단계에서 오류가 발생했습니다.' + }); + await this.debug('worker.capture.error', { + runId, + message + }); + }, + exit: async (message) => { + const enrichedMessage = this.snapshot.logPathDetail + ? `${message} ${this.snapshot.logPathDetail}` + : message; + this.updateSnapshot({ + phase: 'error', + capture: { + ...this.snapshot.capture, + phase: 'error', + lastError: enrichedMessage, + guidance: enrichedMessage + }, + lastError: enrichedMessage, + healthMessage: '실오디오 캡처 프로세스가 중단되었습니다.' + }); + await this.debug('worker.capture.exit', { + runId, + message: enrichedMessage + }); + } + }, (detail) => { + routeDetailMessage = detail; + this.updateSnapshot({ + translationRoute: { + ...this.snapshot.translationRoute, + active: 'deepl-fallback', + activeLabel: PROVIDER_LABELS['deepl-fallback'], + fallbackUsed: true, + detail: `${PROVIDER_LABELS[selectedProvider]}가 실패하여 DeepL 대체 경로로 전환되었습니다. ${detail}` + }, + providerSummary: { + ...this.snapshot.providerSummary, + translation: `deepl-fallback.model=${getDeepLModel()}` + }, + routeLabel: `실오디오 입력 + ${PROVIDER_LABELS['deepl-fallback']} 번역`, + healthMessage: `${PROVIDER_LABELS[selectedProvider]}가 실패하여 DeepL 대체 경로가 활성화되었습니다.` + }); + }); + this.sttProvider = providers.sttProvider; + this.translationProvider = providers.route.translationProvider; + + this.updateSnapshot({ + phase: 'running', + routeLabel: providers.route.routeLabel, + transcriptSource: this.snapshot.capture.platform === 'win32' || this.snapshot.capture.platform === 'darwin' + ? 'live-capture' + : 'fixture', + transcriptSourceLabel: this.snapshot.capture.platform === 'win32' || this.snapshot.capture.platform === 'darwin' + ? '실시간 오디오 입력' + : '데모 자막 입력', + liveCaptureImplemented: this.snapshot.capture.platform === 'win32' || this.snapshot.capture.platform === 'darwin', + providerSummary: { + ...this.snapshot.providerSummary, + stt: this.sttProvider.key, + translation: providers.route.providerSummary + }, + translationRoute: providers.route.translationRoute, + capture: { + ...this.snapshot.capture, + phase: this.snapshot.capture.supportsSystemAudio ? 'probing' : 'unsupported', + guidance: this.snapshot.capture.supportsSystemAudio + ? manualCaptureSetup + ? `${providers.route.routeLabel} 경로가 활성화되어 있습니다. ${this.snapshot.capture.backendLabel}는 실제 장치 입력을 받지만 시스템 오디오를 받으려면 수동 라우팅이 필요합니다. ${this.snapshot.capture.availableBackends[0]?.setupHint ?? ''}`.trim() + : `${providers.route.routeLabel} 경로가 활성화되어 있습니다. ${this.snapshot.capture.backendLabel} 연결을 확인하는 중입니다. 첫 오디오 청크가 저장되면 실제 캡처 상태로 전환됩니다.` + : `${providers.route.routeLabel} 경로가 활성화되어 있지만, 이 호스트에는 아직 지원되는 live system-audio 백엔드가 없습니다.` + }, + healthMessage: + providers.route.healthMessage + || (unsupportedCapture + ? '지원 범위 밖 호스트에서 데모 자막과 번역 이벤트를 provider 구조로 점검 중입니다.' + : '실오디오 캡처와 전사 경로를 점검 중입니다. 첫 입력 청크를 기다리고 있습니다.') + }); + + await this.writeLog({ + type: 'capture.status', + timestamp: new Date().toISOString(), + payload: { + phase: this.snapshot.capture.phase, + backend: this.snapshot.capture.backend, + deviceLabel: this.snapshot.capture.deviceLabel, + routeLabel: this.snapshot.routeLabel, + translationRoute: this.snapshot.translationRoute + } + }); + + await this.sttProvider.start(async (chunk) => { + if (runId !== this.activeRunId || !this.translationProvider) { + return; + } + + await this.debug('worker.transcript.received', { + runId, + chunkId: chunk.id, + finalized: chunk.finalized, + sourceLength: chunk.text.length + }); + + const createdAt = new Date().toISOString(); + const line: TranscriptLine = { + id: chunk.id, + sourceText: chunk.text, + translatedText: '', + replySuggestions: [], + sourceFinal: chunk.finalized, + translatedFinal: false, + createdAt + }; + this.upsertLine(line); + + await this.writeLog({ + type: 'transcript.chunk', + timestamp: createdAt, + payload: { + provider: this.sttProvider?.key, + sourceText: chunk.text + } + }); + + try { + const translated = await this.translationProvider.translate(chunk); + if (runId !== this.activeRunId) { + return; + } + + this.upsertLine({ + ...line, + translatedText: translated.text, + replySuggestions: buildReplySuggestions(chunk.text, translated.text), + translatedFinal: translated.finalized + }); + + this.updateSnapshot({ + translationRoute: routeDetailMessage + ? { + ...this.snapshot.translationRoute, + active: 'deepl-fallback', + activeLabel: PROVIDER_LABELS['deepl-fallback'], + fallbackUsed: true, + detail: `${PROVIDER_LABELS[selectedProvider]}가 실패하여 DeepL 대체 경로로 전환되었습니다. ${routeDetailMessage}` + } + : this.snapshot.translationRoute + }); + + await this.debug('worker.translation.completed', { + runId, + chunkId: chunk.id, + translatedLength: translated.text.length, + translatedFinal: translated.finalized, + provider: this.translationProvider.key + }); + + await this.writeLog({ + type: 'translation.chunk', + timestamp: new Date().toISOString(), + payload: { + provider: this.translationProvider.key, + sourceId: chunk.id, + translatedText: translated.text, + fallbackUsed: this.snapshot.translationRoute.fallbackUsed + } + }); + } catch (error) { + const rawMessage = error instanceof Error ? error.message : String(error); + const message = this.snapshot.logPathDetail ? `${rawMessage} ${this.snapshot.logPathDetail}` : rawMessage; + const retryCount = this.snapshot.retryCount + 1; + this.updateSnapshot({ + retryCount, + lastError: message, + healthMessage: + MODE_CONFIG[mode].translatorKind === 'remote' + ? '선택한 번역 경로가 실패했습니다. 인증 또는 네트워크를 확인한 뒤 다시 시작하세요.' + : 'provider 실패를 기록했고 런타임은 재시도를 위해 살아 있습니다.' + }); + + await this.debug('worker.translation.failed', { + runId, + chunkId: chunk.id, + message, + error + }); + + await this.writeLog({ + type: 'runtime.retry', + timestamp: new Date().toISOString(), + payload: { + retryCount, + message + } + }); + } + }); + + await this.debug('worker.session.running', { + runId, + startedAt: this.snapshot.startedAt, + routeLabel: this.snapshot.routeLabel, + selectedProvider, + activeProvider: this.snapshot.translationRoute.active + }); + } catch (error) { + const rawMessage = error instanceof Error ? error.message : String(error); + const message = this.snapshot.logPathDetail ? `${rawMessage} ${this.snapshot.logPathDetail}` : rawMessage; + this.updateSnapshot({ + phase: 'error', + capture: { + ...this.snapshot.capture, + phase: 'error', + lastError: message, + guidance: message + }, + lastError: message, + healthMessage: '실오디오 캡처 초기화에 실패했습니다.' + }); + await this.debug('worker.session.start.failed', { + runId, + message, + error + }); + throw error; + } + } + + private async stop() { + await this.debug('worker.session.stopping', { + phase: this.snapshot.phase, + mode: this.snapshot.mode, + lineCount: this.snapshot.lines.length + }); + + this.activeRunId += 1; + + if (this.snapshot.phase === 'idle') { + return; + } + + this.updateSnapshot({ + phase: 'stopping', + healthMessage: 'provider를 중지하고 로컬 로그를 정리하는 중입니다.' + }); + + await this.sttProvider?.stop(); + this.sttProvider = null; + this.translationProvider = null; + + await this.writeLog({ + type: 'session.stopped', + timestamp: new Date().toISOString(), + payload: { + retryCount: this.snapshot.retryCount, + lineCount: this.snapshot.lines.length + } + }); + + await this.debug('worker.session.stopped', { + runId: this.activeRunId, + retryCount: this.snapshot.retryCount, + lines: this.snapshot.lines.length + }); + + this.updateSnapshot({ + phase: 'idle', + capture: createCaptureSnapshot(process.platform), + startedAt: null, + healthMessage: '중지되었습니다. 다시 시작할 수 있습니다.' + }); + } +} diff --git a/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-worker.ts b/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-worker.ts new file mode 100644 index 0000000..388bca6 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/electron/worker/runtime-worker.ts @@ -0,0 +1,63 @@ +import { TranslatorRuntime, type RuntimeCommand } from './runtime-core.js'; + +const runtime = new TranslatorRuntime({ + onSnapshot(snapshot) { + if (process.send) { + process.send({ type: 'status', payload: snapshot }); + } + } +}); + +function emitSnapshot() { + if (process.send) { + process.send({ type: 'status', payload: runtime.getSnapshot() }); + } +} + +async function reportFatal(event: string, error?: unknown) { + try { + await runtime.reportHostIssue(event, error); + } catch { + // Best-effort reporting only. + } +} + +process.on('message', async (message: RuntimeCommand) => { + try { + await runtime.handleCommand(message); + } catch (error) { + await reportFatal('worker.command.failed', error); + if (process.send) { + process.send({ + type: 'status', + payload: { + ...runtime.getSnapshot(), + phase: 'error', + lastError: error instanceof Error ? error.message : String(error) + } + }); + } + } +}); + +process.on('disconnect', () => { + void reportFatal('worker.process.disconnect'); +}); + +process.on('SIGTERM', () => { + void reportFatal('worker.process.sigterm'); +}); + +process.on('SIGINT', () => { + void reportFatal('worker.process.sigint'); +}); + +process.on('uncaughtException', (error) => { + void reportFatal('worker.process.uncaught-exception', error); +}); + +process.on('unhandledRejection', (reason) => { + void reportFatal('worker.process.unhandled-rejection', reason); +}); + +emitSnapshot(); diff --git a/windows-jp-ko-translator-transfer-20260504/index.html b/windows-jp-ko-translator-transfer-20260504/index.html new file mode 100644 index 0000000..394eeaf --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/index.html @@ -0,0 +1,15 @@ + + + + + + JP -> KO Realtime Translator + + +
+ + + diff --git a/windows-jp-ko-translator-transfer-20260504/package-lock.json b/windows-jp-ko-translator-transfer-20260504/package-lock.json new file mode 100644 index 0000000..2fb81ad --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/package-lock.json @@ -0,0 +1,6627 @@ +{ + "name": "windows-jp-ko-realtime-translator", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "windows-jp-ko-realtime-translator", + "version": "0.1.0", + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^5.0.0", + "electron": "^36.2.0", + "electron-builder": "^26.0.12", + "typescript": "^5.8.3", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", + "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "36.9.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-36.9.5.tgz", + "integrity": "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/windows-jp-ko-translator-transfer-20260504/package.json b/windows-jp-ko-translator-transfer-20260504/package.json new file mode 100644 index 0000000..f0ca118 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/package.json @@ -0,0 +1,96 @@ +{ + "name": "windows-jp-ko-realtime-translator", + "version": "0.1.0", + "private": true, + "description": "Cross-platform Japanese to Korean subtitle translator with provider selection for Codex auth, Gemini API, ChatGPT API, and DeepL fallback", + "type": "module", + "main": "dist-electron/main/main.js", + "scripts": { + "dev:renderer": "vite", + "build:renderer": "vite build", + "build:main": "tsc -p tsconfig.node.json", + "build": "npm run build:renderer && npm run build:main", + "typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.renderer.json --noEmit", + "verify:runtime": "npm run build:main && node dist-electron/verify-runtime.js", + "verify:runtime:report": "npm run build:main && node dist-electron/verify-runtime.js --report-file artifacts/runtime-verify-latest.json", + "prepare:stt:mac": "node scripts/prepare-distil-faster-whisper.mjs", + "prepare:ffmpeg:windows": "node scripts/prepare-ffmpeg.mjs", + "start": "electron dist-electron/main/main.js", + "pack:windows:portable": "npm run prepare:ffmpeg:windows && npm run build && electron-builder --win portable --config.win.signAndEditExecutable=false", + "pack:windows:exe-bundle": "npm run prepare:ffmpeg:windows && npm run build && electron-builder --win dir --x64 --config.win.signAndEditExecutable=false && node scripts/package-windows-exe-bundle.mjs", + "release:windows": "npm run pack:windows:exe-bundle && node scripts/create-release.mjs --legacy-source", + "release:windows-portable": "npm run pack:windows:portable", + "release:windows-legacy-source": "npm run build && node scripts/create-release.mjs --legacy-source", + "release:windows-debug": "npm run release:windows-legacy-source" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^5.0.0", + "electron": "^36.2.0", + "electron-builder": "^26.0.12", + "typescript": "^5.8.3", + "vite": "^7.0.0" + }, + "build": { + "appId": "com.mingu.jpkotranslator", + "productName": "JP-KO Translator", + "artifactName": "JP-KO-Translator-${version}-${env.RELEASE_STAMP}-windows-portable.${ext}", + "directories": { + "output": "release/electron-builder" + }, + "files": [ + "dist/**/*", + "dist-electron/**/*", + "package.json" + ], + "extraResources": [ + { + "from": "vendor/bin/win32/x64", + "to": "b/wx", + "filter": [ + "ffmpeg.exe", + "whisper-cli.exe", + "stream.exe", + "ggml-base.bin", + "SDL2.dll", + "whisper.dll" + ] + } + ], + "asar": true, + "mac": { + "target": [ + "dir" + ], + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.inherit.plist", + "extendInfo": { + "NSMicrophoneUsageDescription": "Japanese audio input is captured only to transcribe and translate it into Korean subtitles in real time." + } + }, + "win": { + "target": [ + { + "target": "portable", + "arch": [ + "x64" + ] + } + ] + }, + "portable": { + "artifactName": "JP-KO-Translator-${version}-${env.RELEASE_STAMP}-windows-portable.${ext}" + }, + "extraMetadata": { + "main": "dist-electron/main/main.js" + } + } +} diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/create-release.mjs b/windows-jp-ko-translator-transfer-20260504/scripts/create-release.mjs new file mode 100644 index 0000000..3d8524a --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/create-release.mjs @@ -0,0 +1,56 @@ +import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; + +const root = process.cwd(); +const pkg = JSON.parse(execSync('node -p "JSON.stringify(require(\'./package.json\'))"', { cwd: root, encoding: 'utf8' })); +const stamp = process.env.RELEASE_STAMP || new Date().toISOString().slice(0, 10).replace(/-/g, ''); +const version = pkg.version; +const releaseRoot = path.join(root, 'release'); +const buildLegacySource = process.argv.includes('--legacy-source'); + +if (!buildLegacySource) { + console.log('No legacy source bundle requested.'); + process.exit(0); +} + +const folderName = `JP-KO-Translator-${version}-${stamp}-windows-source-bundle`; +const outDir = path.join(releaseRoot, folderName); +const zipPath = path.join(releaseRoot, `${folderName}.zip`); + +if (!existsSync(path.join(root, 'dist')) || !existsSync(path.join(root, 'dist-electron'))) { + throw new Error('Build outputs are missing. Run npm run build first.'); +} + +rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); +mkdirSync(path.join(outDir, 'app'), { recursive: true }); +mkdirSync(path.join(outDir, 'docs'), { recursive: true }); + +cpSync(path.join(root, 'dist'), path.join(outDir, 'app', 'dist'), { recursive: true }); +cpSync(path.join(root, 'dist-electron'), path.join(outDir, 'app', 'dist-electron'), { recursive: true }); +cpSync(path.join(root, 'package.json'), path.join(outDir, 'app', 'package.json')); +cpSync(path.join(root, 'package-lock.json'), path.join(outDir, 'app', 'package-lock.json')); +cpSync(path.join(root, 'README.md'), path.join(outDir, 'docs', 'README.md')); +cpSync(path.join(root, 'docs', 'WINDOWS_RELEASE_GUIDE.md'), path.join(outDir, 'docs', 'WINDOWS_RELEASE_GUIDE.md')); +cpSync(path.join(root, 'docs', 'QA_RELEASE_CHECKLIST.md'), path.join(outDir, 'docs', 'QA_RELEASE_CHECKLIST.md')); +cpSync(path.join(root, 'scripts', 'launch-windows.cmd'), path.join(outDir, 'JP-KO Translator.cmd')); + +writeFileSync( + path.join(outDir, 'VERSION.txt'), + [ + 'Product: JP-KO Translator', + `Version: ${version}`, + `Release stamp: ${stamp}`, + 'Package type: windows-legacy-source-bundle', + 'Primary user package: use the portable .exe build instead of this bundle whenever possible.', + 'Runtime requirement: Node.js LTS must be installed on Windows before launch.', + 'Failure diagnostics: see launcher.log next to JP-KO Translator.cmd.' + ].join('\n') + '\n', + 'utf8' +); + +rmSync(zipPath, { force: true }); +execSync(`zip -qry ${JSON.stringify(zipPath)} ${JSON.stringify(folderName)}`, { cwd: releaseRoot, stdio: 'inherit' }); + +console.log(zipPath); diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows-debug.cmd b/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows-debug.cmd new file mode 100644 index 0000000..21129a4 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows-debug.cmd @@ -0,0 +1,88 @@ +@echo off +setlocal enableextensions + +set "ROOT_DIR=%~dp0" +set "APP_DIR=%ROOT_DIR%app" +set "LOG_FILE=%ROOT_DIR%launcher.log" + +if exist "%LOG_FILE%" del /f /q "%LOG_FILE%" >nul 2>nul +call :log ================================================== +call :log JP-KO Translator launcher started +call :log Root: %ROOT_DIR% +call :log App: %APP_DIR% +call :log ================================================== + +echo JP-KO Translator launcher +echo Logs: launcher.log +echo. + +if not exist "%APP_DIR%\package.json" ( + call :fail "app\package.json is missing. Please extract the entire zip before launching." +) + +if not exist "%APP_DIR%\dist\index.html" ( + call :fail "Renderer build is missing: app\dist\index.html" +) + +if not exist "%APP_DIR%\dist-electron\main\main.js" ( + call :fail "Electron main build is missing: app\dist-electron\main\main.js" +) + +where node >nul 2>nul +if errorlevel 1 ( + call :fail "Node.js was not found in PATH. Install Node.js LTS first, then run JP-KO Translator.cmd again." +) + +where npm >nul 2>nul +if errorlevel 1 ( + call :fail "npm was not found in PATH. Install Node.js LTS first, then run JP-KO Translator.cmd again." +) + +pushd "%APP_DIR%" >nul +if errorlevel 1 ( + call :fail "Could not enter the app directory." +) + +call :log Working directory changed to %CD% + +if not exist node_modules ( + echo Installing runtime dependencies... + call :log Running npm install + call npm.cmd install >> "%LOG_FILE%" 2>&1 + if errorlevel 1 ( + popd >nul + call :fail "npm install failed. See launcher.log for details." + ) +) else ( + call :log node_modules already exists, skipping npm install +) + +echo Translation provider is chosen inside the app settings UI. +echo Starting Electron app... +echo. +call :log Running npm start +call npm.cmd start >> "%LOG_FILE%" 2>&1 +set "EXIT_CODE=%ERRORLEVEL%" +popd >nul + +if not "%EXIT_CODE%"=="0" ( + call :fail "The app exited with code %EXIT_CODE%. See launcher.log for details." +) + +call :log npm start exited successfully +endlocal +exit /b 0 + +:log +echo [%date% %time%] %*>> "%LOG_FILE%" +exit /b 0 + +:fail +call :log ERROR: %~1 +echo. +echo ERROR: %~1 +echo Check launcher.log next to this file. +echo. +pause +endlocal +exit /b 1 diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows.cmd b/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows.cmd new file mode 100644 index 0000000..21129a4 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/launch-windows.cmd @@ -0,0 +1,88 @@ +@echo off +setlocal enableextensions + +set "ROOT_DIR=%~dp0" +set "APP_DIR=%ROOT_DIR%app" +set "LOG_FILE=%ROOT_DIR%launcher.log" + +if exist "%LOG_FILE%" del /f /q "%LOG_FILE%" >nul 2>nul +call :log ================================================== +call :log JP-KO Translator launcher started +call :log Root: %ROOT_DIR% +call :log App: %APP_DIR% +call :log ================================================== + +echo JP-KO Translator launcher +echo Logs: launcher.log +echo. + +if not exist "%APP_DIR%\package.json" ( + call :fail "app\package.json is missing. Please extract the entire zip before launching." +) + +if not exist "%APP_DIR%\dist\index.html" ( + call :fail "Renderer build is missing: app\dist\index.html" +) + +if not exist "%APP_DIR%\dist-electron\main\main.js" ( + call :fail "Electron main build is missing: app\dist-electron\main\main.js" +) + +where node >nul 2>nul +if errorlevel 1 ( + call :fail "Node.js was not found in PATH. Install Node.js LTS first, then run JP-KO Translator.cmd again." +) + +where npm >nul 2>nul +if errorlevel 1 ( + call :fail "npm was not found in PATH. Install Node.js LTS first, then run JP-KO Translator.cmd again." +) + +pushd "%APP_DIR%" >nul +if errorlevel 1 ( + call :fail "Could not enter the app directory." +) + +call :log Working directory changed to %CD% + +if not exist node_modules ( + echo Installing runtime dependencies... + call :log Running npm install + call npm.cmd install >> "%LOG_FILE%" 2>&1 + if errorlevel 1 ( + popd >nul + call :fail "npm install failed. See launcher.log for details." + ) +) else ( + call :log node_modules already exists, skipping npm install +) + +echo Translation provider is chosen inside the app settings UI. +echo Starting Electron app... +echo. +call :log Running npm start +call npm.cmd start >> "%LOG_FILE%" 2>&1 +set "EXIT_CODE=%ERRORLEVEL%" +popd >nul + +if not "%EXIT_CODE%"=="0" ( + call :fail "The app exited with code %EXIT_CODE%. See launcher.log for details." +) + +call :log npm start exited successfully +endlocal +exit /b 0 + +:log +echo [%date% %time%] %*>> "%LOG_FILE%" +exit /b 0 + +:fail +call :log ERROR: %~1 +echo. +echo ERROR: %~1 +echo Check launcher.log next to this file. +echo. +pause +endlocal +exit /b 1 diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/package-windows-exe-bundle.mjs b/windows-jp-ko-translator-transfer-20260504/scripts/package-windows-exe-bundle.mjs new file mode 100644 index 0000000..188f5a5 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/package-windows-exe-bundle.mjs @@ -0,0 +1,30 @@ +import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; + +const root = process.cwd(); +const stamp = process.env.RELEASE_STAMP || new Date().toISOString().slice(0, 10).replace(/-/g, ''); +const pkg = JSON.parse(execSync('node -p "JSON.stringify(require(\'./package.json\'))"', { cwd: root, encoding: 'utf8' })); +const version = pkg.version; +const releaseRoot = path.join(root, 'release'); +const builderOut = path.join(releaseRoot, 'electron-builder'); +const sourceDir = path.join(builderOut, 'win-unpacked'); +const folderName = `wjkt-win-x64`; +const outDir = path.join(releaseRoot, folderName); +const zipPath = path.join(releaseRoot, `${folderName}.zip`); + +if (!existsSync(path.join(sourceDir, 'JP-KO Translator.exe'))) { + throw new Error('Missing win-unpacked/JP-KO Translator.exe. Run the electron-builder dir build first.'); +} + +rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); +cpSync(sourceDir, outDir, { recursive: true }); + +rmSync(zipPath, { force: true }); +execSync(`zip -qry ${JSON.stringify(zipPath)} ${JSON.stringify(folderName)}`, { + cwd: releaseRoot, + stdio: 'inherit' +}); + +console.log(zipPath); diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/prepare-distil-faster-whisper.mjs b/windows-jp-ko-translator-transfer-20260504/scripts/prepare-distil-faster-whisper.mjs new file mode 100644 index 0000000..903bafc --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/prepare-distil-faster-whisper.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +import { mkdir, realpath, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const root = process.cwd(); +const pythonDir = path.join(root, 'vendor', 'python', 'darwin', 'arm64'); +const modelDir = path.join(root, 'vendor', 'models', 'darwin', 'arm64', 'distil-large-v3-ct2'); +const runtimePython = path.join(pythonDir, 'bin', 'python3'); + +function run(command, args, env = process.env) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: root, + env, + stdio: 'inherit' + }); + child.on('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`)); + }); + child.on('error', reject); + }); +} + +await rm(pythonDir, { recursive: true, force: true }); +await mkdir(path.dirname(pythonDir), { recursive: true }); +await mkdir(modelDir, { recursive: true }); + +const uvPythonPath = (await new Promise((resolve, reject) => { + let stdout = ''; + const child = spawn('uv', ['python', 'find', '3.11'], { + cwd: root, + env: process.env, + stdio: ['ignore', 'pipe', 'inherit'] + }); + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.on('exit', (code) => { + if (code === 0) resolve(stdout.trim()); + else reject(new Error(`uv python find 3.11 exited with code ${code ?? 'unknown'}`)); + }); + child.on('error', reject); +})); +const uvRuntimeDir = await realpath(path.dirname(path.dirname(uvPythonPath))); +await run('rsync', ['-a', `${uvRuntimeDir}/`, `${pythonDir}/`]); +await run('uv', ['pip', 'install', '--python', runtimePython, '--target', path.join(pythonDir, 'lib', 'python3.11', 'site-packages'), 'faster-whisper==1.1.1', 'huggingface-hub==0.34.4']); +await run(runtimePython, ['-c', [ + 'from huggingface_hub import snapshot_download', + `snapshot_download(repo_id="Systran/faster-distil-whisper-large-v3", local_dir=r"${modelDir}", local_dir_use_symlinks=False)` +].join('; ')]); + +console.log(JSON.stringify({ + prepared: true, + pythonDir, + modelDir, + runtimePython, + bundledPythonSource: uvRuntimeDir +}, null, 2)); diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/prepare-ffmpeg.mjs b/windows-jp-ko-translator-transfer-20260504/scripts/prepare-ffmpeg.mjs new file mode 100644 index 0000000..f92aca8 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/prepare-ffmpeg.mjs @@ -0,0 +1,157 @@ +import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'; +import path from 'node:path'; +import { pipeline } from 'node:stream/promises'; +import { fileURLToPath } from 'node:url'; +import https from 'node:https'; +import { execFileSync } from 'node:child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const root = path.join(__dirname, '..'); +const vendorRoot = path.join(root, 'vendor', 'bin', 'win32', 'x64'); +const tempRoot = path.join(root, '.tmp-windows-tools'); +const ffmpegPath = path.join(vendorRoot, 'ffmpeg.exe'); +const whisperCliPath = path.join(vendorRoot, 'whisper-cli.exe'); +const whisperStreamPath = path.join(vendorRoot, 'stream.exe'); +const whisperModelPath = path.join(vendorRoot, 'ggml-base.bin'); +const whisperRuntimeDlls = ['SDL2.dll', 'whisper.dll']; +const ffmpegUrl = process.env.FFMPEG_WINDOWS_URL || 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip'; +const whisperZipUrl = process.env.WHISPER_WINDOWS_URL || 'https://remotion-ffmpeg-binaries.s3.eu-central-1.amazonaws.com/whisper-bin-x64-1-5-5.zip'; +const whisperModelUrl = process.env.WHISPER_MODEL_URL || 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin'; + +function download(url, destination) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { 'User-Agent': 'openclaw-jp-ko-translator' } }, (response) => { + if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + response.resume(); + download(response.headers.location, destination).then(resolve).catch(reject); + return; + } + if (response.statusCode !== 200) { + reject(new Error(`Failed to download ${url}: HTTP ${response.statusCode}`)); + response.resume(); + return; + } + const file = createWriteStream(destination); + pipeline(response, file).then(resolve).catch(reject); + }).on('error', reject); + }); +} + +function walkFiles(rootPath) { + const entries = readdirSync(rootPath, { withFileTypes: true }); + return entries.flatMap((entry) => { + const fullPath = path.join(rootPath, entry.name); + return entry.isDirectory() ? walkFiles(fullPath) : [fullPath]; + }); +} + +function extractFirstMatch(archivePath, extractRoot, matcher) { + execFileSync('tar', ['-xf', archivePath, '-C', extractRoot], { stdio: 'inherit' }); + const pattern = new RegExp(matcher); + const found = walkFiles(extractRoot).find((filePath) => pattern.test(filePath)); + if (!found) { + throw new Error(`Archive ${archivePath} did not contain a file matching ${matcher}`); + } + return found; +} + +function copyWhisperRuntimeFiles(executablePath) { + const sourceDirectory = path.dirname(executablePath); + const streamPath = path.join(sourceDirectory, 'stream.exe'); + if (existsSync(streamPath)) { + copyFileSync(streamPath, whisperStreamPath); + chmodSync(whisperStreamPath, 0o755); + } + for (const dllName of whisperRuntimeDlls) { + const sourcePath = path.join(sourceDirectory, dllName); + if (existsSync(sourcePath)) { + copyFileSync(sourcePath, path.join(vendorRoot, dllName)); + chmodSync(path.join(vendorRoot, dllName), 0o755); + } + } +} + +function ffmpegSupportsInputDevice(targetPath, inputDevice) { + if (!existsSync(targetPath)) { + return false; + } + try { + const output = execFileSync(targetPath, ['-hide_banner', '-devices'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); + return new RegExp(`\\b${inputDevice}\\b`, 'i').test(output); + } catch { + return false; + } +} + +function ffmpegHasWindowsAudioInput(targetPath) { + return ffmpegSupportsInputDevice(targetPath, 'wasapi') || ffmpegSupportsInputDevice(targetPath, 'dshow'); +} + +mkdirSync(vendorRoot, { recursive: true }); +if (existsSync(ffmpegPath) && !ffmpegHasWindowsAudioInput(ffmpegPath)) { + console.warn('Existing ffmpeg.exe does not support Windows audio capture; downloading a compatible build.'); + rmSync(ffmpegPath, { force: true }); +} + +if ( + existsSync(ffmpegPath) + && existsSync(whisperCliPath) + && existsSync(whisperStreamPath) + && existsSync(whisperModelPath) + && whisperRuntimeDlls.every((dllName) => existsSync(path.join(vendorRoot, dllName))) +) { + console.log(vendorRoot); + process.exit(0); +} + +rmSync(tempRoot, { recursive: true, force: true }); +mkdirSync(tempRoot, { recursive: true }); + +if (!existsSync(ffmpegPath)) { + const archivePath = path.join(tempRoot, 'ffmpeg-win64.zip'); + const extractRoot = path.join(tempRoot, 'ffmpeg-extract'); + mkdirSync(extractRoot, { recursive: true }); + await download(ffmpegUrl, archivePath); + const extractedPath = extractFirstMatch(archivePath, extractRoot, 'ffmpeg\\.exe$'); + copyFileSync(extractedPath, ffmpegPath); + if (!ffmpegHasWindowsAudioInput(ffmpegPath)) { + rmSync(ffmpegPath, { force: true }); + throw new Error('Downloaded ffmpeg.exe does not support Windows audio capture. Set FFMPEG_WINDOWS_URL to a Windows build with wasapi or dshow input.'); + } + if (!ffmpegSupportsInputDevice(ffmpegPath, 'wasapi')) { + console.warn('Downloaded ffmpeg.exe does not expose WASAPI. The app will require a DirectShow loopback device such as Stereo Mix or virtual-audio-capturer.'); + } + chmodSync(ffmpegPath, 0o755); +} + +if (!existsSync(whisperCliPath)) { + const archivePath = path.join(tempRoot, 'whisper-win64.zip'); + const extractRoot = path.join(tempRoot, 'whisper-extract'); + mkdirSync(extractRoot, { recursive: true }); + await download(whisperZipUrl, archivePath); + const extractedPath = extractFirstMatch(archivePath, extractRoot, 'whisper-cli\\.exe$|whisper\\.exe$|main\\.exe$'); + copyFileSync(extractedPath, whisperCliPath); + copyWhisperRuntimeFiles(extractedPath); + chmodSync(whisperCliPath, 0o755); +} + +if ( + existsSync(whisperCliPath) + && (!existsSync(whisperStreamPath) || !whisperRuntimeDlls.every((dllName) => existsSync(path.join(vendorRoot, dllName)))) +) { + const archivePath = path.join(tempRoot, 'whisper-win64.zip'); + const extractRoot = path.join(tempRoot, 'whisper-extract'); + mkdirSync(extractRoot, { recursive: true }); + await download(whisperZipUrl, archivePath); + const extractedPath = extractFirstMatch(archivePath, extractRoot, 'whisper-cli\\.exe$|whisper\\.exe$|main\\.exe$'); + copyWhisperRuntimeFiles(extractedPath); +} + +if (!existsSync(whisperModelPath)) { + await download(whisperModelUrl, whisperModelPath); + chmodSync(whisperModelPath, 0o644); +} + +rmSync(tempRoot, { recursive: true, force: true }); +console.log(vendorRoot); diff --git a/windows-jp-ko-translator-transfer-20260504/scripts/transcribe_distil_faster_whisper.py b/windows-jp-ko-translator-transfer-20260504/scripts/transcribe_distil_faster_whisper.py new file mode 100644 index 0000000..e635c95 --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/scripts/transcribe_distil_faster_whisper.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--audio', required=True) + parser.add_argument('--model-dir', required=True) + parser.add_argument('--output', required=True) + parser.add_argument('--language', default='ja') + args = parser.parse_args() + + try: + from faster_whisper import WhisperModel + except Exception as exc: + print(f'failed to import faster_whisper: {exc}', file=sys.stderr) + return 2 + + model = WhisperModel(args.model_dir, device='cpu', compute_type='int8') + segments, info = model.transcribe(args.audio, language=args.language, vad_filter=True) + text = ' '.join(segment.text.strip() for segment in segments if segment.text.strip()).strip() + + payload = { + 'text': text, + 'language': getattr(info, 'language', args.language), + 'duration': getattr(info, 'duration', None) + } + Path(args.output).write_text(json.dumps(payload, ensure_ascii=False), encoding='utf-8') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/windows-jp-ko-translator-transfer-20260504/src/App.tsx b/windows-jp-ko-translator-transfer-20260504/src/App.tsx new file mode 100644 index 0000000..d057a2e --- /dev/null +++ b/windows-jp-ko-translator-transfer-20260504/src/App.tsx @@ -0,0 +1,643 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { + CaptureSnapshot, + ProviderConfigState, + RuntimeMode, + TranslationProviderId, + TranslationProviderStatus, + WorkerSnapshot +} from '../electron/preload'; + +type ViewId = 'translate' | 'settings'; + +const providerOrder: TranslationProviderId[] = ['codex-auth', 'auto', 'chatgpt-api', 'gemini-api', 'deepl-fallback', 'local-fixture']; + +const providerLabels: Record = { + auto: '자동 선택', + 'codex-auth': 'Codex auth', + 'chatgpt-api': 'GPT API', + 'gemini-api': 'Gemini', + 'deepl-fallback': 'DeepL', + 'local-fixture': '로컬 테스트' +}; + +const modeLabels: Record = { + hybrid: '권장', + cloud: '클라우드 번역', + local: '로컬 확인' +}; + +const defaultProviderStatuses: TranslationProviderStatus[] = providerOrder.map((id) => ({ + id, + label: providerLabels[id], + configured: id === 'local-fixture', + available: id === 'local-fixture', + state: id === 'local-fixture' ? 'ready' : id === 'codex-auth' ? 'manual-auth' : 'missing-config', + detail: id === 'codex-auth' ? 'Codex CLI 로그인 세션을 사용합니다.' : '설정이 필요합니다.' +})); + +const emptyConfig: ProviderConfigState = { + codexAuthPath: '', + values: { + chatgptApiKey: '', + openaiBaseUrl: '', + chatgptModel: '', + geminiApiKey: '', + geminiBaseUrl: '', + geminiModel: '', + deeplApiKey: '', + deeplBaseUrl: '', + codexModel: '', + macosCaptureDevice: '', + windowsCaptureDevice: '', + ffmpegPath: '', + whisperPath: '', + whisperModelPath: '', + userContext: '', + glossary: '' + } +}; + +const idleCapture: CaptureSnapshot = { + phase: 'ready', + backend: 'windows-wasapi-loopback', + target: 'system-audio', + platform: 'win32', + deviceLabel: 'Default Windows Output', + supportsSystemAudio: true, + recoveryAttempts: 0, + guidance: 'Windows 시스템 소리를 Electron loopback으로 받아옵니다.', + lastError: null, + backendLabel: 'Electron Desktop Audio Bridge', + backendAvailability: 'native-ready', + availableBackends: [] +}; + +const idleSnapshot: WorkerSnapshot = { + phase: 'idle', + mode: 'hybrid', + sourceLanguage: 'ja', + targetLanguage: 'ko', + transcriptSource: 'live-capture', + transcriptSourceLabel: '실시간 오디오 입력', + liveCaptureImplemented: true, + providerSummary: { stt: 'live-capture.ffmpeg+whisper', translation: 'codex-auth' }, + translationRoute: { + selected: 'codex-auth', + active: 'codex-auth', + fallbackUsed: false, + selectedLabel: 'Codex auth', + activeLabel: 'Codex auth', + detail: '번역 경로를 준비 중입니다.', + providerStatuses: defaultProviderStatuses + }, + modeDescriptor: { + label: '권장', + description: '로컬 STT와 Codex auth 번역을 함께 사용합니다.', + localFirst: true, + cloudEnabled: true, + requiresCredentials: true + }, + capture: idleCapture, + startedAt: null, + lastError: null, + retryCount: 0, + healthMessage: '대기 중입니다.', + logPath: null, + debugLogPath: null, + logDirectory: null, + logPathDetail: null, + routeLabel: '오디오 입력 + Codex auth', + lines: [] +}; + +function phaseText(phase: WorkerSnapshot['phase']) { + switch (phase) { + case 'starting': return '시작 중'; + case 'running': return '번역 중'; + case 'stopping': return '정지 중'; + case 'error': return '오류'; + default: return '대기'; + } +} + +function getFallbackSuggestions() { + return [ + { + id: 'fallback-1', + korean: '확인했습니다. 로그를 먼저 열어볼게요.', + japanese: '確認しました。まずログを開いて確認します。', + reading: 'kakunin shimashita. mazu rogu o hiraite kakunin shimasu.' + }, + { + id: 'fallback-2', + korean: '기동 전류 파형도 같이 비교해 보겠습니다.', + japanese: '起動電流の波形もあわせて比較します。', + reading: 'kido denryu no hakei mo awasete hikaku shimasu.' + }, + { + id: 'fallback-3', + korean: '보호 회로 조건을 용어집 기준으로 정리하겠습니다.', + japanese: '保護回路の条件を用語集に沿って整理します。', + reading: 'hogo kairo no joken o yogoshu ni sotte seiri shimasu.' + } + ]; +} + +function providerStateText(status: TranslationProviderStatus) { + if (status.state === 'active') return '사용 중'; + if (status.state === 'fallback-active') return '대체 사용'; + if (status.available) return '준비됨'; + if (status.state === 'manual-auth') return '로그인 필요'; + return '설정 필요'; +} + +type AudioCaptureSession = { + stop: () => void; +}; + +const audioHopMs = 1_000; +const rollingWindowMs = 6_000; +const minRollingWindowMs = 2_000; +const wavSampleRate = 16_000; +const useRendererAudioBridge = false; + +function mergeFloat32Buffers(buffers: Float32Array[]) { + const length = buffers.reduce((total, buffer) => total + buffer.length, 0); + const merged = new Float32Array(length); + let offset = 0; + for (const buffer of buffers) { + merged.set(buffer, offset); + offset += buffer.length; + } + return merged; +} + +function downsampleBuffer(input: Float32Array, inputSampleRate: number, outputSampleRate: number) { + if (inputSampleRate === outputSampleRate) { + return input; + } + + const ratio = inputSampleRate / outputSampleRate; + const outputLength = Math.max(1, Math.floor(input.length / ratio)); + const output = new Float32Array(outputLength); + for (let i = 0; i < outputLength; i += 1) { + const start = Math.floor(i * ratio); + const end = Math.min(input.length, Math.floor((i + 1) * ratio)); + let sum = 0; + let count = 0; + for (let j = start; j < end; j += 1) { + sum += input[j]; + count += 1; + } + output[i] = count > 0 ? sum / count : 0; + } + return output; +} + +function encodeMonoWav(samples: Float32Array, sampleRate: number) { + const bytesPerSample = 2; + const blockAlign = bytesPerSample; + const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample); + const view = new DataView(buffer); + const writeString = (offset: number, value: string) => { + for (let i = 0; i < value.length; i += 1) { + view.setUint8(offset + i, value.charCodeAt(i)); + } + }; + + writeString(0, 'RIFF'); + view.setUint32(4, 36 + samples.length * bytesPerSample, true); + writeString(8, 'WAVE'); + writeString(12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * blockAlign, true); + view.setUint16(32, blockAlign, true); + view.setUint16(34, 16, true); + writeString(36, 'data'); + view.setUint32(40, samples.length * bytesPerSample, true); + + let offset = 44; + for (let i = 0; i < samples.length; i += 1) { + const clamped = Math.max(-1, Math.min(1, samples[i])); + view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true); + offset += bytesPerSample; + } + + return new Uint8Array(buffer); +} + +export function App() { + const runtime = window.translatorRuntime; + const [view, setView] = useState('translate'); + const [snapshot, setSnapshot] = useState(idleSnapshot); + const [mode, setMode] = useState('hybrid'); + const [selectedProvider, setSelectedProvider] = useState('codex-auth'); + const [providerConfig, setProviderConfig] = useState(emptyConfig); + const [bridgeError, setBridgeError] = useState(null); + const [notice, setNotice] = useState(null); + const [busyAction, setBusyAction] = useState(null); + const [showApiKeys, setShowApiKeys] = useState(false); + const [audioCapture, setAudioCapture] = useState(null); + const [audioBridge, setAudioBridge] = useState({ + state: 'idle' as 'idle' | 'starting' | 'recording' | 'error', + detail: '시작을 누르면 whisper.cpp stream.exe가 오디오 장치를 직접 열어 받아쓰기합니다.', + chunks: 0, + bytes: 0 + }); + + useEffect(() => { + if (!runtime) { + setBridgeError('Electron preload bridge를 찾지 못했습니다.'); + return undefined; + } + + Promise.all([runtime.getWorkerStatus(), runtime.getProviderConfig()]) + .then(([status, config]) => { + setSnapshot(status); + setMode(status.mode); + setSelectedProvider(status.translationRoute.selected === 'local-fixture' ? 'codex-auth' : status.translationRoute.selected); + setProviderConfig(config); + setBridgeError(null); + }) + .catch((error: Error) => setBridgeError(error.message)); + + const cleanup = runtime.onWorkerStatus((status) => { + setSnapshot(status); + setMode(status.mode); + setBridgeError(null); + }); + return () => { + cleanup(); + }; + }, [runtime]); + + const providerStatuses = snapshot.translationRoute.providerStatuses.length > 0 + ? snapshot.translationRoute.providerStatuses + : defaultProviderStatuses; + const selectedStatus = providerStatuses.find((status) => status.id === selectedProvider) ?? defaultProviderStatuses[0]; + const latestLine = [...snapshot.lines].reverse().find((line) => line.translatedText.trim()) ?? snapshot.lines[snapshot.lines.length - 1] ?? null; + const suggestions = latestLine?.replySuggestions?.length ? latestLine.replySuggestions : getFallbackSuggestions(); + const isBusy = snapshot.phase === 'starting' || snapshot.phase === 'stopping'; + const selectedUnavailable = + selectedProvider !== 'local-fixture' + && selectedProvider !== 'auto' + && !selectedStatus.available + && selectedStatus.state !== 'manual-auth'; + const canStart = Boolean(runtime) && !bridgeError && !isBusy && snapshot.phase !== 'running' && !selectedUnavailable; + const canStop = Boolean(runtime) && !bridgeError && !isBusy && snapshot.phase !== 'idle'; + + const statusMessage = useMemo(() => { + if (bridgeError) return bridgeError; + if (snapshot.lastError) return snapshot.lastError; + if (notice) return notice; + return snapshot.healthMessage; + }, [bridgeError, notice, snapshot.healthMessage, snapshot.lastError]); + + const updateConfig = (updater: (prev: ProviderConfigState) => ProviderConfigState) => { + setProviderConfig(updater); + setNotice(null); + }; + + const saveConfig = async () => { + if (!runtime) return; + setBusyAction('save'); + try { + const saved = await runtime.saveProviderConfig(providerConfig); + setProviderConfig(saved); + setNotice('설정을 저장했습니다.'); + } catch (error) { + setNotice(error instanceof Error ? error.message : String(error)); + } finally { + setBusyAction(null); + } + }; + + const launchCodexAuth = async () => { + if (!runtime) return; + setBusyAction('codex-login'); + try { + const result = await runtime.launchCodexAuth(); + setNotice(result.detail); + } catch (error) { + setNotice(error instanceof Error ? error.message : String(error)); + } finally { + setBusyAction(null); + } + }; + + const probeProvider = async (provider: TranslationProviderId = selectedProvider) => { + if (!runtime) return; + setBusyAction(`probe-${provider}`); + try { + const result = await runtime.probeTranslation(provider); + setNotice(result.ok + ? `${providerLabels[provider]} 연결 확인 완료${result.translatedText ? `: ${result.translatedText}` : ''}` + : result.detail); + } catch (error) { + setNotice(error instanceof Error ? error.message : String(error)); + } finally { + setBusyAction(null); + } + }; + + const stopRendererAudioCapture = () => { + if (audioCapture) { + audioCapture.stop(); + } + setAudioCapture(null); + setAudioBridge((prev) => ({ + ...prev, + state: 'idle', + detail: '오디오 캡처를 정지했습니다.' + })); + }; + + const startRendererAudioCapture = async () => { + if (!runtime || snapshot.capture.platform !== 'win32') { + return; + } + if (!navigator.mediaDevices?.getDisplayMedia) { + throw new Error('현재 Electron 환경에서 시스템 오디오 캡처 API를 사용할 수 없습니다.'); + } + + setAudioBridge({ state: 'starting', detail: 'Windows loopback 오디오 스트림을 여는 중입니다.', chunks: 0, bytes: 0 }); + const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); + const audioTracks = stream.getAudioTracks(); + const videoTracks = stream.getVideoTracks(); + if (audioTracks.length === 0) { + stream.getTracks().forEach((track) => track.stop()); + throw new Error('Electron이 시스템 오디오 트랙을 받지 못했습니다. 앱을 재시작한 뒤 다시 시도하세요.'); + } + + videoTracks.forEach((track) => { + track.enabled = false; + }); + const audioStream = new MediaStream(audioTracks); + const AudioContextCtor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; + if (!AudioContextCtor) { + stream.getTracks().forEach((track) => track.stop()); + throw new Error('이 Electron 환경에서 Web Audio API를 사용할 수 없습니다.'); + } + + const context = new AudioContextCtor(); + await context.resume(); + const source = context.createMediaStreamSource(audioStream); + const processor = context.createScriptProcessor(4096, 1, 1); + const silentGain = context.createGain(); + silentGain.gain.value = 0; + + let stopped = false; + let buffers: Float32Array[] = []; + let bufferedSamples = 0; + + const flush = () => { + if (stopped || !runtime || buffers.length === 0) { + return; + } + const merged = mergeFloat32Buffers(buffers); + if (merged.length < context.sampleRate * (minRollingWindowMs / 1000)) { + return; + } + + const wav = encodeMonoWav(downsampleBuffer(merged, context.sampleRate, wavSampleRate), wavSampleRate); + runtime.pushRendererAudioChunk({ + data: Array.from(wav), + mimeType: 'audio/wav' + }); + setAudioBridge((prev) => ({ + state: 'recording', + detail: `PCM WAV 오디오를 worker로 전달 중입니다. 마지막 청크 ${Math.round(wav.byteLength / 1024)} KB`, + chunks: prev.chunks + 1, + bytes: prev.bytes + wav.byteLength + })); + }; + + processor.onaudioprocess = (event) => { + if (stopped) { + return; + } + const next = new Float32Array(event.inputBuffer.getChannelData(0)); + buffers.push(next); + bufferedSamples += next.length; + + const maxSamples = Math.ceil(context.sampleRate * (rollingWindowMs / 1000)); + while (bufferedSamples > maxSamples && buffers.length > 1) { + const removed = buffers.shift(); + bufferedSamples -= removed?.length ?? 0; + } + }; + + source.connect(processor); + processor.connect(silentGain); + silentGain.connect(context.destination); + + const timer = window.setInterval(flush, audioHopMs); + const stopSession = () => { + if (stopped) { + return; + } + flush(); + stopped = true; + window.clearInterval(timer); + processor.disconnect(); + source.disconnect(); + silentGain.disconnect(); + stream.getTracks().forEach((track) => track.stop()); + void context.close().catch(() => undefined); + }; + + audioTracks.forEach((track) => { + track.addEventListener('ended', () => { + setAudioBridge((prev) => ({ + ...prev, + state: 'idle', + detail: '시스템 오디오 스트림이 종료되었습니다.' + })); + }, { once: true }); + }); + + setAudioCapture({ stop: stopSession }); + setAudioBridge({ state: 'recording', detail: '최근 6초 오디오를 1초마다 겹쳐서 Whisper에 전달합니다.', chunks: 0, bytes: 0 }); + }; + + const start = async () => { + if (!runtime) return; + setNotice(null); + try { + await runtime.startWorker(mode, selectedProvider); + if (useRendererAudioBridge) { + await startRendererAudioCapture(); + } else { + setAudioBridge({ + state: 'recording', + detail: 'whisper.cpp stream.exe가 상시 실행 중입니다. 텍스트 stdout을 받아 번역합니다.', + chunks: 0, + bytes: 0 + }); + } + } catch (error) { + await runtime.stopWorker().catch(() => undefined); + const message = error instanceof Error ? error.message : String(error); + setNotice(message); + setAudioBridge((prev) => ({ ...prev, state: 'error', detail: message })); + } + }; + + const stop = async () => { + stopRendererAudioCapture(); + await runtime?.stopWorker(); + }; + + const copySuggestion = async (text: string) => { + await navigator.clipboard?.writeText(text); + setNotice('답변 후보를 복사했습니다.'); + }; + + return ( +
+ + +
+
+
+

{view === 'translate' ? 'Realtime JP-KO Translator' : 'Settings'}

+

{view === 'translate' ? '일본어 시스템 음성을 한국어로 받아쓰고 번역합니다.' : 'Codex auth, 오디오, 내 환경, 용어집을 관리합니다.'}

+
+
+ {phaseText(snapshot.phase)} + {view === 'translate' ? ( + <> + + + + ) : ( + + )} +
+
+ + {statusMessage ?
{statusMessage}
: null} + + {view === 'translate' ? ( +
+
+
+ Japanese source + {snapshot.capture.backendLabel} +
+

{latestLine?.sourceText || '시작을 누르면 Windows 시스템 소리를 받아쓰기합니다.'}

+
+ Korean translation +

{latestLine?.translatedText || '번역 결과가 여기에 크게 표시됩니다.'}

+
+ {audioBridge.state === 'recording' ? '오디오 입력 중' : audioBridge.state === 'error' ? '오디오 오류' : '오디오 대기'} + {audioBridge.detail} + chunks {audioBridge.chunks} · {Math.round(audioBridge.bytes / 1024)} KB +
+
+ + +
+ ) : ( +
+
+

번역 경로

+
+ + +
+
+ + +
+
+ {providerStatuses.map((status) => ( + {status.label}: {providerStateText(status)} + ))} +
+
+ +
+

내 환경과 용어집

+
+