From 8b242edc56c424728127c905cd28d7155d426b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fr=C3=B8yland?= <81354124+Andreas-Froyland@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:25:49 +0200 Subject: [PATCH 1/5] feat(drivers): add the Tauri driver adapter and a runnable sample consumer (Task 2.2, part 3b, in progress) - packages/qa/src/drivers/tauri.ts: TauriApp starts tauri-driver as an owned process, opens a WebdriverIO session on the installed app, restarts and closes it (waiting for the app to exit), and owns the native driver and the app (found by parent pid and exact executable path) so the runner reaps them. Configuration mistakes are reported before anything starts; a tauri-driver that dies is reported at once with its exit code. - packages/qa/src/drivers/os.ts: processes by exact executable, children of a process, port checks. - examples/tauri-smoke/qa: project.json, the lifecycle (NSIS into the test root on Windows with the installer's outside effects undone; the .deb unpacked into the test root on Linux), and the Stage 0 persistence scenario. - docs/guides/run-the-sample.md: the setup guide for a fresh Windows or Ubuntu machine. - webdriverio 9.31.9 (the Stage 0 version), kept out of the package's main entry point. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/guides/run-the-sample.md | 97 + examples/tauri-smoke/qa/app.ts | 84 + examples/tauri-smoke/qa/lifecycle.ts | 103 + examples/tauri-smoke/qa/package.json | 5 + examples/tauri-smoke/qa/persistence.spec.ts | 42 + examples/tauri-smoke/qa/project.json | 18 + .../tauri-smoke/scripts/write-candidate.mjs | 26 + package-lock.json | 3582 +++++++++++++++-- packages/qa/package.json | 3 + packages/qa/src/drivers/os.ts | 69 + packages/qa/src/drivers/tauri.ts | 131 + packages/qa/test/drivers/os.test.ts | 60 + packages/qa/test/drivers/tauri.test.ts | 43 + packages/qa/tsconfig.json | 2 +- 14 files changed, 3886 insertions(+), 379 deletions(-) create mode 100644 docs/guides/run-the-sample.md create mode 100644 examples/tauri-smoke/qa/app.ts create mode 100644 examples/tauri-smoke/qa/lifecycle.ts create mode 100644 examples/tauri-smoke/qa/package.json create mode 100644 examples/tauri-smoke/qa/persistence.spec.ts create mode 100644 examples/tauri-smoke/qa/project.json create mode 100644 examples/tauri-smoke/scripts/write-candidate.mjs create mode 100644 packages/qa/src/drivers/os.ts create mode 100644 packages/qa/src/drivers/tauri.ts create mode 100644 packages/qa/test/drivers/os.test.ts create mode 100644 packages/qa/test/drivers/tauri.test.ts diff --git a/docs/guides/run-the-sample.md b/docs/guides/run-the-sample.md new file mode 100644 index 0000000..7774c16 --- /dev/null +++ b/docs/guides/run-the-sample.md @@ -0,0 +1,97 @@ +# Running the sample through Release QA + +This guide takes a fresh machine to a passing `release-qa run` of the sample app's persistence scenario +([`examples/tauri-smoke`](../../examples/tauri-smoke)): the packaged app is installed, saved to, checked on disk, +restarted, cleared, restarted again and removed, all driven through WebDriver as proven in Stage 0 +([decision](../decisions/native-automation.md)). + +It needs a **designated machine**: an interactive Windows desktop session, or Linux with a display (Xvfb is enough +for this sample). Hosted CI runners are not used for this; see [tool layout](../decisions/tool-layout.md#ci). + +The same four pieces are needed everywhere, pinned to what Stage 0 proved: + +| Piece | Version | Why pinned | +| --- | --- | --- | +| Node.js | 22.18 or newer (`.node-version` has the exact CI version) | runs the CLI with no build step | +| `tauri-driver` | 2.0.6, `cargo install --locked` | the driver chain Stage 0 proved | +| Native driver | Windows: Microsoft Edge WebDriver **matching the installed WebView2 runtime**; Linux: `WebKitWebDriver` from `webkit2gtk-driver` | a mismatched Edge WebDriver refuses to start sessions | +| The sample's package | built on the same machine type: NSIS installer (Windows), `.deb` (Linux) | the candidate is the file you built | + +## Windows 11 + +Prerequisites: Git, Node.js, [Rust](https://rustup.rs), and the [Tauri 2 prerequisites](https://v2.tauri.app/start/prerequisites/) +(Microsoft C++ Build Tools; WebView2 is part of Windows 11). In PowerShell, from a clone of this repository: + +```powershell +cargo install tauri-driver --locked --version 2.0.6 + +# The Edge WebDriver must match the WebView2 runtime exactly. Read its version, then fetch that driver. +$v = (Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}').pv +# A per-user WebView2 install registers under HKCU:\Software\Microsoft\EdgeUpdate\Clients\{...} instead. +$tools = "$env:LOCALAPPDATA\release-qa\tools\msedgedriver-$v" +New-Item -ItemType Directory -Force $tools | Out-Null +Invoke-WebRequest "https://msedgedriver.microsoft.com/$v/edgedriver_win64.zip" -OutFile "$tools\edgedriver_win64.zip" +Expand-Archive "$tools\edgedriver_win64.zip" -DestinationPath $tools -Force +Get-FileHash "$tools\msedgedriver.exe" -Algorithm SHA256 # record it with your run +$env:RELEASE_QA_NATIVE_DRIVER = "$tools\msedgedriver.exe" + +npm ci +cd examples\tauri-smoke +npm ci +npm run build:windows +$candidate = node scripts\write-candidate.mjs +cd ..\.. + +node packages\qa\src\cli\main.ts designate +node packages\qa\src\cli\main.ts doctor --project examples\tauri-smoke\qa\project.json --profile windows +node packages\qa\src\cli\main.ts run --project examples\tauri-smoke\qa\project.json --candidate $candidate --profile windows --suite release +``` + +The app window opens and closes three times. Leave the machine alone while it runs. + +The sample must not already be installed for your user: the run refuses, rather than taking over an installation it did +not make. The installer goes into the test root (`/S /D=\smoke-app`); outside it, it adds an uninstall entry, +Start Menu and Desktop shortcuts, and `HKCU\Software\frogbyte\Release QA Smoke`. Cleanup runs the uninstaller in place +and removes that key, which the uninstaller leaves behind. + +## Ubuntu 24.04 + +A fresh machine, a VM or a WSL2 distribution all work; the sample only needs a virtual display. From a clone of this +repository: + +```sh +sudo apt-get update +sudo apt-get install -y build-essential curl wget file pkg-config libwebkit2gtk-4.1-dev libxdo-dev libssl-dev \ + libayatana-appindicator3-dev librsvg2-dev xvfb webkit2gtk-driver git +# Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . "$HOME/.cargo/env" +cargo install tauri-driver --locked --version 2.0.6 +# Node.js: any 22.18+ install; for example the official build pinned in .node-version +``` + +Then: + +```sh +npm ci +(cd examples/tauri-smoke && npm ci && npm run build:linux) +candidate=$(node examples/tauri-smoke/scripts/write-candidate.mjs) + +node packages/qa/src/cli/main.ts designate +xvfb-run -a -s "-screen 0 1280x1024x24" node packages/qa/src/cli/main.ts doctor --project examples/tauri-smoke/qa/project.json --profile linux +xvfb-run -a -s "-screen 0 1280x1024x24" node packages/qa/src/cli/main.ts run --project examples/tauri-smoke/qa/project.json --candidate "$candidate" --profile linux --suite release +``` + +`RELEASE_QA_NATIVE_DRIVER` defaults to `/usr/bin/WebKitWebDriver` on Linux. The `.deb` is **unpacked** into the test +root with `dpkg-deb -x` rather than installed with `apt`: the app binary is the packaged one byte for byte, no root +access is needed, and nothing is installed system-wide, but the package manager's own steps and desktop integration are +not exercised. + +## What a run leaves + +- A passing run exits `0` and prints `linux/persistence: passed` (or `windows/...`). The run's journal and summary are + under `.release-qa/runs//`. +- The app, its data directory (`%APPDATA%\dev.frogbyte.releaseqa.smoke` or `~/.local/share/dev.frogbyte.releaseqa.smoke`) + and everything the installer created are removed. The sample's data directory is wiped at the start and end of + every run, so do not use the sample app for anything else on that machine. +- If a run is interrupted, `resume --run ` continues it; if cleanup could not finish, `reset` clears what it left + once you have fixed the cause. See [local runs](../decisions/local-runs.md). diff --git a/examples/tauri-smoke/qa/app.ts b/examples/tauri-smoke/qa/app.ts new file mode 100644 index 0000000..268da87 --- /dev/null +++ b/examples/tauri-smoke/qa/app.ts @@ -0,0 +1,84 @@ +// What the lifecycle and the scenario share: where the app goes, where its data lives, how to run a process to its +// end as something the run owns, and the one live session. +import { execFile } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join, sep } from 'node:path'; +import { TauriApp } from '../../../packages/qa/src/drivers/tauri.ts'; +import type { RunContext } from '../../../packages/qa/src/runner/execute.ts'; + +export const IDENTIFIER = 'dev.frogbyte.releaseqa.smoke'; +export const PRODUCT = 'Release QA Smoke'; +export const windows = process.platform === 'win32'; + +/** Where the app is installed: inside the designated test root, so the run owns it and the runner can remove it. */ +export const installDir = (ctx: RunContext): string => join(ctx.testRoot, 'smoke-app'); + +/** The installed executable: NSIS puts it at the top of the install directory; the .deb unpacks it under usr/bin. */ +export const executable = (ctx: RunContext): string => + windows ? join(installDir(ctx), 'release-qa-tauri-smoke.exe') : join(installDir(ctx), 'usr', 'bin', 'release-qa-tauri-smoke'); + +/** The app's own data directories. The app decides where these are, so they are outside the test root. */ +export function dataDirs(): string[] { + const roots = windows + ? [process.env.APPDATA, process.env.LOCALAPPDATA] + : [process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache')]; + return roots.flatMap((root) => (root === undefined || root === '' ? [] : [join(root, IDENTIFIER)])); +} + +/** Where Save writes the value (see the sample's README). */ +export const settingFile = (): string => join(dataDirs()[0] as string, 'setting.txt'); + +/** Removes the app's data directories. Only ever a directory named exactly after this sample's identifier. */ +export async function removeData(): Promise { + for (const dir of dataDirs()) { + if (!dir.endsWith(`${sep}${IDENTIFIER}`)) throw new Error(`refusing to delete ${dir}: not this sample's data directory`); + await rm(dir, { recursive: true, force: true }); + } +} + +/** Runs a command as a process the run owns, and waits for it; a non-zero exit is an error naming the command. */ +export async function runToEnd(ctx: RunContext, label: string, command: string, args: readonly string[]): Promise { + const child = await ctx.spawn(label, command, args, { stdio: 'ignore', windowsHide: true }); + const code = await new Promise((resolveExit) => { + if (child.exitCode !== null || child.signalCode !== null) resolveExit(child.exitCode); + else child.once('exit', (exitCode) => resolveExit(exitCode)); + }); + if (code !== 0) throw new Error(`${label} (${command}) exited with ${code}`); +} + +/** Whether a registry key exists (Windows). */ +export function registryKeyExists(key: string): Promise { + return new Promise((resolveQuery) => execFile('reg', ['query', key], { windowsHide: true }, (error) => resolveQuery(error === null))); +} + +/** Deletes a registry key (Windows); a key that is already gone is fine. */ +export async function deleteRegistryKey(key: string): Promise { + if (!(await registryKeyExists(key))) return; + await new Promise((resolveDelete, rejectDelete) => + execFile('reg', ['delete', key, '/f'], { windowsHide: true }, (error) => (error ? rejectDelete(error) : resolveDelete())), + ); +} + +// What a per-user NSIS install creates outside its directory (measured on this sample; see the setup guide). +export const UNINSTALL_KEY = `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${PRODUCT}`; +/** NSIS's remembered install directory. The uninstaller leaves it behind, so cleanup removes it. */ +export const REMEMBERED_DIR_KEY = `HKCU\\Software\\frogbyte\\${PRODUCT}`; +export const VENDOR_KEY = 'HKCU\\Software\\frogbyte'; + +/** The native driver, from the environment: it must match the installed webview, so it is never guessed. */ +export function nativeDriver(): string { + const configured = process.env.RELEASE_QA_NATIVE_DRIVER; + if (configured !== undefined && configured !== '') return configured; + if (!windows && existsSync('/usr/bin/WebKitWebDriver')) return '/usr/bin/WebKitWebDriver'; + throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)'); +} + +/** The live session, set by the lifecycle's launch hook and ended by its cleanup hook. */ +export const app: { session: TauriApp | undefined } = { session: undefined }; + +export function session(): TauriApp { + if (app.session === undefined) throw new Error('the app was not launched'); + return app.session; +} diff --git a/examples/tauri-smoke/qa/lifecycle.ts b/examples/tauri-smoke/qa/lifecycle.ts new file mode 100644 index 0000000..0c8b4db --- /dev/null +++ b/examples/tauri-smoke/qa/lifecycle.ts @@ -0,0 +1,103 @@ +// The sample's lifecycle for `release-qa run`. Everything it creates is owned by the run or undone by cleanup: +// - Windows: the NSIS installer runs silently into the test root (`/S /D=`). Outside that directory it adds an +// uninstall entry, Start Menu and Desktop shortcuts (all removed by its uninstaller) and a remembered-directory +// registry key (which the uninstaller leaves, so cleanup removes it). +// - Linux: the .deb is unpacked into the test root with `dpkg-deb -x`, so no root access is needed and nothing is +// installed system-wide. The binary is the packaged one byte for byte; the package manager's own steps and desktop +// integration are not exercised. +// The app's data directory is wherever the app puts it; reset and cleanup remove exactly that directory. +import { execFile } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { TauriApp } from '../../../packages/qa/src/drivers/tauri.ts'; +import type { Lifecycle, RunContext } from '../../../packages/qa/src/runner/execute.ts'; +import { + app, + deleteRegistryKey, + executable, + installDir, + nativeDriver, + PRODUCT, + registryKeyExists, + REMEMBERED_DIR_KEY, + removeData, + runToEnd, + UNINSTALL_KEY, + VENDOR_KEY, + windows, +} from './app.ts'; + +async function install(ctx: RunContext): Promise { + if (ctx.artifact === undefined) throw new Error('this lifecycle installs the candidate artifact, and the run has none'); + if (windows) { + // An install this run did not make cannot be owned, and installing over it would take over its registry entries. + for (const key of [UNINSTALL_KEY, REMEMBERED_DIR_KEY]) { + if (await registryKeyExists(key)) throw new Error(`${PRODUCT} is already installed for this user (${key} exists); uninstall it before running`); + } + } + const dir = installDir(ctx); + await ctx.own({ kind: 'path', path: dir, label: 'installed app' }); + if (windows) await runToEnd(ctx, 'installer', ctx.artifact.path, ['/S', `/D=${dir}`]); + else await runToEnd(ctx, 'unpack', 'dpkg-deb', ['-x', ctx.artifact.path, dir]); + if (!existsSync(executable(ctx))) throw new Error(`the installer finished but ${executable(ctx)} does not exist`); +} + +async function reset(): Promise { + await removeData(); +} + +async function launch(ctx: RunContext): Promise { + app.session = await TauriApp.start(ctx, { + application: executable(ctx), + nativeDriver: nativeDriver(), + ...(process.env.RELEASE_QA_TAURI_DRIVER ? { tauriDriver: process.env.RELEASE_QA_TAURI_DRIVER } : {}), + }); +} + +/** Undoes everything install and the app did. Every step runs even if an earlier one failed; failures are reported together. */ +async function cleanup(ctx: RunContext): Promise { + const failures: string[] = []; + const attempt = async (what: string, step: () => Promise): Promise => { + try { + await step(); + } catch (error) { + failures.push(`${what}: ${error instanceof Error ? error.message : String(error)}`); + } + }; + + await attempt('closing the app', async () => { + const running = app.session; + app.session = undefined; + await running?.close(); + }); + if (windows) { + const uninstaller = join(installDir(ctx), 'uninstall.exe'); + // `_?=` makes the NSIS uninstaller run in place and finish before returning, instead of relaunching from %TEMP%. + if (existsSync(uninstaller)) await attempt('uninstalling', () => runToEnd(ctx, 'uninstaller', uninstaller, ['/S', `_?=${installDir(ctx)}`])); + await attempt('removing the remembered install directory', () => deleteRegistryKey(REMEMBERED_DIR_KEY)); + await attempt('removing the vendor registry key if nothing else is in it', async () => { + if (await registryKeyIsEmpty(VENDOR_KEY)) await deleteRegistryKey(VENDOR_KEY); + }); + await attempt('checking the uninstall entry is gone', async () => { + if (await registryKeyExists(UNINSTALL_KEY)) throw new Error(`${UNINSTALL_KEY} is still there`); + }); + } + await attempt('removing the app data', removeData); + if (failures.length > 0) throw new Error(failures.join('; ')); +} + +/** + * True when the key exists and has neither values nor subkeys. Measured: `reg query` prints nothing for an empty key, one + * line per subkey, and the key's own name followed by its values when it has values. + */ +function registryKeyIsEmpty(key: string): Promise { + return new Promise((resolveQuery) => + execFile('reg', ['query', key], { windowsHide: true }, (error, stdout) => { + if (error !== null) return resolveQuery(false); + const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + resolveQuery(lines.length === 0); + }), + ); +} + +export const lifecycle: Lifecycle = { install, reset: () => reset(), launch, cleanup }; diff --git a/examples/tauri-smoke/qa/package.json b/examples/tauri-smoke/qa/package.json new file mode 100644 index 0000000..7befa0b --- /dev/null +++ b/examples/tauri-smoke/qa/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "type": "module", + "description": "Release QA consumer files for the sample app, loaded by `release-qa run` under Node type stripping" +} diff --git a/examples/tauri-smoke/qa/persistence.spec.ts b/examples/tauri-smoke/qa/persistence.spec.ts new file mode 100644 index 0000000..ae658b4 --- /dev/null +++ b/examples/tauri-smoke/qa/persistence.spec.ts @@ -0,0 +1,42 @@ +// The Stage 0 persistence check as a Release QA scenario: save a value, confirm it on disk independently, restart the +// app and see it again, clear it, restart and see it gone. Waiting uses the runner's `ctx.waitFor`, so a value that +// never appears is an assertion failure (the candidate misbehaved), not an infrastructure error; checks on disk use +// node:assert. Elements are found by id, as in Stage 0. +import assert from 'node:assert'; +import { randomUUID } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import type { RunContext } from '../../../packages/qa/src/runner/execute.ts'; +import { session, settingFile } from './app.ts'; + +/** The readout's text, or undefined if it cannot be read yet (e.g. the page is still loading after a restart). */ +async function readout(): Promise { + return (await session().browser.$('#saved-value')).getText().catch(() => undefined); +} + +const shows = (ctx: RunContext, expected: string, description: string): Promise => + ctx.waitFor(async () => (await readout()) === expected, { timeoutMs: 20_000, intervalMs: 200, description }); + +export const scenarios = [ + { + id: 'persistence', + async steps(ctx: RunContext): Promise { + const value = `release-qa ${randomUUID().slice(0, 8)} åäö ✓`; + + await shows(ctx, '', 'the readout to start empty'); + await (await session().browser.$('#setting-input')).setValue(value); + await (await session().browser.$('#save-button')).click(); + await shows(ctx, value, 'the saved value to be shown'); + assert.equal(readFileSync(settingFile(), 'utf8'), value, `the value on disk in ${settingFile()}`); + + await session().restart(); + await shows(ctx, value, 'the saved value to be shown after a restart'); + + await (await session().browser.$('#clear-button')).click(); + await shows(ctx, '', 'the value to be cleared'); + assert.equal(existsSync(settingFile()), false, `Clear removes ${settingFile()}`); + + await session().restart(); + await shows(ctx, '', 'the cleared state to be shown after a restart'); + }, + }, +]; diff --git a/examples/tauri-smoke/qa/project.json b/examples/tauri-smoke/qa/project.json new file mode 100644 index 0000000..568e3c4 --- /dev/null +++ b/examples/tauri-smoke/qa/project.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "projectId": "release-qa-tauri-smoke", + "releaseBranch": "main", + "profiles": [ + { "id": "windows", "os": "windows", "arch": "x86_64" }, + { "id": "linux", "os": "linux", "arch": "x86_64" } + ], + "requirements": [ + { "key": "windows/persistence", "mode": "automated", "title": "A saved setting survives a restart and Clear removes it", "capabilities": ["display"] }, + { "key": "linux/persistence", "mode": "automated", "title": "A saved setting survives a restart and Clear removes it", "capabilities": ["display"] } + ], + "suites": [{ "id": "release", "requirements": ["windows/persistence", "linux/persistence"] }], + "scenarioFiles": ["persistence.spec.ts"], + "lifecycleModule": "lifecycle.ts", + "workflows": { "prepare": "qa-prepare.yml", "gate": "qa-gate.yml", "publish": "qa-publish.yml" }, + "markers": { "releaseNotes": "release-notes", "qa": "qa" } +} diff --git a/examples/tauri-smoke/scripts/write-candidate.mjs b/examples/tauri-smoke/scripts/write-candidate.mjs new file mode 100644 index 0000000..1377489 --- /dev/null +++ b/examples/tauri-smoke/scripts/write-candidate.mjs @@ -0,0 +1,26 @@ +// Writes a local candidate manifest for the package this machine just built: the NSIS installer on Windows, the .deb +// on Linux. It hashes the file as it is now; build again and you have a new candidate, so run this again. +// +// node scripts/write-candidate.mjs [id] +// +// The manifest goes next to the bundles (src-tauri/target/release/bundle/candidate.json), because a manifest may only +// name files in or under its own directory. Its path is printed for `release-qa run --candidate`. +import { createHash } from 'node:crypto'; +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const bundle = join(dirname(fileURLToPath(import.meta.url)), '..', 'src-tauri', 'target', 'release', 'bundle'); +const [profile, kind, extension] = process.platform === 'win32' ? ['windows', 'nsis', '.exe'] : ['linux', 'deb', '.deb']; + +const files = readdirSync(join(bundle, kind)).filter((name) => name.endsWith(extension)); +if (files.length !== 1) { + console.error(`expected exactly one ${extension} in ${join(bundle, kind)}, found ${files.length}: build the sample first (npm run build:${profile})`); + process.exit(1); +} +const [name] = files; +const sha256 = createHash('sha256').update(readFileSync(join(bundle, kind, name))).digest('hex'); +const id = process.argv[2] ?? `local-${profile}-${sha256.slice(0, 12)}`; +const path = join(bundle, 'candidate.json'); +writeFileSync(path, `${JSON.stringify({ schemaVersion: 1, id, artifacts: [{ profile, name, path: `${kind}/${name}`, sha256 }] }, null, 2)}\n`); +console.log(path); diff --git a/package-lock.json b/package-lock.json index 0f2a68c..c105940 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,23 @@ "resolved": "apps/desktop", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -65,6 +82,56 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "CC-BY-4.0", + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", @@ -327,6 +394,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -356,13 +429,42 @@ "version": "22.20.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "license": "MIT" + }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -741,229 +843,1701 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "node_modules/@wdio/config": { + "version": "9.31.9", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.31.9.tgz", + "integrity": "sha512-RrEiAJwWVuc8v1sgLFmqMhNoNqXc14UMEXNDEn+U2ihYY+n5M+86tuP9ns06N4SYhnVofjPjly7IZ2Qhadd2cQ==", "license": "MIT", + "dependencies": { + "@wdio/logger": "9.29.1", + "@wdio/types": "9.31.2", + "@wdio/utils": "9.31.9", + "deepmerge-ts": "^8.0.0", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, "engines": { - "node": ">=12" + "node": ">=18.20.0" } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, + "node_modules/@wdio/logger": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.29.1.tgz", + "integrity": "sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==", "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=18" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" + "node": ">=18.20.0" } }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, + "node_modules/@wdio/protocols": { + "version": "9.31.5", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.31.5.tgz", + "integrity": "sha512-e4H5exl5xCcITF0y/3zd6pf9kX4TmV8/0nnWDGt4X0KbgDi/+tGqlquZ+YS4YBEPbv95yTqVhnazEsaBiH85Tw==", "license": "MIT" }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "node_modules/@wdio/types": { + "version": "9.31.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.31.2.tgz", + "integrity": "sha512-lQKaiQDCJJ6VC1K/cKzaAxCpwB9pL1U0VbmX2uGksMxO4EDbNuLvIxWryuUDJ7OaSYTjxILTKYFErdbT/xEikg==", "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "@types/node": "^20.1.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=18.20.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", + "node_modules/@wdio/utils": { + "version": "9.31.9", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.31.9.tgz", + "integrity": "sha512-k0Py9L7PxRq8d8/6+B5l4078c12/7lbDzqUVrrhH7VJDV0/EM0qVTtP1LVttLYZEPazdAn0kLSFBCLiiJmfM7Q==", + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.3" + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.31.2", + "decamelize": "^6.0.0", + "deepmerge-ts": "^8.0.0", + "edgedriver": "^6.3.1", + "geckodriver": "^6.1.1", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "node": ">=18.20.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], + "node_modules/@zip.js/zip.js": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.17.0.tgz", + "integrity": "sha512-WxvSsJvlI+5v4f1gWjGfqvzOB8XkAIughO+iJUPOJR6OSpUkFauBzi1G4xS2MmK1fxHGuDAqq9Lnpq/ri7BWuw==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 12.0.0" + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=6.5" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "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==", + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 14" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 14" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "license": "MIT" + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==" + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge-ts": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edge-paths/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/edge-paths/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/edgedriver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.3.1.tgz", + "integrity": "sha512-wCL/Ydgt0DPkWecjhhPj2/lnDxsGeHLPKeJYKKMTNol+ULFVg4WaTvxoB19GU4tieM6LSA3vZEdZxmDC0DwIpQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.29.1", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "edge-paths": "^3.0.5", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "which": "^6.0.1" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/geckodriver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.1.tgz", + "integrity": "sha512-/AcCyc9o9o6hUbudaSJM2iOtXbxSLqQPOb4GrPvEN40cjraUeaX/j5kH3mSgwiroyMn7qzx2wM61AQ2XY8j3sA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "modern-tar": "^0.7.3" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-arm64-gnu": { @@ -974,280 +2548,1076 @@ "arm64" ], "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", + "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" + } + }, + "node_modules/safaridriver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", + "integrity": "sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.31.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/serialize-error/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">= 12.0.0" + "node": ">=16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=8" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { - "node": ">= 12.0.0" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/magic-string": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", - "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", - "dev": true, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" } ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "license": "Apache-2.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 10.x" } }, - "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", "license": "MIT", - "engines": { - "node": ">=12.20.0" + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=8" } }, - "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", - "dev": true, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.150.0", - "@rolldown/pluginutils": "^1.0.0" + "ansi-regex": "^5.0.1" }, - "bin": { - "rolldown": "bin/cli.mjs" + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=12" }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } }, "node_modules/tinybench": { "version": "6.1.4", @@ -1286,6 +3656,24 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -1321,11 +3709,40 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "license": "MIT" + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "license": "MIT" + }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vite": { @@ -1490,6 +3907,184 @@ } } }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriver": { + "version": "9.31.9", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.31.9.tgz", + "integrity": "sha512-mkRevz4ZIHCBe09FtHQnWg0G/p40bqk3Rv7aBooSthc8hpKG0880B9iga08drtPSiYN6/wC59WBh10ooLPboJA==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.31.9", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.31.5", + "@wdio/types": "9.31.2", + "@wdio/utils": "9.31.9", + "deepmerge-ts": "^8.0.0", + "https-proxy-agent": "^7.0.6", + "undici": "^6.27.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriver/node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/webdriverio": { + "version": "9.31.9", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.31.9.tgz", + "integrity": "sha512-C5Uxgry8Gcs4HJ2Ln/9m8QK55S4s5qNw6bjyFnWIFaZhPaoLrC0YN6wSzZ/LqfnysPdefRjgHxctFX/G6xIi0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.31.9", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.31.5", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.31.2", + "@wdio/utils": "9.31.9", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.31.9" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1507,9 +4102,240 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "packages/qa": { "name": "@frogbyte-io/release-qa", "version": "0.0.0", + "dependencies": { + "webdriverio": "9.31.9" + }, "devDependencies": { "@types/node": "22.20.4", "typescript": "7.0.2", diff --git a/packages/qa/package.json b/packages/qa/package.json index 3dabdcf..e1abc01 100644 --- a/packages/qa/package.json +++ b/packages/qa/package.json @@ -15,5 +15,8 @@ "@types/node": "22.20.4", "typescript": "7.0.2", "vitest": "5.0.1" + }, + "dependencies": { + "webdriverio": "9.31.9" } } diff --git a/packages/qa/src/drivers/os.ts b/packages/qa/src/drivers/os.ts new file mode 100644 index 0000000..e0ea7e4 --- /dev/null +++ b/packages/qa/src/drivers/os.ts @@ -0,0 +1,69 @@ +import { execFile } from 'node:child_process'; +import { readdir, readFile, readlink } from 'node:fs/promises'; +import { connect } from 'node:net'; +import { resolve } from 'node:path'; + +// What a driver adapter needs to know about the machine to own what it starts indirectly: the application a WebDriver +// session launches, and the native driver behind tauri-driver, are not children the runner spawned itself. + +function output(command: string, args: readonly string[]): Promise { + return new Promise((resolveOutput, rejectOutput) => { + execFile(command, [...args], { timeout: 30_000, windowsHide: true }, (error, stdout) => (error ? rejectOutput(error) : resolveOutput(stdout))); + }); +} + +const fold = (path: string): string => (process.platform === 'win32' ? path.toLowerCase() : path); +const pidsIn = (text: string): number[] => text.split(/\s+/).filter(Boolean).map(Number).filter((n) => Number.isSafeInteger(n) && n > 0); +/** For a PowerShell single-quoted string, where the only special character is the single quote itself. */ +const psQuote = (value: string): string => `'${value.replace(/'/g, "''")}'`; + +/** Pids of the processes running from exactly this executable. Never matches by name: two copies elsewhere differ. */ +export async function processesRunning(executable: string): Promise { + const target = fold(resolve(executable)); + if (process.platform === 'win32') { + const script = `Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.ToLowerInvariant() -eq ${psQuote(target)} } | ForEach-Object { $_.ProcessId }`; + return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', script])); + } + const pids: number[] = []; + for (const entry of await readdir('/proc').catch(() => [] as string[])) { + if (!/^\d+$/.test(entry)) continue; + const exe = await readlink(`/proc/${entry}/exe`).catch(() => undefined); + if (exe !== undefined && fold(exe) === target) pids.push(Number(entry)); + } + return pids; +} + +/** Pids of the processes whose parent is `pid`. */ +export async function childrenOf(pid: number): Promise { + if (process.platform === 'win32') { + return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter 'ParentProcessId=${Math.trunc(pid)}').ProcessId`])); + } + const children: number[] = []; + for (const entry of await readdir('/proc').catch(() => [] as string[])) { + if (!/^\d+$/.test(entry)) continue; + const stat = await readFile(`/proc/${entry}/stat`, 'utf8').catch(() => undefined); + // "pid (comm) state ppid ..." where comm may contain spaces and parentheses; the fields start after the last ")". + const parent = stat === undefined ? undefined : Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]); + if (parent === pid) children.push(Number(entry)); + } + return children; +} + +/** Whether something accepts connections on this local port. */ +export function portInUse(port: number): Promise { + return new Promise((resolvePort) => { + const socket = connect(port, '127.0.0.1'); + socket.once('connect', () => { socket.destroy(); resolvePort(true); }); + socket.once('error', () => resolvePort(false)); + }); +} + +/** Resolves once something listens on the port; rejects, naming it, if nothing does within `timeoutMs`. */ +export async function waitForPort(port: number, timeoutMs: number): Promise { + const end = Date.now() + timeoutMs; + while (Date.now() < end) { + if (await portInUse(port)) return; + await new Promise((resolveSleep) => setTimeout(resolveSleep, 100)); + } + throw new Error(`nothing listened on port ${port} within ${timeoutMs} ms`); +} diff --git a/packages/qa/src/drivers/tauri.ts b/packages/qa/src/drivers/tauri.ts new file mode 100644 index 0000000..6aebc2a --- /dev/null +++ b/packages/qa/src/drivers/tauri.ts @@ -0,0 +1,131 @@ +import { existsSync } from 'node:fs'; +import { isAbsolute } from 'node:path'; +import { remote } from 'webdriverio'; +import type { RunContext } from '../runner/execute.ts'; +import { processIdentity } from '../runner/resources.ts'; +import { childrenOf, portInUse, processesRunning, waitForPort } from './os.ts'; + +/** + * Drives an unchanged, installed Tauri application through the WebDriver chain proven in Stage 0 (see + * docs/decisions/native-automation.md): WebdriverIO's `remote()` talks to an external `tauri-driver`, which starts the + * platform's native driver (Microsoft Edge WebDriver on Windows, WebKitWebDriver on Linux), which launches the app. + * + * Everything that chain starts is owned by the run: tauri-driver through `ctx.spawn`, and the native driver and the + * application (which the run did not spawn itself) found by parent and by exact executable path and recorded with + * `ctx.own`, so the runner's cleanup reaps them even if a hook does not. Nothing is ever matched by name. + */ +export interface TauriAppOptions { + /** Absolute path of the installed application executable. */ + application: string; + /** Absolute path of msedgedriver.exe (Windows) or WebKitWebDriver (Linux), matching the installed webview. */ + nativeDriver: string; + /** The tauri-driver command; default `tauri-driver` from PATH. */ + tauriDriver?: string; + /** tauri-driver's port; the native driver gets the next one. Default 4444. */ + port?: number; + /** How long tauri-driver and a new session may take to come up. Default 60 s. */ + startTimeoutMs?: number; + /** How long the application may take to exit after its session ends. Default 10 s. */ + exitTimeoutMs?: number; +} + +export type Browser = Awaited>; + +const DEFAULT_PORT = 4444; + +export class TauriApp { + readonly #ctx: RunContext; + readonly #options: Required> & { tauriDriver: string }; + #browser: Browser | undefined; + + private constructor(ctx: RunContext, options: TauriAppOptions) { + this.#ctx = ctx; + this.#options = { + application: options.application, + nativeDriver: options.nativeDriver, + tauriDriver: options.tauriDriver ?? 'tauri-driver', + port: options.port ?? DEFAULT_PORT, + startTimeoutMs: options.startTimeoutMs ?? 60_000, + exitTimeoutMs: options.exitTimeoutMs ?? 10_000, + }; + } + + /** + * Starts tauri-driver and opens a session, which launches the application. Refuses to start while the application + * is already running (nothing about that instance would be the run's) or while something holds the driver's ports. + */ + static async start(ctx: RunContext, options: TauriAppOptions): Promise { + const app = new TauriApp(ctx, options); + const { application, nativeDriver, port } = app.#options; + // A wrong path is a configuration mistake; say so at once instead of letting tauri-driver fail to start it. + if (isAbsolute(nativeDriver) && !existsSync(nativeDriver)) throw new Error(`the native driver ${nativeDriver} does not exist`); + const running = await processesRunning(application); + if (running.length > 0) throw new Error(`${application} is already running (pid ${running.join(', ')}); a run can only drive an instance it started`); + for (const p of [port, port + 1]) if (await portInUse(p)) throw new Error(`something is already listening on port ${p}`); + + const driver = await ctx.spawn('tauri-driver', app.#options.tauriDriver, ['--native-driver', nativeDriver, '--port', String(port), '--native-port', String(port + 1)], { stdio: 'ignore' }); + // A driver that cannot start exits at once; report that, rather than waiting out the whole start timeout. + const exited = new Promise((_, reject) => { + const fail = (code: number | null, signal: NodeJS.Signals | null): void => reject(new Error(`tauri-driver exited (${code ?? signal}) before it listened on port ${port}`)); + // It may already be gone: spawning waits to record the process, and a driver that cannot start exits at once. + if (driver.exitCode !== null || driver.signalCode !== null) fail(driver.exitCode, driver.signalCode); + else driver.once('exit', fail); + }); + exited.catch(() => undefined); // it also exits normally later, when the run's cleanup stops it + await Promise.race([waitForPort(port, app.#options.startTimeoutMs), exited]); + // The native driver is tauri-driver's child, not the run's; own it so it cannot outlive the run. + await app.#own(await childrenOf(driver.pid as number), 'native driver'); + await app.#open(); + return app; + } + + /** The live session. Throws once the application has been closed. */ + get browser(): Browser { + if (this.#browser === undefined) throw new Error('the application is not running: it was closed or never started'); + return this.#browser; + } + + /** Ends the session and waits for the application to exit, then launches it again. */ + async restart(): Promise { + await this.close(); + await this.#open(); + } + + /** + * Ends the session and waits until no process runs from the application's executable. An application that does + * not exit is reported as an error; it stays owned, so the runner's cleanup still stops it. + */ + async close(): Promise { + const browser = this.#browser; + this.#browser = undefined; + if (browser === undefined) return; + await browser.deleteSession(); + const end = Date.now() + this.#options.exitTimeoutMs; + let running = await processesRunning(this.#options.application); + while (running.length > 0 && Date.now() < end) { + await new Promise((resolveSleep) => setTimeout(resolveSleep, 250)); + running = await processesRunning(this.#options.application); + } + if (running.length > 0) throw new Error(`${this.#options.application} was still running ${this.#options.exitTimeoutMs} ms after its session ended (pid ${running.join(', ')})`); + } + + async #open(): Promise { + this.#browser = await remote({ + hostname: '127.0.0.1', + port: this.#options.port, + path: '/', + logLevel: 'warn', + connectionRetryTimeout: this.#options.startTimeoutMs, + capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities, + }); + await this.#own(await processesRunning(this.#options.application), 'application'); + } + + async #own(pids: readonly number[], label: string): Promise { + for (const pid of pids) { + const identity = await processIdentity(pid); + // A process that already exited needs no owning; one that is alive but unidentifiable cannot be owned safely. + if (identity !== undefined) await this.#ctx.own({ kind: 'process', pid, identity, label }); + } + } +} diff --git a/packages/qa/test/drivers/os.test.ts b/packages/qa/test/drivers/os.test.ts new file mode 100644 index 0000000..019aede --- /dev/null +++ b/packages/qa/test/drivers/os.test.ts @@ -0,0 +1,60 @@ +import { spawn } from 'node:child_process'; +import { createServer, type Server } from 'node:net'; +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { childrenOf, portInUse, processesRunning, waitForPort } from '../../src/drivers/os.ts'; +import { cleanUpProcessesAndRoots, trackProcess } from '../fixtures/processes.ts'; + +// Listing processes starts PowerShell on Windows, which can take seconds on a busy CI runner. +vi.setConfig({ testTimeout: 30_000 }); +afterEach(cleanUpProcessesAndRoots); + +const servers: Server[] = []; +afterEach(async () => { + await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(resolve)))); +}); + +const listen = async (): Promise => { + const server = createServer(); + servers.push(server); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return (server.address() as { port: number }).port; +}; + +const sleeper = () => trackProcess(spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' })); + +describe('finding processes by their exact executable', () => { + test('lists a running process started from that executable', async () => { + const child = sleeper(); + const pids = await processesRunning(process.execPath); + expect(pids).toContain(child.pid); + }); + + test('an executable nothing is running from lists nothing, not everything', async () => { + expect(await processesRunning(`${process.execPath}.not-running`)).toEqual([]); + }); +}); + +describe('finding the children of a process', () => { + test('lists a process this one started, and not unrelated ones', async () => { + const child = sleeper(); + const children = await childrenOf(process.pid); + expect(children).toContain(child.pid); + expect(await childrenOf(child.pid as number)).toEqual([]); + }); +}); + +describe('ports', () => { + test('a listening port is in use; a closed one is not', async () => { + const port = await listen(); + expect(await portInUse(port)).toBe(true); + await new Promise((resolve) => servers.pop()?.close(resolve)); + expect(await portInUse(port)).toBe(false); + }); + + test('waiting for a port that starts listening succeeds; one that never does is a timeout naming it', async () => { + const port = await listen(); + await expect(waitForPort(port, 2000)).resolves.toBeUndefined(); + await new Promise((resolve) => servers.pop()?.close(resolve)); + await expect(waitForPort(port, 300)).rejects.toThrow(String(port)); + }); +}); diff --git a/packages/qa/test/drivers/tauri.test.ts b/packages/qa/test/drivers/tauri.test.ts new file mode 100644 index 0000000..17264d0 --- /dev/null +++ b/packages/qa/test/drivers/tauri.test.ts @@ -0,0 +1,43 @@ +// What the adapter decides before any WebDriver session exists. Driving a real app needs tauri-driver, a native driver +// and an installed application; that is exercised by the sample's real runs (see the setup guide), not here. +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { TauriApp } from '../../src/drivers/tauri.ts'; +import { executeScenario } from '../../src/runner/execute.ts'; +import { arrange, lifecycleOf, scenarioOf } from '../fixtures/execution.ts'; +import { cleanUpProcessesAndRoots } from '../fixtures/processes.ts'; + +vi.setConfig({ testTimeout: 60_000 }); +afterEach(cleanUpProcessesAndRoots); + +async function launchWith(options: Parameters[1]) { + const began = Date.now(); + const { context } = await arrange({ + lifecycle: lifecycleOf([], { launch: async (ctx) => { await TauriApp.start(ctx, options); } }), + timeouts: { phaseMs: 30_000, stepsMs: 2000, cleanupMs: 20_000 }, + }); + const result = await executeScenario(context, scenarioOf()); + return { result, ms: Date.now() - began }; +} + +describe('starting the driver chain', () => { + test('a native driver that does not exist is reported by its path, before anything is started', async () => { + const { result, ms } = await launchWith({ application: process.execPath, nativeDriver: `${process.execPath}.missing`, startTimeoutMs: 20_000 }); + expect(result).toMatchObject({ outcome: 'interrupted', reason: 'infrastructure-error' }); + expect(result.detail).toContain(`${process.execPath}.missing`); + expect(ms).toBeLessThan(15_000); + }); + + test('a tauri-driver that exits before listening is reported with its exit code, not after the full timeout', async () => { + // Node rejects tauri-driver's arguments and exits at once: a driver that cannot start. + const { result, ms } = await launchWith({ application: `${process.execPath}.not-running`, nativeDriver: process.execPath, tauriDriver: process.execPath, startTimeoutMs: 20_000 }); + expect(result).toMatchObject({ outcome: 'interrupted', reason: 'infrastructure-error' }); + expect(result.detail).toMatch(/tauri-driver exited/); + expect(ms).toBeLessThan(15_000); + }); + + test('an application that is already running is refused: that instance would not be the run\'s', async () => { + // This test's own Node process is running from process.execPath. + const { result } = await launchWith({ application: process.execPath, nativeDriver: process.execPath, tauriDriver: process.execPath }); + expect(result.detail).toMatch(/already running/); + }); +}); diff --git a/packages/qa/tsconfig.json b/packages/qa/tsconfig.json index 6b68c46..440c4c5 100644 --- a/packages/qa/tsconfig.json +++ b/packages/qa/tsconfig.json @@ -15,5 +15,5 @@ "noEmit": true, "skipLibCheck": true }, - "include": ["src", "test"] + "include": ["src", "test", "../../examples/tauri-smoke/qa"] } From 15e62628d4d4e863200a06cdb0cc1dacecbe2b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fr=C3=B8yland?= <81354124+Andreas-Froyland@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:15:04 +0200 Subject: [PATCH 2/5] docs(guide): unset WAYLAND_DISPLAY on WSL2 so the run really uses Xvfb Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/guides/run-the-sample.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guides/run-the-sample.md b/docs/guides/run-the-sample.md index 7774c16..8d01e5b 100644 --- a/docs/guides/run-the-sample.md +++ b/docs/guides/run-the-sample.md @@ -81,6 +81,10 @@ xvfb-run -a -s "-screen 0 1280x1024x24" node packages/qa/src/cli/main.ts doctor xvfb-run -a -s "-screen 0 1280x1024x24" node packages/qa/src/cli/main.ts run --project examples/tauri-smoke/qa/project.json --candidate "$candidate" --profile linux --suite release ``` +On **WSL2**, WSLg sets `WAYLAND_DISPLAY`, and GTK then draws through WSLg instead of the virtual X display; run +`unset WAYLAND_DISPLAY` first so the run really uses Xvfb (a headless Ubuntu machine has no `WAYLAND_DISPLAY` to begin +with). `doctor` shows which display the run sees. + `RELEASE_QA_NATIVE_DRIVER` defaults to `/usr/bin/WebKitWebDriver` on Linux. The `.deb` is **unpacked** into the test root with `dpkg-deb -x` rather than installed with `apt`: the app binary is the packaged one byte for byte, no root access is needed, and nothing is installed system-wide, but the package manager's own steps and desktop integration are From 414e2daf7157e977d9c05edd21870484b1535863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fr=C3=B8yland?= <81354124+Andreas-Froyland@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:28:33 +0200 Subject: [PATCH 3/5] test(drivers): cover a busy driver port; docs and evidence for the real sample runs - Evidence: one passing run per platform as the CLI wrote it (Windows 11 desktop; a fresh Ubuntu 24.04 WSL2 machine set up by following the guide), with the versions and hashes behind each. - tool-layout: the Stage 0 harness is promoted; the mapping says what the tool checks and what it does not (driver version match is a setup step, the installed binary was hash-checked by hand); the webdriverio dependency and its npm audit advisory (extract-zip via @puppeteer/browsers, a path the adapter never takes) are recorded. - README and local-runs: the adapter and runnable sample exist, with links to the guide and evidence. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 9 ++++--- docs/decisions/local-runs.md | 2 +- docs/decisions/tool-layout.md | 17 ++++++++---- examples/tauri-smoke/qa/evidence/README.md | 27 +++++++++++++++++++ .../qa/evidence/linux/candidate.json | 12 +++++++++ .../run-20260924T111754Z-28b48a/events.jsonl | 3 +++ .../invocation.json | 9 +++++++ .../run-20260924T111754Z-28b48a/summary.json | 25 +++++++++++++++++ .../run-20260923T202104Z-cff0f5/events.jsonl | 3 +++ .../invocation.json | 9 +++++++ .../run-20260923T202104Z-cff0f5/summary.json | 25 +++++++++++++++++ packages/qa/test/drivers/tauri.test.ts | 13 +++++++++ 12 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 examples/tauri-smoke/qa/evidence/README.md create mode 100644 examples/tauri-smoke/qa/evidence/linux/candidate.json create mode 100644 examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/events.jsonl create mode 100644 examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/invocation.json create mode 100644 examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/summary.json create mode 100644 examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/events.jsonl create mode 100644 examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/invocation.json create mode 100644 examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/summary.json diff --git a/README.md b/README.md index 9debb61..ce187c3 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ The first targets are Tauri applications on Windows and Linux, with Dot X as the ## Status Stage 0 (proving the assumptions) is complete. The runner (environment checks, scenario execution, the durable run -journal) and the CLI's local commands (`doctor`, `designate`, `status`, `reset`, `run`, `resume`) exist. The Tauri -driver adapter and a runnable sample consumer, the dashboard and the GitHub integration do not exist yet. +journal), the CLI's local commands (`doctor`, `designate`, `status`, `reset`, `run`, `resume`), the Tauri driver adapter +and a runnable sample consumer exist: the sample passes through `release-qa run` on Windows and on Ubuntu 24.04 with +Xvfb ([guide](docs/guides/run-the-sample.md), [evidence](examples/tauri-smoke/qa/evidence/)). The dashboard and the +GitHub integration do not exist yet. - [Native automation](docs/decisions/native-automation.md): unchanged packaged Tauri apps can be driven on Windows and Ubuntu. The Dot X feasibility check is still open. - [GitHub merge gate](docs/decisions/github-gate.md): a no-service required check works, with documented design changes and unproven items. @@ -33,7 +35,8 @@ node packages/qa/src/cli/main.ts run --project --candidate [--state ] [--json] ``` -`doctor` and `run` need a consumer's `qa/project.json`; this repository does not ship a runnable sample consumer yet. +`doctor` and `run` need a consumer's `qa/project.json`; the sample's is [`examples/tauri-smoke/qa`](examples/tauri-smoke/qa), +and [the setup guide](docs/guides/run-the-sample.md) takes a fresh machine to a passing run. `run` also needs a [local candidate manifest](docs/decisions/local-runs.md#the-local-candidate-manifest) naming the file to test and its SHA-256, which is checked before anything is installed. diff --git a/docs/decisions/local-runs.md b/docs/decisions/local-runs.md index 506fbe8..7154a88 100644 --- a/docs/decisions/local-runs.md +++ b/docs/decisions/local-runs.md @@ -1,6 +1,6 @@ # Decision: running a suite from a checkout (Task 2.2, part 3a) -Status: **implemented** for the CLI's `run`, `resume` and `reset`, against fixture consumer projects. Running the real sample (`examples/tauri-smoke`) through a Tauri driver adapter is part 3b. +Status: **implemented** for the CLI's `run`, `resume` and `reset`: tested against fixture consumer projects, and run for real with the sample (`examples/tauri-smoke`) through the Tauri driver adapter on Windows and Ubuntu 24.04 ([guide](../guides/run-the-sample.md), [evidence](../../examples/tauri-smoke/qa/evidence/)). ## The local candidate manifest diff --git a/docs/decisions/tool-layout.md b/docs/decisions/tool-layout.md index 6daf16a..502d41d 100644 --- a/docs/decisions/tool-layout.md +++ b/docs/decisions/tool-layout.md @@ -35,16 +35,23 @@ docs/decisions/ decision records ## Stage 0 commands and where they go -The Stage 0 commands are not turned into npm scripts yet, because they need a designated machine, installed drivers and a built package rather than just `npm ci`. They stay documented and runnable in [`experiments/native-automation`](../../experiments/native-automation) until Stage 2 promotes them. +The native-automation harness is promoted (Task 2.2): the same check now runs through `release-qa run` with the sample's +`qa/` consumer, following [the setup guide](../guides/run-the-sample.md), with [evidence](../../examples/tauri-smoke/qa/evidence/) +from Windows and a fresh Ubuntu 24.04 machine. It still needs a designated machine, installed drivers and a built package, +so it is not an npm script and not in CI. The Stage 0 harness stays in [`experiments/native-automation`](../../experiments/native-automation) as the record of Stage 0. | Stage 0 command | Promoted to | | --- | --- | -| `run-attempts.mjs` (launch, save, read disk, restart, clear) | `packages/qa/src/drivers/tauri.ts` and the runner (Task 2.1/2.2), driven by `release-qa run` | -| Driver/WebView2 version match, display presence, no already-running instance | `release-qa doctor` | -| Hash the installed file, not the build tree | Candidate preparation (Task 3.1) and the runner's install step | +| `run-attempts.mjs` (launch, save, read disk, restart, clear) | `packages/qa/src/drivers/tauri.ts`, the runner and `examples/tauri-smoke/qa/`, driven by `release-qa run` | +| Display presence | `release-qa doctor` | +| No already-running instance, native driver present, driver ports free | the Tauri adapter, before it starts anything | +| Driver/WebView2 version match | a setup step in the guide; not checked by the tool | +| Hash the installed file, not the build tree | Candidate preparation (Task 3.1). The runner verifies the candidate package; the sample's installed binary was checked by hand (see the evidence) | | `experiments/github-gate/*.sh`, `sandbox/scripts/qa-evaluate.mjs` | `packages/qa/src/github/*` and consumer workflows (Tasks 3.1-3.3), applying the changes listed in [github-gate.md](github-gate.md) | -The repeatable smoke check for now is `experiments/native-automation/run-attempts.mjs` itself, plus `experiments/github-gate/run-core.sh`. Both are clearly separate from the shipped package: nothing under `packages/` or `apps/` imports from `experiments/` or `examples/`. +The repeatable smoke check is now the sample's `release-qa run` (see the guide), plus `experiments/github-gate/run-core.sh`. Nothing under `packages/` or `apps/` imports from `experiments/` or `examples/`; the sample's `qa/` files import the package (`packages/qa/src/...`) by relative path, since nothing is published. + +**WebdriverIO dependency (Task 2.2).** `packages/qa` depends on `webdriverio` 9.31.9, pinned exactly as proven in Stage 0, and only `packages/qa/src/drivers/tauri.ts` imports it, so the package's main entry point does not load it. `npm audit` reports one advisory through it: `extract-zip` ≤2.0.1 ([GHSA-jmr9-qjv8-65gv](https://github.com/advisories/GHSA-jmr9-qjv8-65gv), [GHSA-7pqw-9j4j-h8q3](https://github.com/advisories/GHSA-7pqw-9j4j-h8q3)), symlink path traversal when extracting an archive, reached through `@puppeteer/browsers`, which WebdriverIO uses to download browsers and drivers. The adapter never takes that path: it connects to an already-running `tauri-driver` by host and port, and drivers are fetched separately and hash-checked (see the guide). No patched `extract-zip` exists; npm's only offered fix is WebdriverIO below 8.15. Revisit when WebdriverIO or `@puppeteer/browsers` moves off `extract-zip`. ## CI diff --git a/examples/tauri-smoke/qa/evidence/README.md b/examples/tauri-smoke/qa/evidence/README.md new file mode 100644 index 0000000..0e15d60 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/README.md @@ -0,0 +1,27 @@ +# Evidence: the sample through `release-qa run` (Task 2.2) + +One passing run per platform, kept as the CLI wrote it (`invocation.json`, the journal `events.jsonl`, and +`summary.json`). Each was driven end to end by `node packages/qa/src/cli/main.ts run` with this directory's +`project.json`, following [the setup guide](../../../../docs/guides/run-the-sample.md). + +| | Windows | Linux | +| --- | --- | --- | +| Machine | Windows 11 Pro 10.0.26200, the interactive desktop session of the laptop used in Stage 0 | A fresh Ubuntu 24.04.5 LTS WSL2 distribution (kernel 5.15.167.4-microsoft-standard-WSL2), installed for this run, set up by following the guide literally, then removed | +| Display | real (interactive session `Console`) | virtual (Xvfb serving `:99`; `WAYLAND_DISPLAY` unset, see the guide) | +| Candidate | Stage 0's NSIS installer, SHA-256 `f2e9663903d3345f11e3b2bba3ecfd18a688b7c5124b3ad7186e63af248ff5dd` (candidate id `local-stage0-nsis-0.1.0`) | Built on that machine from this branch (commit `15e62628`): `.deb` SHA-256 `63e244ad765eb9031b6a941cd31383e03c4954b3eaf1db885806e90985f9e622` ([candidate.json](linux/candidate.json)) | +| App binary | installed into the test root with `/S /D=\smoke-app` | unpacked with `dpkg-deb -x`: SHA-256 `618eb56fdbc15323f5ea6fcdaf4e704e4f07c7324721647b15af1e399074a620`, byte for byte what `apt-get install` put in `/usr/bin` on the same machine, and the same as Stage 0's installed binary | +| tauri-driver | 2.0.6, SHA-256 `61de00257e7b1b5d2aa1326d21cd914a3590a030fd3339a8caad2c75873e9e1e` (Stage 0's) | 2.0.6 built there with `cargo install --locked`, SHA-256 `0f4d1c9c71432f1fc7601798fe741fbaaadfa86d65e42faef218067c945ca88f` | +| Native driver | Microsoft Edge WebDriver 153.0.4234.48 for WebView2 153.0.4234.48, SHA-256 `4ae19c1db425d12f30952bc6b02abf4e61a84f82b33bb17721b4150fafc0402e` (Stage 0's) | `webkit2gtk-driver` 2.52.6-0ubuntu0.24.04.1, `WebKitWebDriver` SHA-256 `488118541ba1dbeaca658073af84322900485b9435bc4fbb43eb1faf21db8b70` | +| Node.js | 24.13.0 | 22.23.2 (official build, checksum verified) | +| Runs | 2 passing (this is the second); plus one with the disk assertion deliberately broken (`failed`, exit 1, cleanup ok) and one with a missing native driver (`interrupted`, exit 3, cleanup ok) | 3 passing (this is the first) | +| Left behind | nothing: no uninstall entry, shortcuts, registry keys, app data, install directory or processes (checked after every run) | nothing: no app data, install directory or processes (checked after every run) | + +The Linux `.deb` hash differs from Stage 0's (`3e7852…`), but the app binary inside it is identical to Stage 0's: +the package's own metadata changed between builds, the binary did not. + +## Not shown here + +- **Evidence files.** Attempts record `evidence: []`: the scenario does not take screenshots yet. The DOM checks and + the independent read of `setting.txt` are what prove state, as in Stage 0. +- **Repeated attempts at Stage 0's scale.** Stage 0 ran 10 attempts in each of 3 runs per platform. These runs show + the same scenario passing through the runner and CLI; they are not a new reliability study. diff --git a/examples/tauri-smoke/qa/evidence/linux/candidate.json b/examples/tauri-smoke/qa/evidence/linux/candidate.json new file mode 100644 index 0000000..7a8ec5c --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/linux/candidate.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": 1, + "id": "local-linux-63e244ad765e", + "artifacts": [ + { + "profile": "linux", + "name": "Release QA Smoke_0.1.0_amd64.deb", + "path": "deb/Release QA Smoke_0.1.0_amd64.deb", + "sha256": "63e244ad765eb9031b6a941cd31383e03c4954b3eaf1db885806e90985f9e622" + } + ] +} diff --git a/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/events.jsonl b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/events.jsonl new file mode 100644 index 0000000..be2512c --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/events.jsonl @@ -0,0 +1,3 @@ +{"schemaVersion":1,"id":"run-20260924T111754Z-28b48a.e1","recordedAt":"2026-09-24T11:17:54Z","type":"run-started","data":{"runId":"run-20260924T111754Z-28b48a","candidateId":"local-linux-63e244ad765e","profile":"linux","machineId":"machine-75ab7407-0c3d-4632-bf20-8069d558e1e4"}} +{"schemaVersion":1,"id":"run-20260924T111754Z-28b48a.e2","recordedAt":"2026-09-24T11:17:54Z","type":"checkpoint","data":{"name":"scenario-started","requirement":"linux/persistence"},"prev":"run-20260924T111754Z-28b48a.e1"} +{"schemaVersion":1,"id":"run-20260924T111754Z-28b48a.e3","recordedAt":"2026-09-24T11:19:34Z","type":"attempt-recorded","data":{"attempt":{"id":"run-20260924T111754Z-28b48a.a1","requirement":"linux/persistence","outcome":"passed","evidence":[]}},"prev":"run-20260924T111754Z-28b48a.e2"} diff --git a/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/invocation.json b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/invocation.json new file mode 100644 index 0000000..08761e1 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/invocation.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "project": "/root/release-qa/examples/tauri-smoke/qa/project.json", + "candidate": "/root/release-qa/examples/tauri-smoke/src-tauri/target/release/bundle/candidate.json", + "profile": "linux", + "suite": "release", + "root": "/root/release-qa/.release-qa", + "artifactSha256": "63e244ad765eb9031b6a941cd31383e03c4954b3eaf1db885806e90985f9e622" +} diff --git a/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/summary.json b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/summary.json new file mode 100644 index 0000000..80bb3f3 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/linux/run-20260924T111754Z-28b48a/summary.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "events": 3, + "attempts": [ + { + "id": "run-20260924T111754Z-28b48a.a1", + "requirement": "linux/persistence", + "outcome": "passed" + } + ], + "pending": [ + "run-20260924T111754Z-28b48a.e1", + "run-20260924T111754Z-28b48a.e2", + "run-20260924T111754Z-28b48a.e3" + ], + "synced": [], + "ackMismatches": [], + "orphanAcks": [], + "missingEvidence": [], + "corrupt": 0, + "truncated": false, + "conflicts": [], + "missingPredecessors": [], + "cyclic": [] +} diff --git a/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/events.jsonl b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/events.jsonl new file mode 100644 index 0000000..0c89767 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/events.jsonl @@ -0,0 +1,3 @@ +{"schemaVersion":1,"id":"run-20260923T202104Z-cff0f5.e1","recordedAt":"2026-09-23T20:21:04Z","type":"run-started","data":{"runId":"run-20260923T202104Z-cff0f5","candidateId":"local-stage0-nsis-0.1.0","profile":"windows","machineId":"machine-88daa2a7-d336-44b7-8ede-452721a544e2"}} +{"schemaVersion":1,"id":"run-20260923T202104Z-cff0f5.e2","recordedAt":"2026-09-23T20:21:04Z","type":"checkpoint","data":{"name":"scenario-started","requirement":"windows/persistence"},"prev":"run-20260923T202104Z-cff0f5.e1"} +{"schemaVersion":1,"id":"run-20260923T202104Z-cff0f5.e3","recordedAt":"2026-09-23T20:21:13Z","type":"attempt-recorded","data":{"attempt":{"id":"run-20260923T202104Z-cff0f5.a1","requirement":"windows/persistence","outcome":"passed","evidence":[]}},"prev":"run-20260923T202104Z-cff0f5.e2"} diff --git a/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/invocation.json b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/invocation.json new file mode 100644 index 0000000..963e821 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/invocation.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "project": "D:\\release-qa\\examples\\tauri-smoke\\qa\\project.json", + "candidate": "D:\\release-qa\\examples\\tauri-smoke\\src-tauri\\target\\release\\bundle\\candidate.json", + "profile": "windows", + "suite": "release", + "root": "D:\\release-qa\\.release-qa", + "artifactSha256": "f2e9663903d3345f11e3b2bba3ecfd18a688b7c5124b3ad7186e63af248ff5dd" +} diff --git a/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/summary.json b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/summary.json new file mode 100644 index 0000000..57ce487 --- /dev/null +++ b/examples/tauri-smoke/qa/evidence/windows/run-20260923T202104Z-cff0f5/summary.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "events": 3, + "attempts": [ + { + "id": "run-20260923T202104Z-cff0f5.a1", + "requirement": "windows/persistence", + "outcome": "passed" + } + ], + "pending": [ + "run-20260923T202104Z-cff0f5.e1", + "run-20260923T202104Z-cff0f5.e2", + "run-20260923T202104Z-cff0f5.e3" + ], + "synced": [], + "ackMismatches": [], + "orphanAcks": [], + "missingEvidence": [], + "corrupt": 0, + "truncated": false, + "conflicts": [], + "missingPredecessors": [], + "cyclic": [] +} diff --git a/packages/qa/test/drivers/tauri.test.ts b/packages/qa/test/drivers/tauri.test.ts index 17264d0..27a886b 100644 --- a/packages/qa/test/drivers/tauri.test.ts +++ b/packages/qa/test/drivers/tauri.test.ts @@ -1,5 +1,6 @@ // What the adapter decides before any WebDriver session exists. Driving a real app needs tauri-driver, a native driver // and an installed application; that is exercised by the sample's real runs (see the setup guide), not here. +import { createServer } from 'node:net'; import { afterEach, describe, expect, test, vi } from 'vitest'; import { TauriApp } from '../../src/drivers/tauri.ts'; import { executeScenario } from '../../src/runner/execute.ts'; @@ -35,6 +36,18 @@ describe('starting the driver chain', () => { expect(ms).toBeLessThan(15_000); }); + test('a port something else already listens on is refused, naming it, before tauri-driver is started', async () => { + const server = createServer(); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const port = (server.address() as { port: number }).port; + try { + const { result } = await launchWith({ application: `${process.execPath}.not-running`, nativeDriver: process.execPath, tauriDriver: process.execPath, port }); + expect(result.detail).toContain(`already listening on port ${port}`); + } finally { + await new Promise((resolve) => server.close(resolve)); + } + }); + test('an application that is already running is refused: that instance would not be the run\'s', async () => { // This test's own Node process is running from process.execPath. const { result } = await launchWith({ application: process.execPath, nativeDriver: process.execPath, tauriDriver: process.execPath }); From 40f49f83b3549f6ffa2ad7c009641b70d0b18e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fr=C3=B8yland?= <81354124+Andreas-Froyland@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:04:49 +0200 Subject: [PATCH 4/5] fix(drivers): own only this run's driver chain and recover a killed run's install (review round 1) - Find the native driver and the app as descendants of this run's tauri-driver, and refuse to start if the app is already running or the ports are taken. - takeOver stops a process it cannot identify or own, then reports it. - Stop waiting for the port as soon as tauri-driver exits, including when it has already exited. - Windows install removes installer entries a killed run left in this root, and refuses entries that point elsewhere. - /proc reads only skip vanished or unreadable pids; other errors surface. - Guide: fall back to a per-user WebView2 install. Evidence: the Linux manifest is kept as a record, not a runnable candidate. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/guides/run-the-sample.md | 8 +- examples/tauri-smoke/qa/app.ts | 71 ++++++++- examples/tauri-smoke/qa/evidence/README.md | 9 +- .../{candidate.json => candidate-record.json} | 0 examples/tauri-smoke/qa/lifecycle.ts | 13 +- examples/tauri-smoke/qa/persistence.spec.ts | 5 + .../tauri-smoke/scripts/write-candidate.mjs | 15 +- packages/qa/src/drivers/os.ts | 109 ++++++++++--- packages/qa/src/drivers/tauri.ts | 148 ++++++++++++++---- packages/qa/test/drivers/os.test.ts | 62 +++++++- packages/qa/test/drivers/tauri.test.ts | 42 ++++- 11 files changed, 404 insertions(+), 78 deletions(-) rename examples/tauri-smoke/qa/evidence/linux/{candidate.json => candidate-record.json} (100%) diff --git a/docs/guides/run-the-sample.md b/docs/guides/run-the-sample.md index 8d01e5b..0310851 100644 --- a/docs/guides/run-the-sample.md +++ b/docs/guides/run-the-sample.md @@ -26,8 +26,12 @@ Prerequisites: Git, Node.js, [Rust](https://rustup.rs), and the [Tauri 2 prerequ cargo install tauri-driver --locked --version 2.0.6 # The Edge WebDriver must match the WebView2 runtime exactly. Read its version, then fetch that driver. -$v = (Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}').pv -# A per-user WebView2 install registers under HKCU:\Software\Microsoft\EdgeUpdate\Clients\{...} instead. +# A machine-wide WebView2 registers under HKLM, a per-user one under HKCU; the first one set is the installed runtime. +$v = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', + 'HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | + ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).pv } | + Where-Object { $_ -and $_ -ne '0.0.0.0' } | Select-Object -First 1 +if (-not $v) { throw 'WebView2 Runtime is not installed' } $tools = "$env:LOCALAPPDATA\release-qa\tools\msedgedriver-$v" New-Item -ItemType Directory -Force $tools | Out-Null Invoke-WebRequest "https://msedgedriver.microsoft.com/$v/edgedriver_win64.zip" -OutFile "$tools\edgedriver_win64.zip" diff --git a/examples/tauri-smoke/qa/app.ts b/examples/tauri-smoke/qa/app.ts index 268da87..4be98ce 100644 --- a/examples/tauri-smoke/qa/app.ts +++ b/examples/tauri-smoke/qa/app.ts @@ -4,7 +4,7 @@ import { execFile } from 'node:child_process'; import { existsSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { homedir } from 'node:os'; -import { join, sep } from 'node:path'; +import { join, resolve, sep } from 'node:path'; import { TauriApp } from '../../../packages/qa/src/drivers/tauri.ts'; import type { RunContext } from '../../../packages/qa/src/runner/execute.ts'; @@ -23,7 +23,8 @@ export const executable = (ctx: RunContext): string => export function dataDirs(): string[] { const roots = windows ? [process.env.APPDATA, process.env.LOCALAPPDATA] - : [process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache')]; + : // XDG says an empty value means "use the default", so `||`, not `??`. + [process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), process.env.XDG_CACHE_HOME || join(homedir(), '.cache')]; return roots.flatMap((root) => (root === undefined || root === '' ? [] : [join(root, IDENTIFIER)])); } @@ -71,8 +72,70 @@ export const VENDOR_KEY = 'HKCU\\Software\\frogbyte'; export function nativeDriver(): string { const configured = process.env.RELEASE_QA_NATIVE_DRIVER; if (configured !== undefined && configured !== '') return configured; - if (!windows && existsSync('/usr/bin/WebKitWebDriver')) return '/usr/bin/WebKitWebDriver'; - throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)'); + if (windows) throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)'); + if (existsSync('/usr/bin/WebKitWebDriver')) return '/usr/bin/WebKitWebDriver'; + throw new Error('WebKitWebDriver is not installed: install webkit2gtk-driver, or set RELEASE_QA_NATIVE_DRIVER to its path (see the setup guide)'); +} + +/** What a per-user NSIS install left outside its directory, and where each thing points. Windows only. */ +interface InstallerTraces { + uninstallLocation?: string; + rememberedDir?: string; + shortcuts: Array<{ path: string; target: string }>; +} + +async function installerTraces(): Promise { + // One PowerShell call: the uninstall entry's location, the remembered directory, and both shortcuts' targets. + const script = ` + $u = Get-ItemProperty -LiteralPath 'Registry::${UNINSTALL_KEY}' -ErrorAction SilentlyContinue + $r = Get-Item -LiteralPath 'Registry::${REMEMBERED_DIR_KEY}' -ErrorAction SilentlyContinue + $shell = New-Object -ComObject WScript.Shell + $links = @((Join-Path ([Environment]::GetFolderPath('Programs')) '${PRODUCT}.lnk'), (Join-Path ([Environment]::GetFolderPath('Desktop')) '${PRODUCT}.lnk')) | + Where-Object { Test-Path -LiteralPath $_ } | ForEach-Object { @{ path = $_; target = $shell.CreateShortcut($_).TargetPath } } + @{ uninstallLocation = $(if ($u) { $u.InstallLocation } else { $null }); rememberedDir = $(if ($r) { $r.GetValue('') } else { $null }); shortcuts = @($links) } | ConvertTo-Json -Compress -Depth 3`; + const stdout = await new Promise((resolveOut, rejectOut) => + execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true }, (error, out) => (error ? rejectOut(error) : resolveOut(out))), + ); + const parsed = JSON.parse(stdout) as { uninstallLocation?: string | null; rememberedDir?: string | null; shortcuts?: Array<{ path: string; target: string }> | null }; + return { + ...(parsed.uninstallLocation ? { uninstallLocation: parsed.uninstallLocation.replace(/^"|"$/g, '') } : {}), + ...(parsed.rememberedDir ? { rememberedDir: parsed.rememberedDir.replace(/^"|"$/g, '') } : {}), + shortcuts: parsed.shortcuts ?? [], + }; +} + +const samePath = (a: string, b: string): boolean => resolve(a).toLowerCase() === resolve(b).toLowerCase(); + +/** + * A run in this same test root that died before its cleanup (and was then `reset`, which removes the install + * directory with the uninstaller in it) leaves the installer's outside effects behind, all pointing into this root's + * install directory. Those are provably that run's, so they are removed here. Anything pointing elsewhere belongs to an + * install this run did not make, and is refused rather than taken over. Windows only; returns what it removed. + */ +export async function recoverInstallerTraces(ctx: RunContext): Promise { + const traces = await installerTraces(); + const ours = installDir(ctx); + const foreign = [ + ...(traces.uninstallLocation !== undefined && !samePath(traces.uninstallLocation, ours) ? [`${UNINSTALL_KEY} (installed at ${traces.uninstallLocation})`] : []), + ...(traces.rememberedDir !== undefined && !samePath(traces.rememberedDir, ours) ? [`${REMEMBERED_DIR_KEY} (${traces.rememberedDir})`] : []), + ...traces.shortcuts.filter((link) => !samePath(link.target, executable(ctx))).map((link) => `${link.path} (to ${link.target})`), + ]; + if (foreign.length > 0) throw new Error(`${PRODUCT} is already installed for this user outside this run's test root; uninstall it before running: ${foreign.join('; ')}`); + + const removed: string[] = []; + if (traces.uninstallLocation !== undefined) { + await deleteRegistryKey(UNINSTALL_KEY); + removed.push(UNINSTALL_KEY); + } + if (traces.rememberedDir !== undefined) { + await deleteRegistryKey(REMEMBERED_DIR_KEY); + removed.push(REMEMBERED_DIR_KEY); + } + for (const link of traces.shortcuts) { + await rm(link.path, { force: true }); + removed.push(link.path); + } + return removed; } /** The live session, set by the lifecycle's launch hook and ended by its cleanup hook. */ diff --git a/examples/tauri-smoke/qa/evidence/README.md b/examples/tauri-smoke/qa/evidence/README.md index 0e15d60..d9f4f68 100644 --- a/examples/tauri-smoke/qa/evidence/README.md +++ b/examples/tauri-smoke/qa/evidence/README.md @@ -8,12 +8,12 @@ One passing run per platform, kept as the CLI wrote it (`invocation.json`, the j | --- | --- | --- | | Machine | Windows 11 Pro 10.0.26200, the interactive desktop session of the laptop used in Stage 0 | A fresh Ubuntu 24.04.5 LTS WSL2 distribution (kernel 5.15.167.4-microsoft-standard-WSL2), installed for this run, set up by following the guide literally, then removed | | Display | real (interactive session `Console`) | virtual (Xvfb serving `:99`; `WAYLAND_DISPLAY` unset, see the guide) | -| Candidate | Stage 0's NSIS installer, SHA-256 `f2e9663903d3345f11e3b2bba3ecfd18a688b7c5124b3ad7186e63af248ff5dd` (candidate id `local-stage0-nsis-0.1.0`) | Built on that machine from this branch (commit `15e62628`): `.deb` SHA-256 `63e244ad765eb9031b6a941cd31383e03c4954b3eaf1db885806e90985f9e622` ([candidate.json](linux/candidate.json)) | +| Candidate | Stage 0's NSIS installer, SHA-256 `f2e9663903d3345f11e3b2bba3ecfd18a688b7c5124b3ad7186e63af248ff5dd` (candidate id `local-stage0-nsis-0.1.0`) | Built on that machine from this branch (commit `15e62628`): `.deb` SHA-256 `63e244ad765eb9031b6a941cd31383e03c4954b3eaf1db885806e90985f9e622` ([manifest record](linux/candidate-record.json)) | | App binary | installed into the test root with `/S /D=\smoke-app` | unpacked with `dpkg-deb -x`: SHA-256 `618eb56fdbc15323f5ea6fcdaf4e704e4f07c7324721647b15af1e399074a620`, byte for byte what `apt-get install` put in `/usr/bin` on the same machine, and the same as Stage 0's installed binary | | tauri-driver | 2.0.6, SHA-256 `61de00257e7b1b5d2aa1326d21cd914a3590a030fd3339a8caad2c75873e9e1e` (Stage 0's) | 2.0.6 built there with `cargo install --locked`, SHA-256 `0f4d1c9c71432f1fc7601798fe741fbaaadfa86d65e42faef218067c945ca88f` | | Native driver | Microsoft Edge WebDriver 153.0.4234.48 for WebView2 153.0.4234.48, SHA-256 `4ae19c1db425d12f30952bc6b02abf4e61a84f82b33bb17721b4150fafc0402e` (Stage 0's) | `webkit2gtk-driver` 2.52.6-0ubuntu0.24.04.1, `WebKitWebDriver` SHA-256 `488118541ba1dbeaca658073af84322900485b9435bc4fbb43eb1faf21db8b70` | | Node.js | 24.13.0 | 22.23.2 (official build, checksum verified) | -| Runs | 2 passing (this is the second); plus one with the disk assertion deliberately broken (`failed`, exit 1, cleanup ok) and one with a missing native driver (`interrupted`, exit 3, cleanup ok) | 3 passing (this is the first) | +| Runs | 2 passing (this is the second); plus one with the disk assertion deliberately broken (`failed`, exit 1, cleanup ok) and one with a missing native driver (`interrupted`, exit 3, cleanup ok); and, after a run was killed mid-scenario and the root `reset`, one passing run that first removed the uninstall entry, remembered-directory key and shortcuts the killed run had left | 3 passing (this is the first) | | Left behind | nothing: no uninstall entry, shortcuts, registry keys, app data, install directory or processes (checked after every run) | nothing: no app data, install directory or processes (checked after every run) | The Linux `.deb` hash differs from Stage 0's (`3e7852…`), but the app binary inside it is identical to Stage 0's: @@ -22,6 +22,9 @@ the package's own metadata changed between builds, the binary did not. ## Not shown here - **Evidence files.** Attempts record `evidence: []`: the scenario does not take screenshots yet. The DOM checks and - the independent read of `setting.txt` are what prove state, as in Stage 0. + the independent read of `setting.txt` are what prove state, as in Stage 0. Tracked in [#19](https://github.com/Frogbyte-io/release-qa/issues/19). - **Repeated attempts at Stage 0's scale.** Stage 0 ran 10 attempts in each of 3 runs per platform. These runs show the same scenario passing through the runner and CLI; they are not a new reliability study. +- **A runnable Linux candidate.** [`linux/candidate-record.json`](linux/candidate-record.json) is the manifest that + run used, kept as a record. The `.deb` it names was not committed, so it cannot be passed to `--candidate`; build + the sample and write a manifest for your own build (see the guide). diff --git a/examples/tauri-smoke/qa/evidence/linux/candidate.json b/examples/tauri-smoke/qa/evidence/linux/candidate-record.json similarity index 100% rename from examples/tauri-smoke/qa/evidence/linux/candidate.json rename to examples/tauri-smoke/qa/evidence/linux/candidate-record.json diff --git a/examples/tauri-smoke/qa/lifecycle.ts b/examples/tauri-smoke/qa/lifecycle.ts index 0c8b4db..463242d 100644 --- a/examples/tauri-smoke/qa/lifecycle.ts +++ b/examples/tauri-smoke/qa/lifecycle.ts @@ -17,7 +17,7 @@ import { executable, installDir, nativeDriver, - PRODUCT, + recoverInstallerTraces, registryKeyExists, REMEMBERED_DIR_KEY, removeData, @@ -29,13 +29,12 @@ import { async function install(ctx: RunContext): Promise { if (ctx.artifact === undefined) throw new Error('this lifecycle installs the candidate artifact, and the run has none'); - if (windows) { - // An install this run did not make cannot be owned, and installing over it would take over its registry entries. - for (const key of [UNINSTALL_KEY, REMEMBERED_DIR_KEY]) { - if (await registryKeyExists(key)) throw new Error(`${PRODUCT} is already installed for this user (${key} exists); uninstall it before running`); - } - } + // An install this run did not make cannot be owned, and installing over it would take over its entries; what an + // earlier run in this same root left behind after dying is recognised by where it points, and removed. + if (windows) await recoverInstallerTraces(ctx); const dir = installDir(ctx); + // Files already there are not this run's: owning the directory would let cleanup delete them. + if (existsSync(dir)) throw new Error(`${dir} already exists and is not this run's; remove it before running`); await ctx.own({ kind: 'path', path: dir, label: 'installed app' }); if (windows) await runToEnd(ctx, 'installer', ctx.artifact.path, ['/S', `/D=${dir}`]); else await runToEnd(ctx, 'unpack', 'dpkg-deb', ['-x', ctx.artifact.path, dir]); diff --git a/examples/tauri-smoke/qa/persistence.spec.ts b/examples/tauri-smoke/qa/persistence.spec.ts index ae658b4..4dab364 100644 --- a/examples/tauri-smoke/qa/persistence.spec.ts +++ b/examples/tauri-smoke/qa/persistence.spec.ts @@ -26,6 +26,8 @@ export const scenarios = [ await (await session().browser.$('#setting-input')).setValue(value); await (await session().browser.$('#save-button')).click(); await shows(ctx, value, 'the saved value to be shown'); + // Checked as an assertion first: a Save that wrote nothing is the candidate failing, not a read error. + assert.ok(existsSync(settingFile()), `Save writes ${settingFile()}`); assert.equal(readFileSync(settingFile(), 'utf8'), value, `the value on disk in ${settingFile()}`); await session().restart(); @@ -37,6 +39,9 @@ export const scenarios = [ await session().restart(); await shows(ctx, '', 'the cleared state to be shown after a restart'); + // The readout starts empty in the page itself, so it alone cannot show that a restart did not bring the value + // back; the file is the independent check, as after every other step. + assert.equal(existsSync(settingFile()), false, `${settingFile()} stays removed after a restart`); }, }, ]; diff --git a/examples/tauri-smoke/scripts/write-candidate.mjs b/examples/tauri-smoke/scripts/write-candidate.mjs index 1377489..e22bcdf 100644 --- a/examples/tauri-smoke/scripts/write-candidate.mjs +++ b/examples/tauri-smoke/scripts/write-candidate.mjs @@ -10,8 +10,17 @@ import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +const PROFILES = { win32: ['windows', 'nsis', '.exe'], linux: ['linux', 'deb', '.deb'] }; +// The id grammar the runner accepts (see packages/qa/src/model/validate.ts). +const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; + +const platform = PROFILES[process.platform]; +if (platform === undefined) { + console.error(`the sample has profiles for Windows and Linux only, not ${process.platform}`); + process.exit(1); +} +const [profile, kind, extension] = platform; const bundle = join(dirname(fileURLToPath(import.meta.url)), '..', 'src-tauri', 'target', 'release', 'bundle'); -const [profile, kind, extension] = process.platform === 'win32' ? ['windows', 'nsis', '.exe'] : ['linux', 'deb', '.deb']; const files = readdirSync(join(bundle, kind)).filter((name) => name.endsWith(extension)); if (files.length !== 1) { @@ -21,6 +30,10 @@ if (files.length !== 1) { const [name] = files; const sha256 = createHash('sha256').update(readFileSync(join(bundle, kind, name))).digest('hex'); const id = process.argv[2] ?? `local-${profile}-${sha256.slice(0, 12)}`; +if (!ID.test(id)) { + console.error(`${JSON.stringify(id)} is not a valid candidate id: letters, digits, ".", "_" or "-", starting with a letter or digit`); + process.exit(1); +} const path = join(bundle, 'candidate.json'); writeFileSync(path, `${JSON.stringify({ schemaVersion: 1, id, artifacts: [{ profile, name, path: `${kind}/${name}`, sha256 }] }, null, 2)}\n`); console.log(path); diff --git a/packages/qa/src/drivers/os.ts b/packages/qa/src/drivers/os.ts index e0ea7e4..3ca25fb 100644 --- a/packages/qa/src/drivers/os.ts +++ b/packages/qa/src/drivers/os.ts @@ -5,6 +5,9 @@ import { resolve } from 'node:path'; // What a driver adapter needs to know about the machine to own what it starts indirectly: the application a WebDriver // session launches, and the native driver behind tauri-driver, are not children the runner spawned itself. +// +// These fail closed: a process list that cannot be read is an error, never an empty list, because "nothing is +// running" is exactly what would let the adapter drive, or leave behind, an instance that is not the run's. function output(command: string, args: readonly string[]): Promise { return new Promise((resolveOutput, rejectOutput) => { @@ -16,6 +19,26 @@ const fold = (path: string): string => (process.platform === 'win32' ? path.toLo const pidsIn = (text: string): number[] => text.split(/\s+/).filter(Boolean).map(Number).filter((n) => Number.isSafeInteger(n) && n > 0); /** For a PowerShell single-quoted string, where the only special character is the single quote itself. */ const psQuote = (value: string): string => `'${value.replace(/'/g, "''")}'`; +const errorCode = (error: unknown): string | undefined => (error as NodeJS.ErrnoException).code; + +/** Every pid in /proc. Not being able to list it is an error, not an empty machine. */ +async function procPids(): Promise { + return (await readdir('/proc')).filter((entry) => /^\d+$/.test(entry)).map(Number); +} + +/** + * Reads something about one process from /proc. A process that exited between listing and reading, or that belongs to + * another user, yields undefined: neither can be one this run started, since those run as this user and are readable. + * Anything else is an error. + */ +async function procRead(read: () => Promise): Promise { + try { + return await read(); + } catch (error) { + if (['ENOENT', 'ESRCH', 'EACCES', 'EPERM'].includes(errorCode(error) ?? '')) return undefined; + throw error; + } +} /** Pids of the processes running from exactly this executable. Never matches by name: two copies elsewhere differ. */ export async function processesRunning(executable: string): Promise { @@ -25,45 +48,87 @@ export async function processesRunning(executable: string): Promise { return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', script])); } const pids: number[] = []; - for (const entry of await readdir('/proc').catch(() => [] as string[])) { - if (!/^\d+$/.test(entry)) continue; - const exe = await readlink(`/proc/${entry}/exe`).catch(() => undefined); - if (exe !== undefined && fold(exe) === target) pids.push(Number(entry)); + for (const pid of await procPids()) { + const exe = await procRead(() => readlink(`/proc/${pid}/exe`)); + if (exe !== undefined && fold(exe) === target) pids.push(pid); } return pids; } -/** Pids of the processes whose parent is `pid`. */ -export async function childrenOf(pid: number): Promise { +/** Every process's parent, as [pid, parent] pairs. */ +async function parents(): Promise> { if (process.platform === 'win32') { - return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter 'ParentProcessId=${Math.trunc(pid)}').ProcessId`])); + const text = await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId) $($_.ParentProcessId)\" }"]); + return text.split(/\r?\n/).map((line) => pidsIn(line)).filter((pair): pair is [number, number] => pair.length === 2); } - const children: number[] = []; - for (const entry of await readdir('/proc').catch(() => [] as string[])) { - if (!/^\d+$/.test(entry)) continue; - const stat = await readFile(`/proc/${entry}/stat`, 'utf8').catch(() => undefined); + const pairs: Array<[number, number]> = []; + for (const pid of await procPids()) { + const stat = await procRead(() => readFile(`/proc/${pid}/stat`, 'utf8')); // "pid (comm) state ppid ..." where comm may contain spaces and parentheses; the fields start after the last ")". - const parent = stat === undefined ? undefined : Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]); - if (parent === pid) children.push(Number(entry)); + if (stat !== undefined) pairs.push([pid, Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])]); + } + return pairs; +} + +/** Pids of the processes whose parent is `pid`. */ +export async function childrenOf(pid: number): Promise { + return (await parents()).filter(([, parent]) => parent === pid).map(([child]) => child); +} + +/** Pids of every process below `pid`: its children, their children, and so on. */ +export async function descendantsOf(pid: number): Promise { + const pairs = await parents(); + const found = new Set(); + const pending = [pid]; + while (pending.length > 0) { + const current = pending.pop() as number; + for (const [child, parent] of pairs) { + // A pid reused as its own ancestor would loop; each process is visited once. + if (parent === current && child !== pid && !found.has(child)) { + found.add(child); + pending.push(child); + } + } } - return children; + return [...found]; } -/** Whether something accepts connections on this local port. */ -export function portInUse(port: number): Promise { +/** + * Whether something accepts connections on this local port. A connection attempt that neither succeeds nor fails + * within `timeoutMs` (a dropped connection) counts as nothing listening. + */ +export function portInUse(port: number, timeoutMs = 2000): Promise { return new Promise((resolvePort) => { const socket = connect(port, '127.0.0.1'); - socket.once('connect', () => { socket.destroy(); resolvePort(true); }); - socket.once('error', () => resolvePort(false)); + const done = (inUse: boolean): void => { + socket.destroy(); + resolvePort(inUse); + }; + socket.setTimeout(timeoutMs, () => done(false)); + socket.once('connect', () => done(true)); + socket.once('error', () => done(false)); }); } -/** Resolves once something listens on the port; rejects, naming it, if nothing does within `timeoutMs`. */ -export async function waitForPort(port: number, timeoutMs: number): Promise { +/** + * Resolves once something listens on the port; rejects, naming it, if nothing does within `timeoutMs`, and at once + * with the abort reason when `signal` is aborted (e.g. because the process that should listen has exited). + */ +export async function waitForPort(port: number, timeoutMs: number, signal?: AbortSignal): Promise { const end = Date.now() + timeoutMs; while (Date.now() < end) { - if (await portInUse(port)) return; - await new Promise((resolveSleep) => setTimeout(resolveSleep, 100)); + if (signal?.aborted) throw signal.reason; + if (await portInUse(port, Math.max(1, Math.min(2000, end - Date.now())))) return; + await new Promise((resolveSleep) => { + const timer = setTimeout(done, 100); + function done(): void { + clearTimeout(timer); + signal?.removeEventListener('abort', done); + resolveSleep(); + } + signal?.addEventListener('abort', done, { once: true }); + }); } + if (signal?.aborted) throw signal.reason; throw new Error(`nothing listened on port ${port} within ${timeoutMs} ms`); } diff --git a/packages/qa/src/drivers/tauri.ts b/packages/qa/src/drivers/tauri.ts index 6aebc2a..c2d12fd 100644 --- a/packages/qa/src/drivers/tauri.ts +++ b/packages/qa/src/drivers/tauri.ts @@ -3,16 +3,18 @@ import { isAbsolute } from 'node:path'; import { remote } from 'webdriverio'; import type { RunContext } from '../runner/execute.ts'; import { processIdentity } from '../runner/resources.ts'; -import { childrenOf, portInUse, processesRunning, waitForPort } from './os.ts'; +import { descendantsOf, portInUse, processesRunning, waitForPort } from './os.ts'; /** * Drives an unchanged, installed Tauri application through the WebDriver chain proven in Stage 0 (see * docs/decisions/native-automation.md): WebdriverIO's `remote()` talks to an external `tauri-driver`, which starts the * platform's native driver (Microsoft Edge WebDriver on Windows, WebKitWebDriver on Linux), which launches the app. * - * Everything that chain starts is owned by the run: tauri-driver through `ctx.spawn`, and the native driver and the - * application (which the run did not spawn itself) found by parent and by exact executable path and recorded with - * `ctx.own`, so the runner's cleanup reaps them even if a hook does not. Nothing is ever matched by name. + * Everything that chain starts is owned by the run. tauri-driver is started through `ctx.spawn`. The native driver and + * the application are not the run's own children, so they are found as descendants of that tauri-driver (the app is + * the native driver's child, measured on Windows and Linux) and recorded with `ctx.own`; the runner's cleanup then + * reaps them even if a hook does not. Nothing is ever matched by name, and an instance of the same executable that + * someone else started is never taken for the run's. */ export interface TauriAppOptions { /** Absolute path of the installed application executable. */ @@ -32,10 +34,65 @@ export interface TauriAppOptions { export type Browser = Awaited>; const DEFAULT_PORT = 4444; +const message = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const alive = (pid: number): boolean => { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === 'EPERM'; + } +}; +const sleep = (ms: number): Promise => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); +async function goneWithin(pid: number, ms: number): Promise { + const end = Date.now() + ms; + while (Date.now() < end) { + if (!alive(pid)) return true; + await sleep(50); + } + return !alive(pid); +} + +/** + * Makes processes the run started indirectly its own, so the runner reaps them. A process that can no longer be owned + * (the phase that started it was cut off) or that is alive but cannot be identified is stopped instead, and this + * throws: a process nobody owns must not be left running. One that exits while being identified needs nothing. + */ +export async function takeOver( + ctx: Pick, + pids: readonly number[], + label: string, + identityOf: (pid: number) => Promise = processIdentity, +): Promise { + const problems: string[] = []; + const stop = async (pid: number, why: string): Promise => { + try { + process.kill(pid); + } catch { + /* already gone */ + } + await goneWithin(pid, 5000); + problems.push(`${label} ${pid} ${why}, so it was stopped`); + }; + for (const pid of pids) { + const identity = await identityOf(pid); + if (identity === undefined) { + if (!(await goneWithin(pid, 2000))) await stop(pid, 'could not be identified'); + continue; + } + try { + await ctx.own({ kind: 'process', pid, identity, label }); + } catch (error) { + await stop(pid, `could not be owned (${message(error)})`); + } + } + if (problems.length > 0) throw new Error(problems.join('; ')); +} export class TauriApp { readonly #ctx: RunContext; readonly #options: Required> & { tauriDriver: string }; + #driverPid = 0; #browser: Browser | undefined; private constructor(ctx: RunContext, options: TauriAppOptions) { @@ -51,30 +108,33 @@ export class TauriApp { } /** - * Starts tauri-driver and opens a session, which launches the application. Refuses to start while the application - * is already running (nothing about that instance would be the run's) or while something holds the driver's ports. + * Starts tauri-driver and opens a session, which launches the application. Configuration mistakes are reported + * before anything starts; so are an application that is already running (that instance would not be the run's) and + * ports something else holds. */ static async start(ctx: RunContext, options: TauriAppOptions): Promise { const app = new TauriApp(ctx, options); const { application, nativeDriver, port } = app.#options; - // A wrong path is a configuration mistake; say so at once instead of letting tauri-driver fail to start it. if (isAbsolute(nativeDriver) && !existsSync(nativeDriver)) throw new Error(`the native driver ${nativeDriver} does not exist`); const running = await processesRunning(application); if (running.length > 0) throw new Error(`${application} is already running (pid ${running.join(', ')}); a run can only drive an instance it started`); for (const p of [port, port + 1]) if (await portInUse(p)) throw new Error(`something is already listening on port ${p}`); const driver = await ctx.spawn('tauri-driver', app.#options.tauriDriver, ['--native-driver', nativeDriver, '--port', String(port), '--native-port', String(port + 1)], { stdio: 'ignore' }); - // A driver that cannot start exits at once; report that, rather than waiting out the whole start timeout. - const exited = new Promise((_, reject) => { - const fail = (code: number | null, signal: NodeJS.Signals | null): void => reject(new Error(`tauri-driver exited (${code ?? signal}) before it listened on port ${port}`)); - // It may already be gone: spawning waits to record the process, and a driver that cannot start exits at once. - if (driver.exitCode !== null || driver.signalCode !== null) fail(driver.exitCode, driver.signalCode); - else driver.once('exit', fail); - }); - exited.catch(() => undefined); // it also exits normally later, when the run's cleanup stops it - await Promise.race([waitForPort(port, app.#options.startTimeoutMs), exited]); + app.#driverPid = driver.pid as number; + // A driver that cannot start exits at once: stop waiting for its port then, and say why. + const listening = new AbortController(); + const exited = (code: number | null, signal: NodeJS.Signals | null): void => listening.abort(new Error(`tauri-driver exited (${code ?? signal}) before it listened on port ${port}`)); + // It may already be gone: spawning waits to record the process, and a driver that cannot start exits at once. + if (driver.exitCode !== null || driver.signalCode !== null) exited(driver.exitCode, driver.signalCode); + else driver.once('exit', exited); + try { + await waitForPort(port, app.#options.startTimeoutMs, listening.signal); + } finally { + driver.off('exit', exited); + } // The native driver is tauri-driver's child, not the run's; own it so it cannot outlive the run. - await app.#own(await childrenOf(driver.pid as number), 'native driver'); + await takeOver(ctx, await descendantsOf(app.#driverPid), 'native driver'); await app.#open(); return app; } @@ -103,29 +163,49 @@ export class TauriApp { const end = Date.now() + this.#options.exitTimeoutMs; let running = await processesRunning(this.#options.application); while (running.length > 0 && Date.now() < end) { - await new Promise((resolveSleep) => setTimeout(resolveSleep, 250)); + await sleep(250); running = await processesRunning(this.#options.application); } if (running.length > 0) throw new Error(`${this.#options.application} was still running ${this.#options.exitTimeoutMs} ms after its session ended (pid ${running.join(', ')})`); } + /** + * Opens a session. Whatever `remote()` does, the application it launched is taken over afterwards: a session that + * fails after launching the app must not leave it running unowned. + */ async #open(): Promise { - this.#browser = await remote({ - hostname: '127.0.0.1', - port: this.#options.port, - path: '/', - logLevel: 'warn', - connectionRetryTimeout: this.#options.startTimeoutMs, - capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities, - }); - await this.#own(await processesRunning(this.#options.application), 'application'); - } - - async #own(pids: readonly number[], label: string): Promise { - for (const pid of pids) { - const identity = await processIdentity(pid); - // A process that already exited needs no owning; one that is alive but unidentifiable cannot be owned safely. - if (identity !== undefined) await this.#ctx.own({ kind: 'process', pid, identity, label }); + let failure: unknown; + try { + this.#browser = await remote({ + hostname: '127.0.0.1', + port: this.#options.port, + path: '/', + logLevel: 'warn', + connectionRetryTimeout: this.#options.startTimeoutMs, + capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities, + }); + } catch (error) { + failure = error; + } + try { + // The run's instances are the ones below its own tauri-driver, never others started from the same executable. + const mine = new Set(await descendantsOf(this.#driverPid)); + const launched = (await processesRunning(this.#options.application)).filter((pid) => mine.has(pid)); + // A session that is up with no application below the driver chain would leave the app unowned without a word. + if (failure === undefined && launched.length === 0) { + failure = new Error(`the session started, but no ${this.#options.application} was found below this run's tauri-driver, so it could not be owned`); + } + await takeOver(this.#ctx, launched, 'application'); + } catch (error) { + failure ??= error; + } + if (failure !== undefined && this.#browser !== undefined) { + // A session whose application could not be owned is ended, so the app it launched closes. + await this.#browser.deleteSession().catch(() => undefined); + } + if (failure !== undefined) { + this.#browser = undefined; + throw failure; } } } diff --git a/packages/qa/test/drivers/os.test.ts b/packages/qa/test/drivers/os.test.ts index 019aede..c944ab8 100644 --- a/packages/qa/test/drivers/os.test.ts +++ b/packages/qa/test/drivers/os.test.ts @@ -1,7 +1,7 @@ import { spawn } from 'node:child_process'; import { createServer, type Server } from 'node:net'; import { afterEach, describe, expect, test, vi } from 'vitest'; -import { childrenOf, portInUse, processesRunning, waitForPort } from '../../src/drivers/os.ts'; +import { childrenOf, descendantsOf, portInUse, processesRunning, waitForPort } from '../../src/drivers/os.ts'; import { cleanUpProcessesAndRoots, trackProcess } from '../fixtures/processes.ts'; // Listing processes starts PowerShell on Windows, which can take seconds on a busy CI runner. @@ -58,3 +58,63 @@ describe('ports', () => { await expect(waitForPort(port, 300)).rejects.toThrow(String(port)); }); }); + +describe('the descendants of a process', () => { + test('includes children and grandchildren', async () => { + // A child that starts its own child and prints its pid. + const script = "const { spawn } = require('child_process'); const g = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' }); console.log(g.pid); setInterval(() => {}, 1000);"; + const child = trackProcess(spawn(process.execPath, ['-e', script], { stdio: ['ignore', 'pipe', 'ignore'] })); + const grandchild = await new Promise((resolve) => child.stdout!.once('data', (chunk: Buffer) => resolve(Number(String(chunk).trim())))); + try { + const descendants = await descendantsOf(process.pid); + expect(descendants).toContain(child.pid); + expect(descendants).toContain(grandchild); + } finally { + try { process.kill(grandchild); } catch { /* gone */ } + } + }); +}); + +describe('waiting for a port can be stopped', () => { + test('an aborted wait rejects at once instead of running to its timeout', async () => { + const controller = new AbortController(); + const began = Date.now(); + setTimeout(() => controller.abort(new Error('the driver exited')), 150); + await expect(waitForPort(1, 20_000, controller.signal)).rejects.toThrow('the driver exited'); + expect(Date.now() - began).toBeLessThan(5000); + }); +}); + +describe.skipIf(process.platform === 'win32')('reading /proc (Linux)', () => { + afterEach(() => { + vi.doUnmock('node:fs/promises'); + vi.resetModules(); + }); + + test('a /proc that cannot be listed is an error, not "no processes": that would wave through a running app', async () => { + vi.doMock('node:fs/promises', async (importOriginal) => ({ ...(await importOriginal()), readdir: async () => { throw Object.assign(new Error('EACCES: /proc'), { code: 'EACCES' }); } })); + vi.resetModules(); + const os = await import('../../src/drivers/os.ts'); + await expect(os.processesRunning(process.execPath)).rejects.toThrow('/proc'); + await expect(os.childrenOf(process.pid)).rejects.toThrow('/proc'); + }); + + test('a process that vanished or belongs to another user is skipped, and the rest still found', async () => { + const child = sleeper(); + vi.doMock('node:fs/promises', async (importOriginal) => { + const real = await importOriginal(); + return { + ...real, + readdir: async (path: string) => ['999999998', '999999999', ...((await real.readdir(path)) as unknown as string[])], + readlink: async (path: string) => { + if (path.startsWith('/proc/999999998/')) throw Object.assign(new Error('gone'), { code: 'ENOENT' }); + if (path.startsWith('/proc/999999999/')) throw Object.assign(new Error('not yours'), { code: 'EACCES' }); + return real.readlink(path); + }, + }; + }); + vi.resetModules(); + const os = await import('../../src/drivers/os.ts'); + expect(await os.processesRunning(process.execPath)).toContain(child.pid); + }); +}); diff --git a/packages/qa/test/drivers/tauri.test.ts b/packages/qa/test/drivers/tauri.test.ts index 27a886b..ce51e2a 100644 --- a/packages/qa/test/drivers/tauri.test.ts +++ b/packages/qa/test/drivers/tauri.test.ts @@ -1,21 +1,23 @@ // What the adapter decides before any WebDriver session exists. Driving a real app needs tauri-driver, a native driver // and an installed application; that is exercised by the sample's real runs (see the setup guide), not here. +import { spawn } from 'node:child_process'; import { createServer } from 'node:net'; import { afterEach, describe, expect, test, vi } from 'vitest'; -import { TauriApp } from '../../src/drivers/tauri.ts'; +import { takeOver, TauriApp } from '../../src/drivers/tauri.ts'; +import { processIdentity, type OwnedResource } from '../../src/runner/resources.ts'; import { executeScenario } from '../../src/runner/execute.ts'; import { arrange, lifecycleOf, scenarioOf } from '../fixtures/execution.ts'; -import { cleanUpProcessesAndRoots } from '../fixtures/processes.ts'; +import { cleanUpProcessesAndRoots, eventually, isAlive, trackProcess } from '../fixtures/processes.ts'; vi.setConfig({ testTimeout: 60_000 }); afterEach(cleanUpProcessesAndRoots); async function launchWith(options: Parameters[1]) { - const began = Date.now(); const { context } = await arrange({ lifecycle: lifecycleOf([], { launch: async (ctx) => { await TauriApp.start(ctx, options); } }), timeouts: { phaseMs: 30_000, stepsMs: 2000, cleanupMs: 20_000 }, }); + const began = Date.now(); const result = await executeScenario(context, scenarioOf()); return { result, ms: Date.now() - began }; } @@ -32,7 +34,7 @@ describe('starting the driver chain', () => { // Node rejects tauri-driver's arguments and exits at once: a driver that cannot start. const { result, ms } = await launchWith({ application: `${process.execPath}.not-running`, nativeDriver: process.execPath, tauriDriver: process.execPath, startTimeoutMs: 20_000 }); expect(result).toMatchObject({ outcome: 'interrupted', reason: 'infrastructure-error' }); - expect(result.detail).toMatch(/tauri-driver exited/); + expect(result.detail).toContain('tauri-driver exited (9)'); // Node's exit code for an unknown option expect(ms).toBeLessThan(15_000); }); @@ -54,3 +56,35 @@ describe('starting the driver chain', () => { expect(result.detail).toMatch(/already running/); }); }); + +describe('taking over what the driver chain started', () => { + const sleeper = () => trackProcess(spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' })); + const owned: OwnedResource[] = []; + const ctx = (refuse = false) => ({ own: async (resource: OwnedResource) => { if (refuse) throw new Error('this run has been stopped'); owned.push(resource); } }); + afterEach(() => { owned.length = 0; }); + + test('records each process with its identity, so the runner can reap it', async () => { + const child = sleeper(); + await takeOver(ctx(), [child.pid as number], 'application'); + expect(owned).toEqual([{ kind: 'process', pid: child.pid, identity: await processIdentity(child.pid as number), label: 'application' }]); + }); + + test('a process the run can no longer own is stopped, and the start fails saying so', async () => { + const child = sleeper(); + await expect(takeOver(ctx(true), [child.pid as number], 'application')).rejects.toThrow(/could not be owned.*stopped/); + await eventually(() => !isAlive(child.pid as number)); + }); + + test('a live process that cannot be identified is stopped, not silently left unowned', async () => { + const child = sleeper(); + await expect(takeOver(ctx(), [child.pid as number], 'application', async () => undefined)).rejects.toThrow(/could not be identified.*stopped/); + await eventually(() => !isAlive(child.pid as number)); + expect(owned).toEqual([]); + }); + + test('one that exits while being identified needs nothing', async () => { + const child = sleeper(); + setTimeout(() => child.kill(), 200); + await expect(takeOver(ctx(), [child.pid as number], 'application', async () => undefined)).resolves.toBeUndefined(); + }); +}); From 709b2f43948dba9a4c34edfbca18f250df95a393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Fr=C3=B8yland?= <81354124+Andreas-Froyland@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:54:25 +0200 Subject: [PATCH 5/5] fix(drivers): signal only the process that was found; refuse unverifiable installer traces (review round 2) - takeOver re-reads a process's identity before stopping one it could not own, and stops an unidentifiable one only while it is still below this run's driver chain, so a reused pid is never signalled. Identification is retried while the process runs. - install refuses an existing install directory before recovering installer traces, so a copy that is still there keeps its uninstall entry and shortcuts. - A present uninstall or remembered-directory key with no location is refused, not treated as absent. Co-Authored-By: Claude Opus 5.5 (1M context) --- examples/tauri-smoke/qa/app.ts | 39 +++++++++++++++++--------- examples/tauri-smoke/qa/lifecycle.ts | 7 +++-- packages/qa/src/drivers/tauri.ts | 36 ++++++++++++++++-------- packages/qa/test/drivers/tauri.test.ts | 34 +++++++++++++++++++--- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/examples/tauri-smoke/qa/app.ts b/examples/tauri-smoke/qa/app.ts index 4be98ce..001075a 100644 --- a/examples/tauri-smoke/qa/app.ts +++ b/examples/tauri-smoke/qa/app.ts @@ -77,10 +77,13 @@ export function nativeDriver(): string { throw new Error('WebKitWebDriver is not installed: install webkit2gtk-driver, or set RELEASE_QA_NATIVE_DRIVER to its path (see the setup guide)'); } -/** What a per-user NSIS install left outside its directory, and where each thing points. Windows only. */ +/** + * What a per-user NSIS install left outside its directory, and where each thing points. Windows only. A key that is + * present is reported even when its location is missing or empty (`location: undefined`), since it still marks an install. + */ interface InstallerTraces { - uninstallLocation?: string; - rememberedDir?: string; + uninstall?: { location: string | undefined }; + remembered?: { location: string | undefined }; shortcuts: Array<{ path: string; target: string }>; } @@ -92,14 +95,21 @@ async function installerTraces(): Promise { $shell = New-Object -ComObject WScript.Shell $links = @((Join-Path ([Environment]::GetFolderPath('Programs')) '${PRODUCT}.lnk'), (Join-Path ([Environment]::GetFolderPath('Desktop')) '${PRODUCT}.lnk')) | Where-Object { Test-Path -LiteralPath $_ } | ForEach-Object { @{ path = $_; target = $shell.CreateShortcut($_).TargetPath } } - @{ uninstallLocation = $(if ($u) { $u.InstallLocation } else { $null }); rememberedDir = $(if ($r) { $r.GetValue('') } else { $null }); shortcuts = @($links) } | ConvertTo-Json -Compress -Depth 3`; + @{ uninstallPresent = [bool]$u; uninstallLocation = $(if ($u) { $u.InstallLocation } else { $null }); rememberedPresent = [bool]$r; rememberedDir = $(if ($r) { $r.GetValue('') } else { $null }); shortcuts = @($links) } | ConvertTo-Json -Compress -Depth 3`; const stdout = await new Promise((resolveOut, rejectOut) => execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true }, (error, out) => (error ? rejectOut(error) : resolveOut(out))), ); - const parsed = JSON.parse(stdout) as { uninstallLocation?: string | null; rememberedDir?: string | null; shortcuts?: Array<{ path: string; target: string }> | null }; + const parsed = JSON.parse(stdout) as { + uninstallPresent: boolean; + uninstallLocation?: string | null; + rememberedPresent: boolean; + rememberedDir?: string | null; + shortcuts?: Array<{ path: string; target: string }> | null; + }; + const location = (value: string | null | undefined): string | undefined => (value ? value.replace(/^"|"$/g, '') || undefined : undefined); return { - ...(parsed.uninstallLocation ? { uninstallLocation: parsed.uninstallLocation.replace(/^"|"$/g, '') } : {}), - ...(parsed.rememberedDir ? { rememberedDir: parsed.rememberedDir.replace(/^"|"$/g, '') } : {}), + ...(parsed.uninstallPresent ? { uninstall: { location: location(parsed.uninstallLocation) } } : {}), + ...(parsed.rememberedPresent ? { remembered: { location: location(parsed.rememberedDir) } } : {}), shortcuts: parsed.shortcuts ?? [], }; } @@ -109,25 +119,28 @@ const samePath = (a: string, b: string): boolean => resolve(a).toLowerCase() === /** * A run in this same test root that died before its cleanup (and was then `reset`, which removes the install * directory with the uninstaller in it) leaves the installer's outside effects behind, all pointing into this root's - * install directory. Those are provably that run's, so they are removed here. Anything pointing elsewhere belongs to an - * install this run did not make, and is refused rather than taken over. Windows only; returns what it removed. + * install directory. Those are provably that run's, so they are removed here. Anything pointing elsewhere, or a key + * whose location is missing, belongs to an install this run did not make (or cannot be shown to be this root's), and + * is refused rather than taken over. Windows only; returns what it removed. */ export async function recoverInstallerTraces(ctx: RunContext): Promise { const traces = await installerTraces(); const ours = installDir(ctx); + const elsewhere = (trace: { location: string | undefined } | undefined): boolean => trace !== undefined && (trace.location === undefined || !samePath(trace.location, ours)); + const where = (trace: { location: string | undefined } | undefined): string => trace?.location ?? 'no location recorded'; const foreign = [ - ...(traces.uninstallLocation !== undefined && !samePath(traces.uninstallLocation, ours) ? [`${UNINSTALL_KEY} (installed at ${traces.uninstallLocation})`] : []), - ...(traces.rememberedDir !== undefined && !samePath(traces.rememberedDir, ours) ? [`${REMEMBERED_DIR_KEY} (${traces.rememberedDir})`] : []), + ...(elsewhere(traces.uninstall) ? [`${UNINSTALL_KEY} (installed at ${where(traces.uninstall)})`] : []), + ...(elsewhere(traces.remembered) ? [`${REMEMBERED_DIR_KEY} (${where(traces.remembered)})`] : []), ...traces.shortcuts.filter((link) => !samePath(link.target, executable(ctx))).map((link) => `${link.path} (to ${link.target})`), ]; if (foreign.length > 0) throw new Error(`${PRODUCT} is already installed for this user outside this run's test root; uninstall it before running: ${foreign.join('; ')}`); const removed: string[] = []; - if (traces.uninstallLocation !== undefined) { + if (traces.uninstall !== undefined) { await deleteRegistryKey(UNINSTALL_KEY); removed.push(UNINSTALL_KEY); } - if (traces.rememberedDir !== undefined) { + if (traces.remembered !== undefined) { await deleteRegistryKey(REMEMBERED_DIR_KEY); removed.push(REMEMBERED_DIR_KEY); } diff --git a/examples/tauri-smoke/qa/lifecycle.ts b/examples/tauri-smoke/qa/lifecycle.ts index 463242d..7d0ccb9 100644 --- a/examples/tauri-smoke/qa/lifecycle.ts +++ b/examples/tauri-smoke/qa/lifecycle.ts @@ -29,12 +29,13 @@ import { async function install(ctx: RunContext): Promise { if (ctx.artifact === undefined) throw new Error('this lifecycle installs the candidate artifact, and the run has none'); + const dir = installDir(ctx); + // Files already there are not this run's: owning the directory would let cleanup delete them. Checked before + // recovery, so an install that is still there keeps its uninstall entry and shortcuts. + if (existsSync(dir)) throw new Error(`${dir} already exists and is not this run's; remove it before running`); // An install this run did not make cannot be owned, and installing over it would take over its entries; what an // earlier run in this same root left behind after dying is recognised by where it points, and removed. if (windows) await recoverInstallerTraces(ctx); - const dir = installDir(ctx); - // Files already there are not this run's: owning the directory would let cleanup delete them. - if (existsSync(dir)) throw new Error(`${dir} already exists and is not this run's; remove it before running`); await ctx.own({ kind: 'path', path: dir, label: 'installed app' }); if (windows) await runToEnd(ctx, 'installer', ctx.artifact.path, ['/S', `/D=${dir}`]); else await runToEnd(ctx, 'unpack', 'dpkg-deb', ['-x', ctx.artifact.path, dir]); diff --git a/packages/qa/src/drivers/tauri.ts b/packages/qa/src/drivers/tauri.ts index c2d12fd..d0d0ad4 100644 --- a/packages/qa/src/drivers/tauri.ts +++ b/packages/qa/src/drivers/tauri.ts @@ -53,17 +53,24 @@ async function goneWithin(pid: number, ms: number): Promise { return !alive(pid); } +export interface TakeOverChecks { + /** Whether `pid` is still below this run's driver chain; a pid that is not may have been reused by another process. */ + stillMine: (pid: number) => Promise; + /** Reads a process's identity; default `processIdentity`. */ + identityOf?: (pid: number) => Promise; +} + /** * Makes processes the run started indirectly its own, so the runner reaps them. A process that can no longer be owned * (the phase that started it was cut off) or that is alive but cannot be identified is stopped instead, and this * throws: a process nobody owns must not be left running. One that exits while being identified needs nothing. + * + * A pid is only signalled while it is still the process that was found: one that could not be owned must still have + * the identity read for it, and one that could never be identified must still be below the driver chain. A pid that + * fails its check now belongs to someone else, so the process that was found has exited and there is nothing to stop. */ -export async function takeOver( - ctx: Pick, - pids: readonly number[], - label: string, - identityOf: (pid: number) => Promise = processIdentity, -): Promise { +export async function takeOver(ctx: Pick, pids: readonly number[], label: string, checks: TakeOverChecks): Promise { + const identityOf = checks.identityOf ?? processIdentity; const problems: string[] = []; const stop = async (pid: number, why: string): Promise => { try { @@ -75,15 +82,21 @@ export async function takeOver( problems.push(`${label} ${pid} ${why}, so it was stopped`); }; for (const pid of pids) { - const identity = await identityOf(pid); + // Identifying a process that has just started can fail for a moment; keep trying while it runs. + let identity = await identityOf(pid); + const end = Date.now() + 2000; + while (identity === undefined && alive(pid) && Date.now() < end) { + await sleep(100); + identity = await identityOf(pid); + } if (identity === undefined) { - if (!(await goneWithin(pid, 2000))) await stop(pid, 'could not be identified'); + if (alive(pid) && (await checks.stillMine(pid))) await stop(pid, 'could not be identified'); continue; } try { await ctx.own({ kind: 'process', pid, identity, label }); } catch (error) { - await stop(pid, `could not be owned (${message(error)})`); + if ((await identityOf(pid)) === identity) await stop(pid, `could not be owned (${message(error)})`); } } if (problems.length > 0) throw new Error(problems.join('; ')); @@ -94,6 +107,7 @@ export class TauriApp { readonly #options: Required> & { tauriDriver: string }; #driverPid = 0; #browser: Browser | undefined; + readonly #checks: TakeOverChecks = { stillMine: async (pid) => (await descendantsOf(this.#driverPid)).includes(pid) }; private constructor(ctx: RunContext, options: TauriAppOptions) { this.#ctx = ctx; @@ -134,7 +148,7 @@ export class TauriApp { driver.off('exit', exited); } // The native driver is tauri-driver's child, not the run's; own it so it cannot outlive the run. - await takeOver(ctx, await descendantsOf(app.#driverPid), 'native driver'); + await takeOver(ctx, await descendantsOf(app.#driverPid), 'native driver', app.#checks); await app.#open(); return app; } @@ -195,7 +209,7 @@ export class TauriApp { if (failure === undefined && launched.length === 0) { failure = new Error(`the session started, but no ${this.#options.application} was found below this run's tauri-driver, so it could not be owned`); } - await takeOver(this.#ctx, launched, 'application'); + await takeOver(this.#ctx, launched, 'application', this.#checks); } catch (error) { failure ??= error; } diff --git a/packages/qa/test/drivers/tauri.test.ts b/packages/qa/test/drivers/tauri.test.ts index ce51e2a..269c518 100644 --- a/packages/qa/test/drivers/tauri.test.ts +++ b/packages/qa/test/drivers/tauri.test.ts @@ -63,28 +63,54 @@ describe('taking over what the driver chain started', () => { const ctx = (refuse = false) => ({ own: async (resource: OwnedResource) => { if (refuse) throw new Error('this run has been stopped'); owned.push(resource); } }); afterEach(() => { owned.length = 0; }); + const mine = async () => true; + test('records each process with its identity, so the runner can reap it', async () => { const child = sleeper(); - await takeOver(ctx(), [child.pid as number], 'application'); + await takeOver(ctx(), [child.pid as number], 'application', { stillMine: mine }); expect(owned).toEqual([{ kind: 'process', pid: child.pid, identity: await processIdentity(child.pid as number), label: 'application' }]); }); test('a process the run can no longer own is stopped, and the start fails saying so', async () => { const child = sleeper(); - await expect(takeOver(ctx(true), [child.pid as number], 'application')).rejects.toThrow(/could not be owned.*stopped/); + await expect(takeOver(ctx(true), [child.pid as number], 'application', { stillMine: mine })).rejects.toThrow(/could not be owned.*stopped/); await eventually(() => !isAlive(child.pid as number)); }); + test('a pid that now belongs to another process is not signalled, even when owning it was refused', async () => { + const child = sleeper(); + let reads = 0; + // The first read is the process that was found; later reads see a different one under the same pid. + const identityOf = async () => (reads++ === 0 ? 'the process that was found' : 'another process'); + await expect(takeOver(ctx(true), [child.pid as number], 'application', { stillMine: mine, identityOf })).resolves.toBeUndefined(); + expect(isAlive(child.pid as number)).toBe(true); + }); + test('a live process that cannot be identified is stopped, not silently left unowned', async () => { const child = sleeper(); - await expect(takeOver(ctx(), [child.pid as number], 'application', async () => undefined)).rejects.toThrow(/could not be identified.*stopped/); + await expect(takeOver(ctx(), [child.pid as number], 'application', { stillMine: mine, identityOf: async () => undefined })).rejects.toThrow(/could not be identified.*stopped/); await eventually(() => !isAlive(child.pid as number)); expect(owned).toEqual([]); }); + test('an unidentifiable pid that is no longer below the driver chain is not signalled', async () => { + const child = sleeper(); + await expect(takeOver(ctx(), [child.pid as number], 'application', { stillMine: async () => false, identityOf: async () => undefined })).resolves.toBeUndefined(); + expect(isAlive(child.pid as number)).toBe(true); + }); + + test('one that can be identified after a moment is owned, not stopped', async () => { + const child = sleeper(); + let reads = 0; + const identityOf = async (pid: number) => (reads++ < 2 ? undefined : processIdentity(pid)); + await takeOver(ctx(), [child.pid as number], 'application', { stillMine: mine, identityOf }); + expect(owned).toMatchObject([{ kind: 'process', pid: child.pid, label: 'application' }]); + expect(isAlive(child.pid as number)).toBe(true); + }); + test('one that exits while being identified needs nothing', async () => { const child = sleeper(); setTimeout(() => child.kill(), 200); - await expect(takeOver(ctx(), [child.pid as number], 'application', async () => undefined)).resolves.toBeUndefined(); + await expect(takeOver(ctx(), [child.pid as number], 'application', { stillMine: mine, identityOf: async () => undefined })).resolves.toBeUndefined(); }); });