diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e54a9b..8427f25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,16 +12,23 @@ env: jobs: unit: - name: Unit tests + name: Unit tests (node ${{ matrix.node }}) runs-on: ubuntu-latest # A stalled apt/playwright install should fail fast, not burn the full # 6h default job budget. The unit suite itself runs in seconds. timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + # Both ends of the supported range. Testing only the top hid a test + # that used a Node 22+ API and silently collected zero assertions + # everywhere below it. + node: [20, 24] steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: 24 + node-version: ${{ matrix.node }} # ffmpeg is needed for boundary-frame extraction and shader pre-render. - run: sudo apt-get update && sudo apt-get install -y ffmpeg - run: npm install diff --git a/CLAUDE.md b/CLAUDE.md index ac25301..8f66f34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,18 @@ Argo turns Playwright demo scripts into polished product demo videos with AI voi - Kokoro TTS defaults: model `onnx-community/Kokoro-82M-v1.0-ONNX`, dtype `q8` - Clear TTS cache if voiceover text changes: `rm -rf .argo//clips` +## Optional Engine Dependencies (`src/optional-deps.ts`) + +All six TTS/STT SDKs (`kokoro-js`, `@huggingface/transformers`, `openai`, `@elevenlabs/elevenlabs-js`, `@google/genai`, `sarvamai`) are **optional peer dependencies**, not `dependencies` or `optionalDependencies`. This is deliberate: npm installs `optionalDependencies` by default (the field only tolerates install *failure*), while `peerDependenciesMeta.optional` is the one field npm never auto-installs. Base install is ~27 MB; a Kokoro user reaches ~435 MB only if they ask for it. + +- Every adapter loads its SDK through `importOptional()`, which converts `ERR_MODULE_NOT_FOUND` into an install hint and lets every other failure propagate untouched. Never replace it with a bare `catch` that assumes "missing package" (that was the old behaviour and it misreported broken installs). +- `src/tts/transcribe.ts` MUST keep its `@huggingface/transformers` import dynamic. A top-level import there is reachable from `src/cli.ts`, so it breaks *every* command (`validate`, `export`, `doctor`) when the package is absent, in all three install modes. This was the single blocker to the whole design. +- `@huggingface/transformers` is declared `^3.5.1 || ^4.2.0` on purpose. `kokoro-js@1.2.1` wants `^3.5.1`; pinning Argo to `^4` forces a second nested copy plus a second ONNX runtime (~765 MB vs ~410 MB) because `overrides` only apply in a root `package.json` and stop working once Argo is a dependency. Word-level Whisper timestamps are verified working on 3.8.1. +- The permissive peer *range* does not license a permissive install *hint*. `TRANSFORMERS_DEP`, `WHISPER_DEP` and `MUSICGEN_DEP` all print `@huggingface/transformers@3`, because `latest` is 4.x and a bare `npm i` next to an existing `kokoro-js` produces exactly the two-runtime tree above. Every command Argo prints must resolve to the major `kokoro-js` shares; `tests/optional-deps.test.ts` pins this. Drop the pin only when `kokoro-js` moves to v4. +- Neither `isDepInstalled()` nor `detectInstallMode()` may propagate a resolver failure. A probe can fail without the package being absent: `ERR_INVALID_PACKAGE_CONFIG` from an interrupted install, `ERR_PACKAGE_PATH_NOT_EXPORTED` from an `exports` map with no matching condition (which this package shipped once, see Publishing). Both run inside `importOptional`'s `catch`, where a throw replaces the import error the user needs, and `argo doctor` calls `detectInstallMode()` before anything else, so a throw there costs the whole table on exactly the broken tree the command exists to diagnose. `isDepInstalled` answers "not known to be absent" and `detectInstallMode` falls back to `project`. Both recover only on errors carrying a `code`: a bug in Argo surfaces as a bare `TypeError` and must still throw. +- `detectInstallMode()` distinguishes project / global / npx, because the correct install command differs. Global trees do not hoist, so Kokoro needs `npm i -g kokoro-js@1 @huggingface/transformers@3` in **one** command; two separate `npm i -g` runs produce two ONNX copies (~840 MB vs ~410 MB). Detection resolves `@argo-video/cli` from the cwd and compares identity with the running copy, which keeps pnpm, nested npm, and Yarn PnP working without path-string special cases. +- Surfaces that report engine availability: `argo doctor` (full table), `argo init` (hint when nothing is installed), and the adapters' runtime errors. Keep them consistent when adding an engine. + ## Publishing - Package: `@argo-video/cli` (npm org: `@argo-video`) diff --git a/README.md b/README.md index f53343b..f8fda89 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,15 @@ Write a demo script with Playwright. Add a scenes manifest. Run one command. Get ## Quick start ```bash -# Install +# Install the core (about 27 MB, no TTS engine yet) npm i -D @argo-video/cli +# Add the TTS engine you want. Engines are optional peer dependencies, +# so you only pay for the one you use. Run `npx argo doctor` any time to +# see which engines are installed and the exact command for your setup. +npm i kokoro-js@1 # local, free, no API key (~410 MB of ONNX runtime) +npm i openai # cloud, needs OPENAI_API_KEY (~20 MB) + # Initialize project npx argo init @@ -352,8 +358,8 @@ import { defineConfig, demosProject, engines } from '@argo-video/cli'; ## Requirements -- **Node.js** >= 18 -- **Playwright** >= 1.40 (peer dependency) +- **Node.js** >= 20 (Playwright requires it) +- **Playwright** >= 1.59 (peer dependency) - **ffmpeg** — system install required for export ```bash @@ -374,15 +380,45 @@ choco install ffmpeg # Windows }); ``` - | Engine | Type | Install | API Key | - |--------|------|---------|---------| - | `engines.kokoro()` | local | built-in | none | - | `engines.mlxAudio()` | local | `pip install mlx-audio` | none | - | `engines.openai()` | cloud | `npm i openai` | `OPENAI_API_KEY` | - | `engines.elevenlabs()` | cloud | `npm i @elevenlabs/elevenlabs-js` | `ELEVENLABS_API_KEY` | - | `engines.gemini()` | cloud | `npm i @google/genai` | `GEMINI_API_KEY` | - | `engines.sarvam()` | cloud | `npm i sarvamai` | `SARVAM_API_KEY` | - | `engines.transformers()` | local | built-in | none | + Every engine is an **optional peer dependency**: npm does not install it + for you, so the base package stays small. Install the one you use. + + | Engine | Type | Install | Size | API Key | + |--------|------|---------|------|---------| + | `engines.kokoro()` | local | `npm i kokoro-js@1` | ~410 MB | none | + | `engines.mlxAudio()` | local | `pip install mlx-audio` | n/a (Python) | none | + | `engines.openai()` | cloud | `npm i openai` | ~20 MB | `OPENAI_API_KEY` | + | `engines.elevenlabs()` | cloud | `npm i @elevenlabs/elevenlabs-js` | ~88 MB | `ELEVENLABS_API_KEY` | + | `engines.gemini()` | cloud | `npm i @google/genai` | ~36 MB | `GEMINI_API_KEY` | + | `engines.sarvam()` | cloud | `npm i sarvamai` | ~7 MB | `SARVAM_API_KEY` | + | `engines.transformers()` | local | `npm i @huggingface/transformers@3` | ~380 MB | none | + + Sizes are `node_modules` on disk for that package alone in an empty + project. They do not simply add up, because engines share transitive + dependencies with Argo. Measured end to end, a project install comes to + about 27 MB with no engine, 47 MB with OpenAI, and 435 MB with Kokoro. + + The commands above are for a project-local install. A **global** install + (`npm i -g @argo-video/cli`) needs `-g` on the engine too, and Kokoro + needs both packages in **one** command, because separate global installs + do not deduplicate and you end up with two copies of the ONNX runtime: + + ```bash + npm i -g kokoro-js@1 @huggingface/transformers@3 # one command, ~410 MB + ``` + + With **npx**, compose the engine into the same invocation: + + ```bash + npx -p @argo-video/cli -p openai -- argo pipeline example + ``` + + `npx argo doctor` prints the right command for whichever of the three + you are using. + + Word-level transcription (`tts.transcribe`) needs + `@huggingface/transformers`. Installing `kokoro-js` already brings it in + on a project install, so there is usually nothing extra to do. **Transformers.js** — Use any HuggingFace `text-to-speech` model locally. Supertonic, or any future ONNX TTS model: diff --git a/package-lock.json b/package-lock.json index 5698b3d..b221b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,39 +9,65 @@ "version": "0.38.2", "license": "MIT", "dependencies": { - "@huggingface/transformers": "^4.2.0", "commander": "^12.0.0", - "gsap": "^3.15.0", - "kokoro-js": "^1.2.1" + "gsap": "^3.15.0" }, "bin": { "argo": "bin/argo.js" }, "devDependencies": { + "@elevenlabs/elevenlabs-js": "^2.63.0", + "@google/genai": "^1.52.0", + "@huggingface/transformers": "^3.5.1", "@playwright/test": "^1.59.1", "@types/node": "^25.5.0", + "kokoro-js": "^1.2.1", + "openai": "^4.104.0", "playwright": "^1.59.1", "sarvamai": "^1.1.8", "typescript": "^5.5.0", "vitest": "^3.2.6" }, - "optionalDependencies": { - "@elevenlabs/elevenlabs-js": "^2.0.0", - "@google/genai": "^1.0.0", - "openai": "^4.0.0", - "sarvamai": "^1.0.0" + "engines": { + "node": ">=20" }, "peerDependencies": { + "@elevenlabs/elevenlabs-js": ">=2", + "@google/genai": ">=1", + "@huggingface/transformers": "^3.5.1 || ^4.2.0", "@playwright/test": ">=1.59.0", - "playwright": ">=1.59.0" + "kokoro-js": "^1.2.1", + "openai": ">=4", + "playwright": ">=1.59.0", + "sarvamai": ">=1" + }, + "peerDependenciesMeta": { + "@elevenlabs/elevenlabs-js": { + "optional": true + }, + "@google/genai": { + "optional": true + }, + "@huggingface/transformers": { + "optional": true + }, + "kokoro-js": { + "optional": true + }, + "openai": { + "optional": true + }, + "sarvamai": { + "optional": true + } } }, "node_modules/@elevenlabs/elevenlabs-js": { - "version": "2.39.0", - "resolved": "https://registry.npmjs.org/@elevenlabs/elevenlabs-js/-/elevenlabs-js-2.39.0.tgz", - "integrity": "sha512-Yfh2wa5Y7wwoEHi2Na1YWrkc3z21oeHMo8qhGc5kcbQoWfgQfIxWuGfXHZSmUXOJz0mNL5bAfp3hHaJEX68DIA==", + "version": "2.64.0", + "resolved": "https://registry.npmjs.org/@elevenlabs/elevenlabs-js/-/elevenlabs-js-2.64.0.tgz", + "integrity": "sha512-g1o6Bs0tsjZYFWSRsG2cND2b4DMhTHvWS+e0SozCtXqUVY+KQa039eas0P7BxLVUN940+IJ+SDY2fJx2pUo39g==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "command-exists": "^1.2.9", "node-fetch": "^2.7.0", @@ -55,6 +81,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -504,11 +531,12 @@ } }, "node_modules/@google/genai": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.45.0.tgz", - "integrity": "sha512-+sNRWhKiRibVgc4OKi7aBJJ0A7RcoVD8tGG+eFkqxAWRjASDW+ktS9lLwTDnAxZICzCVoeAdu8dYLJVTX60N9w==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", @@ -531,34 +559,30 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/@huggingface/tokenizers": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", - "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", - "license": "Apache-2.0" - }, "node_modules/@huggingface/transformers": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", - "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@huggingface/jinja": "^0.5.6", - "@huggingface/tokenizers": "^0.1.3", - "onnxruntime-node": "1.24.3", - "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", - "sharp": "^0.34.5" + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" } }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -571,6 +595,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -593,6 +618,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -615,6 +641,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -631,6 +658,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -647,6 +675,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -663,6 +692,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -679,6 +709,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -695,6 +726,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -711,6 +743,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -727,6 +760,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -743,6 +777,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -759,6 +794,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -775,6 +811,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -797,6 +834,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -819,6 +857,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -841,6 +880,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -863,6 +903,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -885,6 +926,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -907,6 +949,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -929,6 +972,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -951,6 +995,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -970,6 +1015,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -989,6 +1035,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1008,6 +1055,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1020,6 +1068,19 @@ "url": "https://opencollective.com/libvips" } }, + "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/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1047,30 +1108,35 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1" @@ -1080,24 +1146,28 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1479,6 +1549,7 @@ "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -1488,8 +1559,8 @@ "version": "2.6.13", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -1499,8 +1570,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/@vitest/expect": { "version": "3.2.7", @@ -1621,8 +1692,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -1630,21 +1701,12 @@ "node": ">=6.5" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "license": "MIT", - "engines": { - "node": ">=12.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", - "optional": true, "engines": { "node": ">= 14" } @@ -1653,8 +1715,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "humanize-ms": "^1.2.1" }, @@ -1676,13 +1738,14 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "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", @@ -1697,15 +1760,14 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": "*" } @@ -1715,14 +1777,15 @@ "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" }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause", - "optional": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/cac": { "version": "6.7.14", @@ -1738,8 +1801,8 @@ "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", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1775,12 +1838,22 @@ "node": ">= 16" } }, + "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/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", - "optional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1792,8 +1865,8 @@ "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "12.1.0", @@ -1808,8 +1881,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 12" } @@ -1818,7 +1891,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1846,6 +1919,7 @@ "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", "dependencies": { "es-define-property": "^1.0.0", @@ -1863,6 +1937,7 @@ "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", "dependencies": { "define-data-property": "^1.0.1", @@ -1880,8 +1955,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.4.0" } @@ -1890,6 +1965,7 @@ "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" @@ -1899,14 +1975,15 @@ "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" }, "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", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -1920,8 +1997,8 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -1930,6 +2007,7 @@ "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" @@ -1939,6 +2017,7 @@ "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" @@ -1955,8 +2034,8 @@ "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", - "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -1968,8 +2047,8 @@ "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", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -1984,6 +2063,7 @@ "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" }, "node_modules/esbuild": { @@ -2032,6 +2112,7 @@ "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", "engines": { "node": ">=10" @@ -2054,8 +2135,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -2074,8 +2155,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", @@ -2099,6 +2180,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, "funding": [ { "type": "github", @@ -2110,7 +2192,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -2123,14 +2204,15 @@ "version": "25.9.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2146,8 +2228,8 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -2174,8 +2256,8 @@ "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", - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2184,8 +2266,8 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -2199,8 +2281,8 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -2218,8 +2300,8 @@ "version": "8.1.2", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -2233,8 +2315,8 @@ "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", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -2258,8 +2340,8 @@ "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", - "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -2272,6 +2354,7 @@ "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", "dependencies": { "boolean": "^3.0.1", @@ -2289,6 +2372,7 @@ "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", "dependencies": { "define-properties": "^1.2.1", @@ -2305,8 +2389,8 @@ "version": "10.6.2", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -2323,8 +2407,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=14" } @@ -2333,6 +2417,7 @@ "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" @@ -2351,12 +2436,14 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "dev": true, "license": "ISC" }, "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", "dependencies": { "es-define-property": "^1.0.0" @@ -2369,8 +2456,8 @@ "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", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -2382,8 +2469,8 @@ "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", - "optional": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -2398,8 +2485,8 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2411,8 +2498,8 @@ "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", - "optional": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -2425,8 +2512,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "ms": "^2.0.0" } @@ -2442,8 +2529,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -2452,14 +2539,15 @@ "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" }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -2470,8 +2558,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2481,6 +2569,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.2.1.tgz", "integrity": "sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@huggingface/transformers": "^3.5.1", @@ -2491,6 +2580,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/loupe": { @@ -2514,6 +2604,7 @@ "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", "dependencies": { "escape-string-regexp": "^4.0.0" @@ -2526,8 +2617,8 @@ "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", - "optional": true, "engines": { "node": ">= 0.4" } @@ -2536,8 +2627,8 @@ "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", - "optional": true, "engines": { "node": ">= 0.6" } @@ -2546,8 +2637,8 @@ "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", - "optional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -2555,11 +2646,34 @@ "node": ">= 0.6" } }, + "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/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/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2586,6 +2700,7 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", + "dev": true, "funding": [ { "type": "github", @@ -2597,7 +2712,6 @@ } ], "license": "MIT", - "optional": true, "engines": { "node": ">=10.5.0" } @@ -2606,8 +2720,8 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2627,21 +2741,24 @@ "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", "engines": { "node": ">= 0.4" } }, "node_modules/onnxruntime-common": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", - "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "dev": true, "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", - "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "os": [ @@ -2650,37 +2767,39 @@ "linux" ], "dependencies": { - "adm-zip": "^0.5.16", "global-agent": "^3.0.0", - "onnxruntime-common": "1.24.3" + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" } }, "node_modules/onnxruntime-web": { - "version": "1.26.0-dev.20260416-b7804b056c", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", - "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "dev": true, "license": "MIT", "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", - "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.24.0-dev.20251116-b39e144322", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", - "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "dev": true, "license": "MIT" }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -2710,8 +2829,8 @@ "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2720,15 +2839,15 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/openai/node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" @@ -2741,15 +2860,15 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/openai/node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 14" } @@ -2758,8 +2877,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -2789,6 +2908,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz", "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==", + "dev": true, "license": "Apache-2.0" }, "node_modules/picocolors": { @@ -2815,6 +2935,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "dev": true, "license": "MIT" }, "node_modules/playwright": { @@ -2882,6 +3003,7 @@ "version": "7.6.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -2905,8 +3027,8 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 4" } @@ -2915,6 +3037,7 @@ "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", "dependencies": { "boolean": "^3.0.1", @@ -2977,6 +3100,7 @@ "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", @@ -2991,8 +3115,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/sarvamai": { "version": "1.1.8", @@ -3010,6 +3133,7 @@ "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" @@ -3022,12 +3146,14 @@ "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" }, "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", "dependencies": { "type-fest": "^0.13.1" @@ -3043,6 +3169,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3104,6 +3231,7 @@ "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" }, "node_modules/stackback": { @@ -3133,6 +3261,23 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "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/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3198,13 +3343,14 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true + "dev": true, + "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD", "optional": true }, @@ -3212,6 +3358,7 @@ "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)", "engines": { "node": ">=10" @@ -3238,6 +3385,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -3430,8 +3578,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 8" } @@ -3440,15 +3588,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -3475,7 +3623,7 @@ "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3492,6 +3640,16 @@ "optional": true } } + }, + "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" + } } } } diff --git a/package.json b/package.json index 4240055..21a96a7 100644 --- a/package.json +++ b/package.json @@ -22,31 +22,26 @@ "test:watch": "vitest" }, "peerDependencies": { + "@elevenlabs/elevenlabs-js": ">=2", + "@google/genai": ">=1", + "@huggingface/transformers": "^3.5.1 || ^4.2.0", "@playwright/test": ">=1.59.0", - "playwright": ">=1.59.0" + "kokoro-js": "^1.2.1", + "openai": ">=4", + "playwright": ">=1.59.0", + "sarvamai": ">=1" }, "dependencies": { - "@huggingface/transformers": "^4.2.0", "commander": "^12.0.0", - "gsap": "^3.15.0", - "kokoro-js": "^1.2.1" + "gsap": "^3.15.0" }, "overrides": { - "kokoro-js": { - "@huggingface/transformers": "$@huggingface/transformers" - }, "ws": "^8.21.0", "form-data": "^4.0.6", "vite": "^7.3.5", "esbuild": "^0.28.1", "protobufjs": "^7.6.3" }, - "optionalDependencies": { - "@elevenlabs/elevenlabs-js": "^2.0.0", - "@google/genai": "^1.0.0", - "openai": "^4.0.0", - "sarvamai": "^1.0.0" - }, "publishConfig": { "access": "public" }, @@ -61,11 +56,39 @@ "scripts" ], "devDependencies": { + "@elevenlabs/elevenlabs-js": "^2.63.0", + "@google/genai": "^1.52.0", + "@huggingface/transformers": "^3.5.1", "@playwright/test": "^1.59.1", "@types/node": "^25.5.0", + "kokoro-js": "^1.2.1", + "openai": "^4.104.0", "playwright": "^1.59.1", "sarvamai": "^1.1.8", "typescript": "^5.5.0", "vitest": "^3.2.6" + }, + "peerDependenciesMeta": { + "@elevenlabs/elevenlabs-js": { + "optional": true + }, + "@google/genai": { + "optional": true + }, + "@huggingface/transformers": { + "optional": true + }, + "kokoro-js": { + "optional": true + }, + "openai": { + "optional": true + }, + "sarvamai": { + "optional": true + } + }, + "engines": { + "node": ">=20" } } diff --git a/skills/argo-guide/SKILL.md b/skills/argo-guide/SKILL.md index 5bea2f3..dcbd7d0 100644 --- a/skills/argo-guide/SKILL.md +++ b/skills/argo-guide/SKILL.md @@ -16,6 +16,8 @@ Argo turns Playwright demo scripts into polished product demo videos with AI voi ## Prerequisites +**No TTS engine ships by default.** Every engine is an optional peer dependency. Install one before running a pipeline: `npm i kokoro-js@1` (local, no API key) or `npm i openai` (cloud, needs `OPENAI_API_KEY`). Run `npx argo doctor` to see which engines are present and the exact install command for this project. + 1. **`@argo-video/cli`** in `devDependencies` — install with `npm i -D @argo-video/cli` if missing 2. **`argo.config.mjs`** in project root — scaffold with `npx argo init` if missing (use `.mjs` to avoid ESM warnings) 3. **Playwright browsers** — `npx playwright install chromium` (or `webkit` for best macOS quality) diff --git a/skills/argo-guide/references/tts-engines.md b/skills/argo-guide/references/tts-engines.md index 8faafe7..d962026 100644 --- a/skills/argo-guide/references/tts-engines.md +++ b/skills/argo-guide/references/tts-engines.md @@ -15,15 +15,24 @@ export default defineConfig({ }); ``` +Every engine is an optional peer dependency. npm does not install any of +them automatically, so a fresh `@argo-video/cli` has no TTS engine until one +is added. `npx argo doctor` lists which are present. + | Engine | Type | Install | Voices | |--------|------|---------|--------| -| `engines.kokoro()` | local | built-in | `af_heart`, `am_michael` | +| `engines.kokoro()` | local | `npm i kokoro-js@1` | `af_heart`, `am_michael` | | `engines.openai()` | cloud | `npm i openai` | `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer` | | `engines.elevenlabs()` | cloud | `npm i @elevenlabs/elevenlabs-js` | ElevenLabs voice IDs | | `engines.gemini()` | cloud | `npm i @google/genai` | Gemini voice names | | `engines.sarvam()` | cloud | `npm i sarvamai` | `meera` + Indian language voices | | `engines.mlxAudio()` | local | `pip install mlx-audio` | model-dependent (Apple Silicon only) | -| `engines.transformers()` | local | built-in | any HuggingFace `text-to-speech` model | +| `engines.transformers()` | local | `npm i @huggingface/transformers@3` | any HuggingFace `text-to-speech` model | + +Those commands assume a project-local Argo. For a global install add `-g`, +and install Kokoro's two packages in one command so npm deduplicates the +ONNX runtime: `npm i -g kokoro-js@1 @huggingface/transformers@3`. For npx, +compose them: `npx -p @argo-video/cli -p openai -- argo pipeline `. Cloud engines read API keys from environment variables (`OPENAI_API_KEY`, `ELEVENLABS_API_KEY`, `GEMINI_API_KEY`, `SARVAM_API_KEY`) or accept `apiKey` in factory options. diff --git a/src/cli.ts b/src/cli.ts index 1bfbfd9..4eed981 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -40,6 +40,14 @@ function validateDemoName(name: string): string { return name; } +/** Default to Kokoro when the config names no engine. + * + * Kokoro is an optional peer dependency, but do not probe for it here. + * Constructing the engine loads nothing, and TTS is the first step of the + * pipeline, so `importOptional` raises the same install hint at the first + * `generate()` a moment later. Probing up front instead breaks the demos + * that need no engine at all: silent demos carry no `text`, so + * `generateClips` filters every scene out and never calls `generate()`. */ async function ensureTTSEngine(config: ArgoConfig): Promise { if (!config.tts.engine) { const { KokoroEngine } = await import('./tts/kokoro.js'); @@ -555,5 +563,12 @@ export function createProgram(): Command { } if (process.env.VITEST === undefined) { - createProgram().parseAsync(process.argv).catch((err) => { console.error(err.message); process.exit(1); }); + createProgram().parseAsync(process.argv).catch((err) => { + console.error(err.message); + // Optional-dependency errors wrap the original resolution failure. Without + // this, the underlying cause is unreachable from every CLI path. + if (err?.cause instanceof Error) console.error(` caused by: ${err.cause.message}`); + if (process.env.DEBUG) console.error(err.stack); + process.exit(1); + }); } diff --git a/src/doctor.ts b/src/doctor.ts index 16e6d9e..9830ae1 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -1,6 +1,19 @@ import { execFileSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { loadConfig } from './config.js'; +import { + detectInstallMode, + installCommand, + isDepInstalled, + KOKORO_DEP, + OPENAI_DEP, + ELEVENLABS_DEP, + GEMINI_DEP, + SARVAM_DEP, + TRANSFORMERS_DEP, + WHISPER_DEP, + type OptionalDepSpec, +} from './optional-deps.js'; interface CheckResult { name: string; @@ -103,6 +116,57 @@ export async function runDoctor(cwd: string = process.cwd()): Promise = [ + ['kokoro', KOKORO_DEP, true], + ['openai', OPENAI_DEP, true], + ['elevenlabs', ELEVENLABS_DEP, true], + ['gemini', GEMINI_DEP, true], + ['sarvam', SARVAM_DEP, true], + ['transformers', TRANSFORMERS_DEP, true], + ['transcribe', WHISPER_DEP, false], + ]; + + const results: CheckResult[] = [ + { name: 'install mode', status: 'ok', message: mode }, + ]; + let anyEngine = false; + + for (const [name, spec, isVoiceEngine] of specs) { + if (isDepInstalled(spec)) { + if (isVoiceEngine) anyEngine = true; + results.push({ name: `engine/${name}`, status: 'ok', message: 'installed' }); + } else { + results.push({ + name: `engine/${name}`, + status: 'warn', + message: `not installed. ${installCommand(spec, mode)}`, + }); + } + } + + if (!anyEngine) { + results.push({ + name: 'engines', + status: 'warn', + message: `No npm TTS engine installed. ${installCommand(KOKORO_DEP, mode)} (or use engines.mlxAudio)`, + }); + } + return results; } diff --git a/src/init.ts b/src/init.ts index 3eb0fb4..c6a72b9 100644 --- a/src/init.ts +++ b/src/init.ts @@ -5,6 +5,16 @@ import { generateDemoScript, generateScenesSkeleton, } from './parse-playwright.js'; +import { + installCommand, + isDepInstalled, + KOKORO_DEP, + OPENAI_DEP, + ELEVENLABS_DEP, + GEMINI_DEP, + SARVAM_DEP, + TRANSFORMERS_DEP, +} from './optional-deps.js'; async function writeIfMissing(filePath: string, content: string): Promise { try { @@ -147,6 +157,22 @@ export async function init(cwd: string = process.cwd()): Promise { console.log(' 1. Edit demos/example.demo.ts'); console.log(' 2. Run: npx argo record example'); console.log(' 3. Run: npx argo pipeline example'); + printEngineHintIfMissing(); +} + +/** Scaffolding is the first place a new user can discover that no TTS engine + * is installed. Say so here rather than letting the first `pipeline` run + * fail on it. Silent when an engine is already present. */ +function printEngineHintIfMissing(): void { + const specs = [KOKORO_DEP, OPENAI_DEP, ELEVENLABS_DEP, GEMINI_DEP, SARVAM_DEP, TRANSFORMERS_DEP]; + if (specs.some(isDepInstalled)) return; + // Always the project-mode command: `init` has just scaffolded a project + // here, so that is where the engine belongs even when Argo itself was + // invoked globally or through npx. + console.log('\nNo TTS engine is installed yet. Pick one:'); + console.log(` local, no API key: ${installCommand(KOKORO_DEP, 'project')}`); + console.log(` cloud, needs a key: ${installCommand(OPENAI_DEP, 'project')}`); + console.log(' see all engines: npx argo doctor'); } export interface InitFromOptions { diff --git a/src/music/musicgen.ts b/src/music/musicgen.ts index 7f35678..148f585 100644 --- a/src/music/musicgen.ts +++ b/src/music/musicgen.ts @@ -6,6 +6,7 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; +import { importOptional, MUSICGEN_DEP } from '../optional-deps.js'; import { createWavBuffer } from '../tts/engine.js'; export interface MusicGenOptions { @@ -76,18 +77,10 @@ export async function generateMusic(options: MusicGenOptions): Promise { const maxNewTokens = Math.ceil(durationSec * TOKENS_PER_SECOND); // Lazy-load transformers (same pattern as TTS Transformers engine) - let AutoTokenizer: any; - let MusicgenForConditionalGeneration: any; - try { - ({ AutoTokenizer, MusicgenForConditionalGeneration } = await import( - '@huggingface/transformers' - )); - } catch { - throw new Error( - "MusicGen requires the '@huggingface/transformers' package. " + - 'Install it with: npm i @huggingface/transformers', - ); - } + const { AutoTokenizer, MusicgenForConditionalGeneration }: any = await importOptional( + () => import('@huggingface/transformers'), + MUSICGEN_DEP, + ); console.log(` \u25b8 Loading model: ${MODEL_ID}`); console.log( diff --git a/src/optional-deps.ts b/src/optional-deps.ts new file mode 100644 index 0000000..bd8f522 --- /dev/null +++ b/src/optional-deps.ts @@ -0,0 +1,270 @@ +/** + * Optional engine dependencies: detection and actionable install hints. + * + * Argo's TTS engines and the Whisper transcriber are optional peer + * dependencies. npm does not install them automatically, so the package + * stays small for anyone who only uses one engine. When a code path needs + * a package that isn't there, the error has to say exactly what to run. + * + * The correct command differs by how Argo itself was installed: + * + * - project (`npm i -D @argo-video/cli`): npm hoists the engine's own + * transitive deps to the project root, where Argo can resolve them. + * - global (`npm i -g`): each global install is its own tree with no + * hoisting between them. `npm i -g kokoro-js` alone leaves + * `@huggingface/transformers` nested privately under `kokoro-js/`, where + * Argo cannot see it, so the global hint names both packages. They must + * go in ONE command: two separate `npm i -g` runs produce two copies of + * onnxruntime (~840MB vs ~410MB). + * - npx: the throwaway tree holds only what `--package` put in it. + */ +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { dirname, join, sep } from 'node:path'; + +export type InstallMode = 'project' | 'global' | 'npx'; + +const PKG_NAME = '@argo-video/cli'; + +/** `kokoro-js@1` -> `kokoro-js`. Install lists carry version ranges for the + * user to paste; resolution and error text want the bare name. + * + * Only the digit-led forms the specs actually use are recognised. A range + * written `@^1` would survive into the "bare" name and resolve to nothing, + * which is why the specs are tested for `^` and must not contain it. */ +function bareName(specifier: string): string { + return specifier.replace(/@[\d.].*$/, ''); +} + +/** Resolve a bare specifier as if from `dir`, or null if it does not resolve. + * + * Uses `createRequire` rather than `import.meta.resolve`: the latter is + * undefined on Node before 18.19/20.6 and under vite-node's SSR transform, + * where an optional-chained call silently succeeds and every probe answers + * "installed". `createRequire().resolve` exists everywhere and throws. */ +function resolveFrom(dir: string, specifier: string): string | null { + try { + return createRequire(pathToFileURL(join(dir, 'noop.js'))).resolve(specifier); + } catch (err) { + if (isModuleNotFound(err)) return null; + throw err; + } +} + +/** Packages to install for one optional capability, per install mode. + * `global` is a separate list because global trees do not hoist. */ +export interface OptionalDepSpec { + /** Human-readable capability name, used in the error message. */ + feature: string; + /** Install list for a project-local Argo. */ + project: string[]; + /** Install list for a globally installed Argo. */ + global: string[]; +} + +// The specs below pin a major, and write it `@3` rather than `@^3`. The two +// ranges are identical to npm (node-semver normalises both to +// `>=3.0.0 <4.0.0-0`), but `^` is a glob operator under zsh's `extendedglob`, +// where the pasted command dies with `no matches found` and never reaches npm. +// These strings exist to be pasted into a shell, so they avoid the character. +// The cloud SDKs further down need no pin: their peer ranges are open-ended, +// so whatever `latest` gives satisfies them. +// +// The pin itself is what keeps one ONNX runtime in the tree. `latest` is 4.x, +// so a bare `npm i @huggingface/transformers` next to an existing kokoro-js +// re-nests its 3.x copy underneath v4: ~765 MB against ~410 MB. A user who +// installs Kokoro and later enables `tts.transcribe` reaches that by following +// the hint printed here. Everything Argo asks of the package (`pipeline`, +// `AutoTokenizer`, `MusicgenForConditionalGeneration`, word-level Whisper +// timestamps) is verified on 3.8.1. Drop the pin once kokoro-js moves to v4. +export const KOKORO_DEP: OptionalDepSpec = { + feature: 'Kokoro local TTS', + // Pinned to the major the peer range allows: an unpinned `npm i kokoro-js` + // would resolve to a future v2 and fail ERESOLVE against `^1.2.1`. + project: ['kokoro-js@1'], + // kokoro-js keeps its own transformers copy private in a global tree. + global: ['kokoro-js@1', '@huggingface/transformers@3'], +}; + +export const TRANSFORMERS_DEP: OptionalDepSpec = { + feature: 'local Transformers.js models', + project: ['@huggingface/transformers@3'], + global: ['@huggingface/transformers@3'], +}; + +export const WHISPER_DEP: OptionalDepSpec = { + feature: 'Whisper word-level transcription (`tts.transcribe`)', + project: ['@huggingface/transformers@3'], + global: ['@huggingface/transformers@3'], +}; + +export const MUSICGEN_DEP: OptionalDepSpec = { + feature: 'MusicGen background music generation', + project: ['@huggingface/transformers@3'], + global: ['@huggingface/transformers@3'], +}; + +export const OPENAI_DEP: OptionalDepSpec = { + feature: 'OpenAI TTS', + project: ['openai'], + global: ['openai'], +}; + +export const ELEVENLABS_DEP: OptionalDepSpec = { + feature: 'ElevenLabs TTS', + project: ['@elevenlabs/elevenlabs-js'], + global: ['@elevenlabs/elevenlabs-js'], +}; + +export const GEMINI_DEP: OptionalDepSpec = { + feature: 'Gemini TTS', + project: ['@google/genai'], + global: ['@google/genai'], +}; + +export const SARVAM_DEP: OptionalDepSpec = { + feature: 'Sarvam TTS', + project: ['sarvamai'], + global: ['sarvamai'], +}; + +let cachedMode: InstallMode | null = null; + +/** Where is this copy of Argo installed from? Cached per process. + * + * npx unpacks into `~/.npm/_npx//`, which is unambiguous. Otherwise + * ask the real resolver: does the user's project resolve `@argo-video/cli` + * to the very copy now running? If so, `npm i ` lands somewhere + * this process can see it, which is what makes the hint correct. + * + * Delegating to the resolver rather than parsing the path keeps pnpm's + * `.pnpm//node_modules/...` store, npm's nested fallback layout, and + * Yarn PnP working without special cases. Comparing identity rather than + * mere resolvability also catches running a global Argo from inside a + * project that happens to have its own copy. */ +export function detectInstallMode(): InstallMode { + if (cachedMode) return cachedMode; + let selfDir: string; + try { + selfDir = dirname(fileURLToPath(import.meta.url)); + } catch { + // Bundled or otherwise non-file URL. Assume the common case. + cachedMode = 'project'; + return cachedMode; + } + if (selfDir.includes(`${sep}_npx${sep}`)) { + cachedMode = 'npx'; + return cachedMode; + } + try { + const selfEntry = resolveFrom(selfDir, PKG_NAME); + if (selfEntry === null) { + // Running from a source checkout, not from an installed tree. + cachedMode = 'project'; + return cachedMode; + } + cachedMode = resolveFrom(process.cwd(), PKG_NAME) === selfEntry ? 'project' : 'global'; + } catch (err) { + // Resolving Argo's own name can fail without Argo being absent: an + // `exports` map with no condition matching the caller throws + // `ERR_PACKAGE_PATH_NOT_EXPORTED`, which this package shipped once (see + // Publishing in CLAUDE.md). Falling back beats propagating, because + // `doctor` calls this before anything else and must still print its + // table, and `installCommand` runs inside `importOptional`'s catch, where + // a throw here would replace the import failure the user needs to see. + // + // Same boundary as `isDepInstalled`: only resolution failures recover, + // and Node tags every one of those with a `code`. A bug in Argo must not + // be absorbed into a confident answer about where it is installed. + if (typeof (err as { code?: unknown } | null)?.code !== 'string') throw err; + cachedMode = 'project'; + } + return cachedMode; +} + +/** Reset the cached mode. Tests only. */ +export function resetInstallModeCache(): void { + cachedMode = null; +} + +/** The exact command to install `spec` for the current install mode. */ +export function installCommand(spec: OptionalDepSpec, mode = detectInstallMode()): string { + switch (mode) { + case 'global': + // One command, not several: separate global installs do not dedupe. + return `npm i -g ${spec.global.join(' ')}`; + case 'npx': { + const pkgs = [PKG_NAME, ...spec.global].map((p) => `-p ${p}`).join(' '); + return `npx ${pkgs} -- argo `; + } + case 'project': + return `npm i ${spec.project.join(' ')}`; + } +} + +/** True when `err` is Node's "package is not installed" failure. + * + * Distinguishes a missing optional dependency from a real fault inside a + * dependency that *is* installed. Those must keep propagating unchanged. */ +export function isModuleNotFound(err: unknown): boolean { + const code = (err as { code?: string } | null)?.code; + return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND'; +} + +/** Error explaining which optional package is missing and how to install it. */ +export function missingDependencyError(spec: OptionalDepSpec, cause?: unknown): Error { + const names = spec.project.map(bareName); + const list = names.length === 1 ? `'${names[0]}'` : names.map((n) => `'${n}'`).join(' and '); + const err = new Error( + `${spec.feature} requires ${list}, which is an optional dependency and is not installed.\n` + + ` Install it with: ${installCommand(spec)}`, + ); + if (cause !== undefined) (err as Error & { cause?: unknown }).cause = cause; + return err; +} + +/** Dynamic-import wrapper that turns a missing optional package into an + * actionable error, and leaves every other failure untouched. + * + * A module-not-found is not enough on its own to blame the optional + * package: the same code surfaces when the package is present but one of + * *its* dependencies fails to load. `onnxruntime-node` does exactly that, + * requiring a per-arch native binding at runtime, so a machine without a + * matching prebuilt binary would otherwise be told to install `kokoro-js` + * when it already has it. Confirm the package really is absent first. */ +export async function importOptional(load: () => Promise, spec: OptionalDepSpec): Promise { + try { + return await load(); + } catch (err) { + if (isModuleNotFound(err) && !isDepInstalled(spec)) { + throw missingDependencyError(spec, err); + } + throw err; + } +} + +/** Is the optional package for `spec` resolvable right now? Used by + * `argo doctor` to report engine availability without loading anything. + * + * Only a genuine module-not-found means "absent". A corrupt manifest or a + * broken `exports` map throws a different code and must not be reported as + * "not installed", or the user reinstalls a package that is already there. + * + * Reading this as "not known to be absent" is what makes it total. A probe + * can fail on its own: an interrupted `npm i` leaves a truncated + * `package.json` (`ERR_INVALID_PACKAGE_CONFIG`) and an `exports` map with no + * matching condition throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. Neither may + * escape. `importOptional` calls this from inside a `catch`, where a throw + * would replace the real import failure, and `argo doctor` is the command a + * user runs precisely because their install is half-broken. */ +export function isDepInstalled(spec: OptionalDepSpec): boolean { + try { + return resolveFrom(dirname(fileURLToPath(import.meta.url)), bareName(spec.project[0])) !== null; + } catch (err) { + // Only resolution failures recover. Node tags every one of them with a + // `code`; a bug in Argo (a spec with an empty `project`, say) surfaces as + // a bare TypeError and must not be answered with "installed". + if (typeof (err as { code?: unknown } | null)?.code === 'string') return true; + throw err; + } +} diff --git a/src/tts/engines/elevenlabs.ts b/src/tts/engines/elevenlabs.ts index 2887b96..5d1600e 100644 --- a/src/tts/engines/elevenlabs.ts +++ b/src/tts/engines/elevenlabs.ts @@ -1,4 +1,5 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; +import { importOptional, ELEVENLABS_DEP } from '../../optional-deps.js'; export interface ElevenLabsEngineOptions { apiKey?: string; @@ -39,15 +40,13 @@ export class ElevenLabsEngine implements TTSEngine { async generate(text: string, options: TTSEngineOptions): Promise { if (!text?.trim()) throw new Error('TTS text must not be empty'); - let ElevenLabsClient: any; - try { - // @ts-ignore — @elevenlabs/elevenlabs-js is an optional dependency - ({ ElevenLabsClient } = await import('@elevenlabs/elevenlabs-js')); - } catch { - throw new Error( - "ElevenLabs TTS engine requires the '@elevenlabs/elevenlabs-js' package. Install it with: npm i @elevenlabs/elevenlabs-js" - ); - } + // Loosely typed on purpose: the `convert()` call below uses snake_case + // keys that the current SDK typings do not accept. Typing it would change + // request shape, which is a behavioural fix, not a packaging one. + const { ElevenLabsClient }: any = await importOptional( + () => import('@elevenlabs/elevenlabs-js'), + ELEVENLABS_DEP, + ); const client = new ElevenLabsClient({ apiKey: this.resolveApiKey() }); const audioStream = await client.textToSpeech.convert( diff --git a/src/tts/engines/gemini.ts b/src/tts/engines/gemini.ts index 05ae400..f3e7b2f 100644 --- a/src/tts/engines/gemini.ts +++ b/src/tts/engines/gemini.ts @@ -1,4 +1,5 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; +import { importOptional, GEMINI_DEP } from '../../optional-deps.js'; export interface GeminiEngineOptions { apiKey?: string; @@ -33,15 +34,10 @@ export class GeminiEngine implements TTSEngine { async generate(text: string, options: TTSEngineOptions): Promise { if (!text?.trim()) throw new Error('TTS text must not be empty'); - let GoogleGenAI: any; - try { - // @ts-ignore — @google/genai is an optional dependency - ({ GoogleGenAI } = await import('@google/genai')); - } catch { - throw new Error( - "Gemini TTS engine requires the '@google/genai' package. Install it with: npm i @google/genai" - ); - } + const { GoogleGenAI } = await importOptional( + () => import('@google/genai'), + GEMINI_DEP, + ); const ai = new GoogleGenAI({ apiKey: this.resolveApiKey() }); const response = await ai.models.generateContent({ diff --git a/src/tts/engines/kokoro.ts b/src/tts/engines/kokoro.ts index 161f0dd..71c46f6 100644 --- a/src/tts/engines/kokoro.ts +++ b/src/tts/engines/kokoro.ts @@ -1,5 +1,6 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; import { splitTextForTTS, concatSamples } from '../engine.js'; +import { importOptional, KOKORO_DEP } from '../../optional-deps.js'; export interface KokoroEngineOptions { modelId?: string; @@ -31,8 +32,16 @@ export class KokoroEngine implements TTSEngine { if (this.tts) return this.tts; if (!this.initPromise) { this.initPromise = (async () => { + // Resolve the package first so a missing optional dependency reports + // the install command instead of being reported as a failed download. + const { KokoroTTS } = await importOptional( + () => import('kokoro-js'), + KOKORO_DEP, + ).catch((err) => { + this.initPromise = null; + throw err; + }); try { - const { KokoroTTS } = await import('kokoro-js'); this.tts = await KokoroTTS.from_pretrained(this.modelId, { dtype: this.dtype as 'fp32' | 'fp16' | 'q8' | 'q4' | 'q4f16', device: this.device, diff --git a/src/tts/engines/openai.ts b/src/tts/engines/openai.ts index 1b48b0c..2b5d70f 100644 --- a/src/tts/engines/openai.ts +++ b/src/tts/engines/openai.ts @@ -1,4 +1,5 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; +import { importOptional, OPENAI_DEP } from '../../optional-deps.js'; export interface OpenAIEngineOptions { apiKey?: string; @@ -39,15 +40,10 @@ export class OpenAIEngine implements TTSEngine { async generate(text: string, options: TTSEngineOptions): Promise { if (!text?.trim()) throw new Error('TTS text must not be empty'); - let OpenAI: any; - try { - // @ts-ignore — openai is an optional dependency - ({ OpenAI } = await import('openai')); - } catch { - throw new Error( - "OpenAI TTS engine requires the 'openai' package. Install it with: npm i openai" - ); - } + const { OpenAI } = await importOptional( + () => import('openai'), + OPENAI_DEP, + ); const client = new OpenAI({ apiKey: this.resolveApiKey() }); diff --git a/src/tts/engines/sarvam.ts b/src/tts/engines/sarvam.ts index 1fd8219..7a6573a 100644 --- a/src/tts/engines/sarvam.ts +++ b/src/tts/engines/sarvam.ts @@ -1,4 +1,5 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; +import { importOptional, SARVAM_DEP } from '../../optional-deps.js'; export interface SarvamEngineOptions { apiKey?: string; @@ -36,15 +37,11 @@ export class SarvamEngine implements TTSEngine { // `SarvamAIClient` is the client class. The package has no default export // and its `SarvamAI` export is a namespace object, so destructuring either // yields undefined and fails at `new` with an unrelated-looking TypeError. - let SarvamAIClient: any; - try { - // @ts-ignore — sarvamai is an optional dependency - ({ SarvamAIClient } = await import('sarvamai')); - } catch { - throw new Error( - "Sarvam TTS engine requires the 'sarvamai' package. Install it with: npm i sarvamai" - ); - } + // Loosely typed on purpose: the SDK's own typings don't describe this shape. + const { SarvamAIClient }: any = await importOptional( + () => import('sarvamai'), + SARVAM_DEP, + ); // The package resolved but does not expose the client — a version skew or // a rename in a future major. Say so, rather than letting `new undefined()` diff --git a/src/tts/engines/transformers.ts b/src/tts/engines/transformers.ts index 3038bdf..37e524e 100644 --- a/src/tts/engines/transformers.ts +++ b/src/tts/engines/transformers.ts @@ -1,5 +1,6 @@ import type { TTSEngine, TTSEngineOptions, TTSEngineMetadata } from '../engine.js'; import { splitTextForTTS, concatSamples } from '../engine.js'; +import { importOptional, TRANSFORMERS_DEP } from '../../optional-deps.js'; export interface TransformersEngineOptions { /** Model ID from Hugging Face Hub. Default: 'onnx-community/Supertonic-TTS-ONNX' */ @@ -43,15 +44,15 @@ export class TransformersEngine implements TTSEngine { if (this.pipeline) return this.pipeline; if (!this.initPromise) { this.initPromise = (async () => { - let pipeline: any; - try { - ({ pipeline } = await import('@huggingface/transformers')); - } catch { - throw new Error( - "Transformers TTS engine requires the '@huggingface/transformers' package. " + - 'Install it with: npm i @huggingface/transformers', - ); - } + // `pipeline` stays loosely typed: dtype/device are user-supplied + // strings, and the SDK types them as closed literal unions. + const { pipeline }: any = await importOptional( + () => import('@huggingface/transformers'), + TRANSFORMERS_DEP, + ).catch((err) => { + this.initPromise = null; + throw err; + }); try { this.pipeline = await pipeline('text-to-speech', this.model, { dtype: this.dtype, diff --git a/src/tts/transcribe.ts b/src/tts/transcribe.ts index 5029b44..a7024e4 100644 --- a/src/tts/transcribe.ts +++ b/src/tts/transcribe.ts @@ -7,12 +7,18 @@ * the source of truth is the rendered audio itself — transcribe it * back with Whisper and read the per-word timestamps. * - * Uses `@huggingface/transformers` (already in tree for Kokoro). Whisper - * runs locally via ONNX, no cloud round-trip. The pipeline instance is - * cached per process so repeated calls reuse the loaded model. + * Uses `@huggingface/transformers`. Whisper runs locally via ONNX, no cloud + * round-trip. The pipeline instance is cached per process so repeated calls + * reuse the loaded model. + * + * The import is dynamic and lives inside `getTranscriber()` on purpose. + * `@huggingface/transformers` is an optional peer dependency that pulls + * ~400MB of ONNX runtime, and transcription is opt-in (`tts.transcribe`). + * A top-level import would make every Argo command, including `validate` + * and `export`, fail to even load when the package is absent. */ -import { pipeline } from '@huggingface/transformers'; import { spawnSync } from 'node:child_process'; +import { importOptional, WHISPER_DEP } from '../optional-deps.js'; export interface WordTiming { /** The word as Whisper transcribed it. Whisper emits leading spaces on @@ -53,6 +59,10 @@ let cached: { model: string; transcriber: Transcriber } | null = null; * Switching model busts the cache (rare). */ async function getTranscriber(model: string): Promise { if (cached?.model === model) return cached.transcriber; + const { pipeline } = await importOptional( + () => import('@huggingface/transformers'), + WHISPER_DEP, + ); const transcriber = (await pipeline('automatic-speech-recognition', model)) as unknown as Transcriber; cached = { model, transcriber }; return transcriber; diff --git a/tests/optional-deps.test.ts b/tests/optional-deps.test.ts new file mode 100644 index 0000000..dd3570d --- /dev/null +++ b/tests/optional-deps.test.ts @@ -0,0 +1,254 @@ +import { describe, it, expect, beforeEach, beforeAll, afterAll, vi } from 'vitest'; +import { mkdirSync, readdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { + installCommand, + isModuleNotFound, + missingDependencyError, + importOptional, + isDepInstalled, + detectInstallMode, + resetInstallModeCache, + KOKORO_DEP, + TRANSFORMERS_DEP, + WHISPER_DEP, + MUSICGEN_DEP, + OPENAI_DEP, + ELEVENLABS_DEP, + GEMINI_DEP, + SARVAM_DEP, + type OptionalDepSpec, + type InstallMode, +} from '../src/optional-deps.js'; + +/** `src/`, where `isDepInstalled` resolves from. A fixture placed under + * `src/node_modules` is therefore the first thing its probe finds. */ +const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'src'); + +/** Every spec Argo ships. Add new engines here so the shell-safety check + * below covers them. */ +const ALL_SPECS: OptionalDepSpec[] = [ + KOKORO_DEP, + TRANSFORMERS_DEP, + WHISPER_DEP, + MUSICGEN_DEP, + OPENAI_DEP, + ELEVENLABS_DEP, + GEMINI_DEP, + SARVAM_DEP, +]; + +/** A spec pointing at a package that is deliberately not installed. */ +const MISSING_DEP: OptionalDepSpec = { + feature: 'a package that does not exist', + project: ['argo-no-such-package-xyz'], + global: ['argo-no-such-package-xyz'], +}; + +describe('installCommand', () => { + const cases: Array<[name: string, spec: OptionalDepSpec, mode: InstallMode, expected: string]> = [ + ['project mode installs plainly', OPENAI_DEP, 'project', 'npm i openai'], + ['global mode adds -g', OPENAI_DEP, 'global', 'npm i -g openai'], + // Kokoro names both packages in ONE global command: two separate + // `npm i -g` runs do not dedupe and leave a second copy of onnxruntime + // (~840MB vs ~410MB). In project mode npm hoists it, so one name is enough. + [ + 'global Kokoro names both packages in one command', + KOKORO_DEP, + 'global', + 'npm i -g kokoro-js@1 @huggingface/transformers@3', + ], + ['project Kokoro omits the hoisted transformers', KOKORO_DEP, 'project', 'npm i kokoro-js@1'], + [ + 'transformers specs pin the major kokoro-js shares', + WHISPER_DEP, + 'project', + 'npm i @huggingface/transformers@3', + ], + [ + 'npx composes packages onto the invocation', + OPENAI_DEP, + 'npx', + 'npx -p @argo-video/cli -p openai -- argo ', + ], + ]; + + it.each(cases)('%s', (_name, spec, mode, expected) => { + expect(installCommand(spec, mode)).toBe(expected); + }); +}); + +describe('isModuleNotFound', () => { + it('recognises both ESM and CJS resolution failures', () => { + expect(isModuleNotFound({ code: 'ERR_MODULE_NOT_FOUND' })).toBe(true); + expect(isModuleNotFound({ code: 'MODULE_NOT_FOUND' })).toBe(true); + }); + + it('does not claim unrelated errors', () => { + expect(isModuleNotFound(new Error('boom'))).toBe(false); + expect(isModuleNotFound({ code: 'ECONNREFUSED' })).toBe(false); + expect(isModuleNotFound(null)).toBe(false); + expect(isModuleNotFound(undefined)).toBe(false); + }); +}); + +describe('missingDependencyError', () => { + it('names the feature, the package, and the command', () => { + const err = missingDependencyError(WHISPER_DEP); + expect(err.message).toContain('Whisper word-level transcription'); + expect(err.message).toContain("'@huggingface/transformers'"); + expect(err.message).toContain('Install it with:'); + }); + + it('strips version ranges from the package name it reports', () => { + const spec: OptionalDepSpec = { + feature: 'test', + project: ['some-pkg@3'], + global: ['some-pkg@3'], + }; + expect(missingDependencyError(spec).message).toContain("'some-pkg'"); + expect(missingDependencyError(spec).message).not.toContain("'some-pkg@3'"); + }); +}); + +describe('install commands are safe to paste', () => { + // `^` is a glob operator under zsh's `extendedglob`, where a pasted + // `npm i pkg@^3` dies with `no matches found` before npm ever runs. It also + // defeats `bareName`, which would leave `pkg@^3` as the name to resolve and + // report a present package as missing. Write ranges as `@3`. + it.each(ALL_SPECS)('$feature avoids shell metacharacters', (spec) => { + for (const entry of [...spec.project, ...spec.global]) { + expect(entry).not.toContain('^'); + } + }); +}); + +describe('importOptional', () => { + it('returns the module when the import succeeds', async () => { + const mod = await importOptional(async () => ({ ok: 1 }), OPENAI_DEP); + expect(mod).toEqual({ ok: 1 }); + }); + + it('converts a missing package into an actionable error', async () => { + const notFound = Object.assign(new Error('nope'), { code: 'ERR_MODULE_NOT_FOUND' }); + await expect( + importOptional(() => Promise.reject(notFound), MISSING_DEP), + ).rejects.toThrow(/optional dependency and is not installed/); + }); + + it('lets a genuine fault inside an installed package propagate unchanged', async () => { + // A package that IS installed but throws on load must not be reported as + // missing, or the user is told to install something they already have. + const boom = new Error('the package itself is broken'); + await expect( + importOptional(() => Promise.reject(boom), OPENAI_DEP), + ).rejects.toThrow('the package itself is broken'); + }); + + it('does not blame the package when a transitive dep is what went missing', async () => { + // onnxruntime-node require()s a per-arch native binding at runtime. On a + // machine without a matching prebuilt, that surfaces as MODULE_NOT_FOUND + // from three levels down. Telling the user to install openai, which they + // demonstrably have, sends them in circles. + const transitive = Object.assign( + new Error("Cannot find module './bin/napi-v3/linux/arm64/binding.node'"), + { code: 'MODULE_NOT_FOUND' }, + ); + await expect( + importOptional(() => Promise.reject(transitive), OPENAI_DEP), + ).rejects.toThrow(/binding\.node/); + }); + + it('preserves the original error as `cause`', async () => { + const notFound = Object.assign(new Error('nope'), { code: 'ERR_MODULE_NOT_FOUND' }); + const err = await importOptional(() => Promise.reject(notFound), MISSING_DEP).catch( + (e: Error & { cause?: unknown }) => e, + ); + expect(err.cause).toBe(notFound); + }); +}); + +describe('isDepInstalled', () => { + it('finds a package that is present', () => { + // openai is a devDependency of this repo, so it resolves during tests. + expect(isDepInstalled(OPENAI_DEP)).toBe(true); + }); + + it('reports a package that is absent', () => { + expect(isDepInstalled(MISSING_DEP)).toBe(false); + }); + + it('ignores a version range attached to the specifier', () => { + expect(isDepInstalled({ ...OPENAI_DEP, project: ['openai@4'] })).toBe(true); + }); + + it('still throws when the failure is a bug rather than a resolution', () => { + // An empty `project` makes `bareName` throw a bare TypeError. Recovering + // from that would hide the mistake behind a confident "installed". + expect(() => isDepInstalled({ feature: 'malformed', project: [], global: [] })).toThrow( + TypeError, + ); + }); + + describe('when the probe itself fails', () => { + // A package can be present and still refuse to resolve: an `exports` map + // with no condition matching the caller throws + // `ERR_PACKAGE_PATH_NOT_EXPORTED`, which is what an interrupted or + // mis-published install looks like. Answering "not installed" there would + // tell the user to reinstall something they already have, and throwing + // would take down `argo doctor`, the command they ran to diagnose it. + const FIXTURE = join(SRC_DIR, 'node_modules', 'argo-broken-exports-fixture'); + + beforeAll(() => { + mkdirSync(FIXTURE, { recursive: true }); + writeFileSync( + join(FIXTURE, 'package.json'), + JSON.stringify({ name: 'argo-broken-exports-fixture', exports: { './sub': './sub.js' } }), + ); + }); + // Remove only what was created. `src/node_modules` is covered by the + // repo's `node_modules/` gitignore, so deleting a copy this test did not + // make would be both silent and unrecoverable. + afterAll(() => { + rmSync(FIXTURE, { recursive: true, force: true }); + const parent = join(SRC_DIR, 'node_modules'); + if (readdirSync(parent).length === 0) rmdirSync(parent); + }); + + it('answers "not known to be absent" rather than throwing', () => { + const spec: OptionalDepSpec = { + feature: 'fixture', + project: ['argo-broken-exports-fixture'], + global: ['argo-broken-exports-fixture'], + }; + expect(isDepInstalled(spec)).toBe(true); + }); + }); +}); + +describe('detectInstallMode', () => { + beforeEach(() => resetInstallModeCache()); + + it('reports project mode when running from the source checkout', () => { + // Both probes resolve `@argo-video/cli` to this repo's own `dist/`, via + // Node's self-reference (the root manifest has both `name` and `exports`), + // so they match and the mode is project. Not the `selfEntry === null` + // branch, which a source checkout cannot reach for the same reason. + expect(detectInstallMode()).toBe('project'); + }); + + // `detectInstallMode`'s own resolver-failure fallback has no test here: the + // self-reference above always wins for Argo's own name, so no fixture under + // `src/node_modules` can shadow it, and reproducing it needs a real + // installed consumer tree with a broken `exports` map. Verified by hand + // there instead: `argo doctor` prints its table rather than exiting 1. + + it('caches the result instead of recomputing', () => { + const first = detectInstallMode(); + const spy = vi.spyOn(process, 'cwd'); + expect(detectInstallMode()).toBe(first); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); +}); diff --git a/tests/optional-imports.test.ts b/tests/optional-imports.test.ts new file mode 100644 index 0000000..51124b4 --- /dev/null +++ b/tests/optional-imports.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, relative } from 'node:path'; + +const SRC = join(dirname(fileURLToPath(import.meta.url)), '..', 'src'); + +/** Every `.ts` under `dir`, recursively. + * + * Hand-rolled rather than `fs.globSync` (Node 22+) or `readdirSync`'s + * `recursive` option (Node 18.17/20.1+): this file is the only guard against + * an optional peer regressing to a static import, so it must not be the one + * thing in the suite that needs a newer Node than the package supports. On + * Node 20 `globSync` is undefined and the file died at collection, taking all + * of its assertions with it while CI stayed green on 24. */ +function sourceFiles(dir: string): string[] { + const found: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) found.push(...sourceFiles(full)); + else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) found.push(full); + } + return found; +} + +/** Packages declared as optional peers in package.json. None may be loaded at + * module scope, or the whole install-footprint design collapses: a static + * import fails at link time and a top-level `require` at evaluation, either + * of which takes down every command, including ones that never touch TTS. */ +const OPTIONAL_PACKAGES = [ + 'kokoro-js', + '@huggingface/transformers', + 'openai', + '@elevenlabs/elevenlabs-js', + '@google/genai', + 'sarvamai', +]; + +/** Matches every form that loads the package eagerly: + * `import x from 'pkg'`, `export * from 'pkg'`, the bare side-effect + * `import 'pkg'`, and `require('pkg')`. Dynamic `import('pkg')` has no `from` + * and no quote directly after `import`, so it never matches, which is what + * lets `importOptional(() => import('pkg'), SPEC)` through. + * + * Subpaths count. `import { OpenAI } from 'openai/index.mjs'` links at load + * time exactly like the bare specifier does, and would otherwise slip past + * the one test standing between a regression and a broken install. + * + * `require.resolve('pkg')` is deliberately NOT matched: resolving asks + * whether a package is there without loading it, which is exactly what + * `isDepInstalled` does. */ +function eagerLoadOf(pkg: string): RegExp { + const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp( + `(?:\\bfrom\\s*|^\\s*import\\s*|\\brequire\\s*\\(\\s*)['"]${escaped}(?:/[^'"]*)?['"]`, + ); +} + +/** `createRequire(url)('pkg')` loads just as eagerly as `require('pkg')`. + * Rewriting the call to `require` lets one pattern cover both instead of + * growing `eagerLoadOf` into something nobody can read. + * + * Known gap: `const r = createRequire(url); r('pkg')` splits the call from + * the specifier across statements, and the binding can be named anything, so + * no line-based scan can see it. That two-step form is what `src/cli.ts` and + * `src/overlays/gsap-runtime.ts` already use, so treat this as defence in + * depth rather than proof. */ +const CREATE_REQUIRE = /createRequire\s*\([^)]*\)/g; + +/** `import type` / `export type` is erased before it reaches the runtime, so + * it cannot break an install that lacks the package. + * + * Stripped from the whole source rather than line by line: a wrapped type + * import leaves its `from` clause on a later line, which no per-line check + * can attribute back to the `type` keyword that opened the statement. */ +const TYPE_ONLY = /^[ \t]*(?:import|export)\s+type\b[\s\S]*?from\s*['"][^'"]*['"]/gm; + +describe('optional packages are never loaded at module scope', () => { + // All six are devDependencies, so they resolve during tests. Nothing else in + // the suite can notice a regression from `await import(x)` back to a + // top-level `import x from`. This test is the only guard. + const files = sourceFiles(SRC); + + it('finds source files to scan', () => { + expect(files.length).toBeGreaterThan(20); + }); + + for (const pkg of OPTIONAL_PACKAGES) { + it(`no eager load of ${pkg}`, () => { + const pattern = eagerLoadOf(pkg); + const offenders = files + .filter((file) => { + const src = readFileSync(file, 'utf-8') + .replace(TYPE_ONLY, '') + .replace(CREATE_REQUIRE, 'require'); + return src.split('\n').some((line) => { + const code = line.replace(/\/\/.*$/, '').replace(/^\s*\*.*$/, ''); + return pattern.test(code); + }); + }) + .map((f) => relative(SRC, f)); + expect(offenders).toEqual([]); + }); + } +});