diff --git a/.github/actions/eng-metrics/.env.example b/.github/actions/eng-metrics/.env.example
new file mode 100644
index 0000000000..26dfd29b6c
--- /dev/null
+++ b/.github/actions/eng-metrics/.env.example
@@ -0,0 +1,17 @@
+# GitHub API token with repo scope
+GITHUB_TOKEN=your_github_token_here
+
+# Repositories to track (comma-separated list)
+# REPOSITORIES=octocat/Hello-World,example-org/example-repo
+
+# BigQuery dataset and table IDs (optional, defaults in config.json)
+# BIGQUERY_DATASET_ID=github_metrics
+
+# Path to the service account key file (optional, default in config.json)
+# SERVICE_ACCOUNT_KEY_PATH=./service-account-key.json
+
+# Target branch to track PRs for (optional, default: main)
+# TARGET_BRANCH=main
+
+# Print-only mode (set to 'true' to print metrics to console instead of uploading to BigQuery)
+# PRINT_ONLY=true
diff --git a/.github/actions/eng-metrics/.nvmrc b/.github/actions/eng-metrics/.nvmrc
new file mode 100644
index 0000000000..d4b7699d36
--- /dev/null
+++ b/.github/actions/eng-metrics/.nvmrc
@@ -0,0 +1 @@
+20.18.1
diff --git a/.github/actions/eng-metrics/README.md b/.github/actions/eng-metrics/README.md
new file mode 100644
index 0000000000..01db06444f
--- /dev/null
+++ b/.github/actions/eng-metrics/README.md
@@ -0,0 +1,308 @@
+# Engineering metrics collector
+
+A comprehensive tool to collect GitHub engineering metrics and upload them to BigQuery. This tool can be used as a standalone application or as a GitHub Action to track various development workflow metrics.
+
+## Supported Metrics
+
+```mermaid
+---
+title: Software development life cycle (SDLC)
+---
+graph TD
+ requirements[Understand the why]
+ design[Design solution]
+ implement[Implement code]
+
+ subgraph Time_to_Merge["⏱️ Time to Merge"]
+ subgraph Time_to_First_Review["⏱️ Time to First Review"]
+ pr_ready_for_review[PR ready for review]
+ first_review[Receive first review]
+ end
+ first_review --> rework["Changes requested &
re-review (optional)"]
+ rework --> code_approved[Code approved]
+ code_approved --> pr_merged[Merge pull request]
+ end
+
+ testing[QA]
+ ready_for_release[Ready for release]
+ release[Release]
+ monitoring[Monitoring & feedback]
+
+ requirements --> design
+ design --> implement
+ implement --> pr_ready_for_review
+ pr_ready_for_review --> first_review
+ pr_merged --> testing
+ testing --> ready_for_release
+ ready_for_release --> release
+ release --> monitoring
+```
+
+This tool collects the following engineering metrics:
+
+### 1. Time to First Review
+Previously known as "PR Pickup Time" - measures the time between when a PR is marked as "Ready for Review" and when a reviewer has submitted a review.
+
+- **Start Time**: When a PR is marked as "Ready for Review" - this can be:
+ - When a PR is created as a non-draft PR
+ - When a draft PR is converted to ready for review
+ - If multiple ready_for_review events exist, the tool uses the most recent one that occurred before the first review
+- **End Time**: When the first review submission occurs (comment, approval, or changes requested)
+- **Metric**: The time difference between these two events, excluding weekends
+
+### 2. Time to Merge
+Measures the time from when a PR is marked as "Ready for Review" to when it is merged.
+
+- **Start Time**: When a PR is marked as "Ready for Review"
+- **End Time**: When the PR is merged into the target branch
+- **Metric**: The time difference between these two events, excluding weekends
+
+### 3. User Group Management
+Extracts GitHub usernames from a product groups markdown file and syncs them to BigQuery for team analytics.
+
+- **Source**: Markdown file containing product group tables with GitHub usernames
+- **Processing**: Validates GitHub usernames and creates dual group membership (specific group + engineering)
+- **Storage**: Syncs to BigQuery `user_group` table with differential updates
+- **Groups**: Supports MDM, Orchestration, and Software groups with automatic engineering group inclusion
+
+### 4. Time to QA Ready (Planned)
+Measures the time from issue reaching "In Progress" status to when it reaches "Awaiting QA" status in GitHub Projects.
+
+- **Start Time**: When an issue is moved to "In Progress"
+- **End Time**: When the issue status changes to "Awaiting QA" or "Ready for release" in GitHub Projects
+- **Metric**: The time difference between these two events, excluding weekends
+
+### 5. Time to Production Ready (Planned)
+Measures the time from issue reaching "In Progress" status to when it reaches "Ready for Release" status in GitHub Projects.
+
+- **Start Time**: When an issue is moved to "In Progress"
+- **End Time**: When the issue status changes to "Ready for Release" in GitHub Projects
+- **Metric**: The time difference between these two events, excluding weekends
+
+## Current Implementation Status
+
+- ✅ **Time to First Review**: Fully implemented and active
+- ✅ **Time to Merge**: Fully implemented and active
+- ✅ **User Group Management**: Fully implemented and active
+- 🚧 **Time to QA Ready**: Planned for future implementation
+- 🚧 **Time to Production Ready**: Planned for future implementation
+
+## Features
+
+- Collects engineering metrics from GitHub repositories
+- Uploads metrics to Google BigQuery for analysis
+- Configurable via JSON file and environment variables
+- Can run as a standalone application or as a GitHub Action
+- Supports multiple repositories (not tested)
+- Only tracks PRs targeting the main branch
+- Excludes weekends from time calculations
+- Supports print-only mode for testing without BigQuery
+
+## Prerequisites
+
+- Node.js 20 or higher
+- A GitHub token with `public_repo` scope or `repo` scope (for private repos)
+- A Google Cloud project with BigQuery enabled
+- A Google Cloud service account with BigQuery permissions
+
+## Installation
+
+```bash
+# Clone the repo and go here
+git clone
+cd /.github/actions/eng-metrics
+
+# Install dependencies
+npm install
+```
+
+## Configuration
+
+### Configuration File
+
+Create a `config.json` file with the following structure:
+
+```json
+{
+ "repositories": [
+ "owner/repo1",
+ "owner/repo2"
+ ],
+ "targetBranch": "main",
+ "bigQueryDatasetId": "github_metrics",
+ "lookbackDays": 5,
+ "serviceAccountKeyPath": "./service-account-key.json",
+ "printOnly": false,
+ "userGroupEnabled": true,
+ "userGroupFilepath": "../../../handbook/company/product-groups.md",
+ "metrics": {
+ "timeToFirstReview": {
+ "enabled": true,
+ "tableName": "pr_first_review"
+ },
+ "timeToMerge": {
+ "enabled": true,
+ "tableName": "pr_merge"
+ }
+ }
+}
+```
+
+### Environment Variables
+
+You can also configure the tool using environment variables:
+
+- `GITHUB_TOKEN`: GitHub token with repo scope
+- `REPOSITORIES`: Comma-separated list of repositories to track (optional, overrides config.json)
+- `BIGQUERY_DATASET_ID`: BigQuery dataset ID (optional, defaults to config.json)
+- `SERVICE_ACCOUNT_KEY_PATH`: Path to the service account key file (optional, overrides config.json)
+- `TARGET_BRANCH`: Target branch to track PRs for (optional, default: main)
+- `PRINT_ONLY`: Set to 'true' to print metrics to console instead of uploading to BigQuery
+- `ENABLED_METRICS`: Comma-separated list of metrics to collect (e.g., "time_to_first_review,time_to_merge")
+- `TIME_TO_FIRST_REVIEW_TABLE`: Override table name for Time to First Review metrics (optional, defaults to "pr_first_review")
+- `TIME_TO_MERGE_TABLE`: Override table name for Time to Merge metrics (optional, defaults to "pr_merge")
+- `USER_GROUP_ENABLED`: Set to 'true' to enable user group processing (optional, defaults to false)
+- `USER_GROUP_FILEPATH`: Path to the product groups markdown file (optional, defaults to "../../../handbook/company/product-groups.md")
+
+Create a `.env` file based on the provided `.env.example` to set these variables.
+
+### Configuration Priority
+
+The tool uses the following configuration priority order (highest priority overrides lower priority):
+
+1. **Environment Variables** (highest priority) - Values from `.env` file or system environment
+2. **JSON Configuration File** (medium priority) - Values from `config.json` or specified config file
+3. **Default Configuration** (lowest priority) - Built-in default values
+
+This means that environment variables will always override values in the JSON configuration file, and both will override any default values. For example, if you have `"printOnly": false` in your `config.json` file but set `PRINT_ONLY=true` in your `.env` file, the tool will run in print-only mode.
+
+## Usage
+
+### As a Standalone Application
+
+```bash
+# Run with default config.json
+npm start
+
+# Run with a custom config file
+npm start -- path/to/config.json
+
+# Run in print-only mode (no BigQuery upload)
+npm start -- --print-only
+
+# Run with a custom config file in print-only mode
+npm start -- path/to/config.json --print-only
+```
+
+### As a GitHub Action
+
+See example in .github/workflows/collect-eng-metrics.yml
+
+#### How the GitHub Action Works
+
+1. **Service Account Key Handling**:
+ - The workflow writes the `ENG_METRICS_GCP_SERVICE_ACCOUNT_KEY` secret directly to a file
+ - It verifies that the file contains valid JSON using `jq`
+ - It then sets the `SERVICE_ACCOUNT_KEY_PATH` environment variable to point to this file
+ - The application uses this environment variable to locate the service account key file
+
+ **Important**: The service account key should be stored as a JSON string in the GitHub secret. Copy the entire contents of your service account key JSON file to the secret value.
+
+2. **Configuration**:
+ - The workflow passes configuration values as environment variables
+ - Environment variables are used directly by the application
+
+Make sure to set the following secrets in your repository:
+
+- `GITHUB_TOKEN`: GitHub token with repo scope (automatically provided by GitHub Actions)
+- `ENG_METRICS_GCP_SERVICE_ACCOUNT_KEY`: JSON service account key as a string
+
+## BigQuery Schema
+
+### Multi-Table Architecture
+
+The tool uses separate BigQuery tables for different metric types to optimize performance and enable independent analysis:
+
+#### Table 1: `pr_first_review` (Time to First Review)
+
+| Field | Type | Description |
+|---------------------|-----------|------------------------------------------------------------------------------|
+| review_date | DATE | Date when the reviewer started looking at the PR |
+| pr_creator | STRING | GitHub username of the PR creator (cluster key) |
+| pr_url | STRING | HTTP link to the PR |
+| pickup_time_seconds | INTEGER | Time in seconds from "Ready for Review" to first review (excluding weekends) |
+| repository | STRING | Repository name (owner/repo) |
+| pr_number | INTEGER | PR number (cluster key) |
+| target_branch | STRING | Branch the PR is targeting (always "main") |
+| ready_time | TIMESTAMP | Timestamp when PR was marked ready for review |
+| first_review_time | TIMESTAMP | Timestamp of first review activity (partition key) |
+
+#### Table 2: `pr_merge` (Time to Merge)
+
+| Field | Type | Description |
+|--------------------|-----------|-----------------------------------------------------------------------|
+| merge_date | DATE | Date when the PR was merged |
+| pr_creator | STRING | GitHub username of the PR creator (cluster key) |
+| pr_url | STRING | HTTP link to the PR |
+| merge_time_seconds | INTEGER | Time in seconds from "Ready for Review" to merge (excluding weekends) |
+| repository | STRING | Repository name (owner/repo) |
+| pr_number | INTEGER | PR number (cluster key) |
+| target_branch | STRING | Branch the PR is targeting (always "main") |
+| ready_time | TIMESTAMP | Timestamp when PR was marked ready for review |
+| merge_time | TIMESTAMP | Timestamp when PR was merged (partition key) |
+
+#### Table 3: `user_group` (User Group Management)
+
+| Field | Type | Description |
+|----------|--------|----------------------------------------------------------------------|
+| group | STRING | Group name (mdm, orchestration, software, engineering) - cluster key |
+| username | STRING | GitHub username |
+
+**Table Optimizations:**
+- `user_group` table is clustered by `group` for efficient group-based queries
+- Supports dual group membership (specific group + engineering group for all users)
+- Uses differential sync to minimize BigQuery operations
+- Records are managed through intelligent insert/delete operations
+- Due to BigQuery buffering, records cannot be updated/deleted within a 90-minute timeframe. This is OK as this update should run once per day.
+
+**Multi-Table Optimizations:**
+- `pr_first_review` table is partitioned by `DATE(first_review_time)` for efficient date-range queries
+- `pr_merge` table is partitioned by `DATE(merge_time)` for efficient date-range queries
+- `pr_first_review` and `pr_merge` tables are clustered by `pr_creator` and `pr_number` for efficient user-based analysis
+- Each table uses `pr_number` as unique identifier (enforced at application level)
+- Records are insert-only (no updates) to preserve historical data integrity
+
+## Print-Only Mode
+
+The tool supports a print-only mode that prints metrics to the console instead of uploading them to BigQuery. This is useful for testing without setting up BigQuery.
+
+To enable print-only mode:
+
+1. Set `printOnly: true` in your config.json file, OR
+2. Set the `PRINT_ONLY=true` environment variable, OR
+3. Use the `--print-only` command line flag
+
+When running in print-only mode, you don't need to provide BigQuery credentials or configuration.
+
+## Development
+
+### Running Tests
+
+```bash
+npm test
+```
+
+### Linting
+
+```bash
+npm run lint
+```
+
+## Contributing
+
+When contributing to this project, please:
+
+1. Update tests for any new metrics or functionality
+2. Update documentation to reflect changes
+3. Follow the existing code style and patterns
+4. Consider backward compatibility for BigQuery schema changes
diff --git a/.github/actions/eng-metrics/action.yml b/.github/actions/eng-metrics/action.yml
new file mode 100644
index 0000000000..47899de5e1
--- /dev/null
+++ b/.github/actions/eng-metrics/action.yml
@@ -0,0 +1,10 @@
+name: 'Engineering metrics collector'
+description: 'Collects comprehensive GitHub engineering metrics including time to first review, time to merge, and GitHub Projects workflow metrics, then uploads them to BigQuery for analysis'
+
+runs:
+ using: 'node20'
+ main: 'src/index.js'
+
+branding:
+ icon: 'bar-chart-2'
+ color: 'blue'
diff --git a/.github/actions/eng-metrics/config.json b/.github/actions/eng-metrics/config.json
new file mode 100644
index 0000000000..0689f2c584
--- /dev/null
+++ b/.github/actions/eng-metrics/config.json
@@ -0,0 +1,22 @@
+{
+ "repositories": [
+ "fleetdm/fleet"
+ ],
+ "targetBranch": "main",
+ "bigQueryDatasetId": "github_metrics",
+ "lookbackDays": 5,
+ "serviceAccountKeyPath": "./service-account-key.json",
+ "printOnly": false,
+ "userGroupEnabled": true,
+ "userGroupFilepath": "../../../handbook/company/product-groups.md",
+ "metrics": {
+ "timeToFirstReview": {
+ "enabled": true,
+ "tableName": "pr_first_review"
+ },
+ "timeToMerge": {
+ "enabled": true,
+ "tableName": "pr_merge"
+ }
+ }
+}
diff --git a/.github/actions/eng-metrics/eslint.config.js b/.github/actions/eng-metrics/eslint.config.js
new file mode 100644
index 0000000000..c20e213b64
--- /dev/null
+++ b/.github/actions/eng-metrics/eslint.config.js
@@ -0,0 +1,53 @@
+import js from '@eslint/js';
+
+export default [
+ js.configs.recommended,
+ {
+ languageOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ globals: {
+ console: 'readonly',
+ process: 'readonly',
+ Buffer: 'readonly',
+ __dirname: 'readonly',
+ __filename: 'readonly',
+ global: 'readonly',
+ setTimeout: 'readonly',
+ clearTimeout: 'readonly',
+ setInterval: 'readonly',
+ clearInterval: 'readonly'
+ }
+ },
+ rules: {
+ 'indent': ['error', 2],
+ 'linebreak-style': ['error', 'unix'],
+ 'quotes': ['error', 'single'],
+ 'semi': ['error', 'always'],
+ 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
+ 'no-console': 'off',
+ 'prefer-const': 'error',
+ 'no-var': 'error',
+ 'object-shorthand': 'error',
+ 'prefer-arrow-callback': 'error'
+ }
+ },
+ {
+ files: ['test/**/*.js'],
+ languageOptions: {
+ globals: {
+ describe: 'readonly',
+ test: 'readonly',
+ expect: 'readonly',
+ beforeEach: 'readonly',
+ afterEach: 'readonly',
+ beforeAll: 'readonly',
+ afterAll: 'readonly',
+ jest: 'readonly'
+ }
+ },
+ rules: {
+ 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_' }]
+ }
+ }
+];
\ No newline at end of file
diff --git a/.github/actions/eng-metrics/jest.config.js b/.github/actions/eng-metrics/jest.config.js
new file mode 100644
index 0000000000..a792b8f2c2
--- /dev/null
+++ b/.github/actions/eng-metrics/jest.config.js
@@ -0,0 +1,41 @@
+/**
+ * Jest configuration for engineering metrics tests
+ */
+
+export default {
+ // Test environment
+ testEnvironment: "node",
+
+ // Transform configuration for ES modules
+ transform: {},
+
+ // File extensions to consider
+ moduleFileExtensions: ["js", "json"],
+
+ // Test file patterns
+ testMatch: ["**/test/**/*.test.js"],
+
+ // Coverage configuration
+ collectCoverage: false, // Disable for now to focus on functionality
+ coverageDirectory: "coverage",
+ coverageReporters: ["text", "lcov", "html"],
+
+ // Files to collect coverage from
+ collectCoverageFrom: [
+ "src/**/*.js",
+ "!src/index.js", // Exclude main entry point
+ "!src/logger.js", // Exclude logger (simple utility)
+ ],
+
+ // Setup files
+ setupFilesAfterEnv: ["/test/setup.js"],
+
+ // Clear mocks between tests
+ clearMocks: true,
+
+ // Restore mocks after each test
+ restoreMocks: true,
+
+ // Verbose output
+ verbose: true,
+};
diff --git a/.github/actions/eng-metrics/package-lock.json b/.github/actions/eng-metrics/package-lock.json
new file mode 100644
index 0000000000..ba9adfafe8
--- /dev/null
+++ b/.github/actions/eng-metrics/package-lock.json
@@ -0,0 +1,6571 @@
+{
+ "name": "engineering-metrics-collector",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "engineering-metrics-collector",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@google-cloud/bigquery": "^8.1.0",
+ "dotenv": "^17.0.0",
+ "octokit": "^5.0.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.30.0",
+ "eslint": "^9.30.0",
+ "jest": "^30.0.3"
+ },
+ "engines": {
+ "node": "20.18.1"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz",
+ "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz",
+ "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.6",
+ "@babel/parser": "^7.27.7",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.27.7",
+ "@babel/types": "^7.27.7",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
+ "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.5",
+ "@babel/types": "^7.27.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz",
+ "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz",
+ "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.5",
+ "@babel/parser": "^7.27.7",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.7",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz",
+ "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
+ "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.0.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
+ "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
+ "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
+ "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.30.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz",
+ "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
+ "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
+ "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@google-cloud/bigquery": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-8.1.0.tgz",
+ "integrity": "sha512-eDleD/IHKQIRm4GmMnwJvPkx4PgSaK8m8DCmDmVOf0gIhqPLSdvOAEeM4QjyyZGUGjV4yHyJfEJxzULTzl22Aw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@google-cloud/common": "^6.0.0",
+ "@google-cloud/paginator": "^6.0.0",
+ "@google-cloud/precise-date": "^5.0.0",
+ "@google-cloud/promisify": "^5.0.0",
+ "arrify": "^3.0.0",
+ "big.js": "^6.2.2",
+ "duplexify": "^4.1.3",
+ "extend": "^3.0.2",
+ "is": "^3.3.0",
+ "stream-events": "^1.0.5",
+ "teeny-request": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google-cloud/common": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-6.0.0.tgz",
+ "integrity": "sha512-IXh04DlkLMxWgYLIUYuHHKXKOUwPDzDgke1ykkkJPe48cGIS9kkL2U/o0pm4ankHLlvzLF/ma1eO86n/bkumIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@google-cloud/projectify": "^4.0.0",
+ "@google-cloud/promisify": "^4.0.0",
+ "arrify": "^2.0.0",
+ "duplexify": "^4.1.3",
+ "extend": "^3.0.2",
+ "google-auth-library": "^10.0.0-rc.1",
+ "html-entities": "^2.5.2",
+ "retry-request": "^8.0.0",
+ "teeny-request": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google-cloud/common/node_modules/@google-cloud/promisify": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.1.0.tgz",
+ "integrity": "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google-cloud/common/node_modules/arrify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@google-cloud/paginator": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-6.0.0.tgz",
+ "integrity": "sha512-g5nmMnzC+94kBxOKkLGpK1ikvolTFCC3s2qtE4F+1EuArcJ7HHC23RDQVt3Ra3CqpUYZ+oXNKZ8n5Cn5yug8DA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google-cloud/precise-date": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-5.0.0.tgz",
+ "integrity": "sha512-9h0Gvw92EvPdE8AK8AgZPbMnH5ftDyPtKm7/KUfcJVaPEPjwGDsJd1QV0H8esBDV4II41R/2lDWH1epBqIoKUw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google-cloud/projectify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
+ "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@google-cloud/promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-5.0.0.tgz",
+ "integrity": "sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.2.tgz",
+ "integrity": "sha512-krGElPU0FipAqpVZ/BRZOy0MZh/ARdJ0Nj+PiH1ykFY1+VpBlYNLjdjVA5CFKxnKR6PFqFutO4Z7cdK9BlGiDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "jest-message-util": "30.0.2",
+ "jest-util": "30.0.2",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.3.tgz",
+ "integrity": "sha512-Mgs1N+NSHD3Fusl7bOq1jyxv1JDAUwjy+0DhVR93Q6xcBP9/bAQ+oZhXb5TTnP5sQzAHgb7ROCKQ2SnovtxYtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.2",
+ "@jest/pattern": "30.0.1",
+ "@jest/reporters": "30.0.2",
+ "@jest/test-result": "30.0.2",
+ "@jest/transform": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "exit-x": "^0.2.2",
+ "graceful-fs": "^4.2.11",
+ "jest-changed-files": "30.0.2",
+ "jest-config": "30.0.3",
+ "jest-haste-map": "30.0.2",
+ "jest-message-util": "30.0.2",
+ "jest-regex-util": "30.0.1",
+ "jest-resolve": "30.0.2",
+ "jest-resolve-dependencies": "30.0.3",
+ "jest-runner": "30.0.3",
+ "jest-runtime": "30.0.3",
+ "jest-snapshot": "30.0.3",
+ "jest-util": "30.0.2",
+ "jest-validate": "30.0.2",
+ "jest-watcher": "30.0.2",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.0.2",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/diff-sequences": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
+ "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz",
+ "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "jest-mock": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz",
+ "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "30.0.3",
+ "jest-snapshot": "30.0.3"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz",
+ "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz",
+ "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.0.2",
+ "jest-mock": "30.0.2",
+ "jest-util": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/get-type": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz",
+ "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz",
+ "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.2",
+ "@jest/expect": "30.0.3",
+ "@jest/types": "30.0.1",
+ "jest-mock": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/pattern": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
+ "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-regex-util": "30.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.2.tgz",
+ "integrity": "sha512-l4QzS/oKf57F8WtPZK+vvF4Io6ukplc6XgNFu4Hd/QxaLEO9f+8dSFzUua62Oe0HKlCUjKHpltKErAgDiMJKsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "30.0.2",
+ "@jest/test-result": "30.0.2",
+ "@jest/transform": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "collect-v8-coverage": "^1.0.2",
+ "exit-x": "^0.2.2",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^5.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "30.0.2",
+ "jest-util": "30.0.2",
+ "jest-worker": "30.0.2",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.2",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
+ "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/snapshot-utils": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz",
+ "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "natural-compare": "^1.4.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz",
+ "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "callsites": "^3.1.0",
+ "graceful-fs": "^4.2.11"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.2.tgz",
+ "integrity": "sha512-KKMuBKkkZYP/GfHMhI+cH2/P3+taMZS3qnqqiPC1UXZTJskkCS+YU/ILCtw5anw1+YsTulDHFpDo70mmCedW8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "collect-v8-coverage": "^1.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.2.tgz",
+ "integrity": "sha512-fbyU5HPka0rkalZ3MXVvq0hwZY8dx3Y6SCqR64zRmh+xXlDeFl0IdL4l9e7vp4gxEXTYHbwLFA1D+WW5CucaSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "30.0.2",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.2",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz",
+ "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@jest/types": "30.0.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "babel-plugin-istanbul": "^7.0.0",
+ "chalk": "^4.1.2",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.2",
+ "jest-regex-util": "30.0.1",
+ "jest-util": "30.0.2",
+ "micromatch": "^4.0.8",
+ "pirates": "^4.0.7",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^5.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
+ "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.1",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
+ "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.9.0"
+ }
+ },
+ "node_modules/@octokit/app": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.0.1.tgz",
+ "integrity": "sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-app": "^8.0.1",
+ "@octokit/auth-unauthenticated": "^7.0.1",
+ "@octokit/core": "^7.0.2",
+ "@octokit/oauth-app": "^8.0.1",
+ "@octokit/plugin-paginate-rest": "^13.0.0",
+ "@octokit/types": "^14.0.0",
+ "@octokit/webhooks": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-app": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz",
+ "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^9.0.1",
+ "@octokit/auth-oauth-user": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "toad-cache": "^3.7.0",
+ "universal-github-app-jwt": "^2.2.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz",
+ "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.1",
+ "@octokit/auth-oauth-user": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz",
+ "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-methods": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz",
+ "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.1",
+ "@octokit/oauth-methods": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz",
+ "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz",
+ "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^6.0.0",
+ "@octokit/graphql": "^9.0.1",
+ "@octokit/request": "^10.0.2",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "before-after-hook": "^4.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz",
+ "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz",
+ "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^9.0.1",
+ "@octokit/auth-oauth-user": "^6.0.0",
+ "@octokit/auth-unauthenticated": "^7.0.1",
+ "@octokit/core": "^7.0.2",
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/oauth-methods": "^6.0.0",
+ "@types/aws-lambda": "^8.10.83",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-authorization-url": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz",
+ "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz",
+ "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/openapi-webhooks-types": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.0.3.tgz",
+ "integrity": "sha512-90MF5LVHjBedwoHyJsgmaFhEN1uzXyBDRLEBe7jlTYx/fEhPAk3P3DAJsfZwC54m8hAIryosJOL+UuZHB3K3yA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-graphql": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz",
+ "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.0.tgz",
+ "integrity": "sha512-16iNOa4rTTjaWtfsPGJcYYL79FJakseX8TQFIPfVuSPC3s5nkS/DSNQPFPc5lJHgEDBWNMxSApHrEymNblhA9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.1.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz",
+ "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.1.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-retry": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.1.tgz",
+ "integrity": "sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=7"
+ }
+ },
+ "node_modules/@octokit/plugin-throttling": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.1.tgz",
+ "integrity": "sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": "^7.0.0"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz",
+ "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/webhooks": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.0.2.tgz",
+ "integrity": "sha512-16TtZXNOfH8RaRsV+iag5dTYeJvdOdZDBcpEPCULdKS3eTRJqAYxBNZPFaDJ3cx3WNyvbaQ0IxsPpnaR/tgGFA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-webhooks-types": "12.0.3",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/webhooks-methods": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/webhooks-methods": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz",
+ "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
+ "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.37",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
+ "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
+ "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aws-lambda": {
+ "version": "8.10.150",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.150.tgz",
+ "integrity": "sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/caseless": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
+ "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.0.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.6.tgz",
+ "integrity": "sha512-ZOyn+gOs749xU7ovp+Ibj0g1o3dFRqsfPnT22C2t5JzcRvgsEDpGawPbCISGKLudJk9Y0wiu9sYd6kUh0pc9TA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
+ "node_modules/@types/request": {
+ "version": "2.48.12",
+ "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
+ "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/caseless": "*",
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "form-data": "^2.5.0"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.33",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz",
+ "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz",
+ "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz",
+ "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz",
+ "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz",
+ "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz",
+ "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz",
+ "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz",
+ "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz",
+ "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz",
+ "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz",
+ "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz",
+ "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz",
+ "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz",
+ "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz",
+ "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz",
+ "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz",
+ "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz",
+ "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz",
+ "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/arrify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz",
+ "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/babel-jest": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.2.tgz",
+ "integrity": "sha512-A5kqR1/EUTidM2YC2YMEUDP2+19ppgOwK0IAd9Swc3q2KqFb5f9PtRUXVeZcngu0z5mDMyZ9zH2huJZSOMLiTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "30.0.2",
+ "@types/babel__core": "^7.20.5",
+ "babel-plugin-istanbul": "^7.0.0",
+ "babel-preset-jest": "30.0.1",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz",
+ "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-instrument": "^6.0.2",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz",
+ "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "@types/babel__core": "^7.20.5"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
+ "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz",
+ "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "30.0.1",
+ "babel-preset-current-node-syntax": "^1.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/before-after-hook": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/big.js": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz",
+ "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bigjs"
+ }
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
+ "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bottleneck": {
+ "version": "2.19.5",
+ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
+ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
+ "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001726",
+ "electron-to-chromium": "^1.5.173",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001726",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
+ "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
+ "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz",
+ "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexify": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+ "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.4.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1",
+ "stream-shift": "^1.0.2"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.177",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz",
+ "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
+ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.30.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz",
+ "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.0",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.30.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.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==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/exit-x": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
+ "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz",
+ "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "30.0.3",
+ "@jest/get-type": "30.0.1",
+ "jest-matcher-utils": "30.0.3",
+ "jest-message-util": "30.0.2",
+ "jest-mock": "30.0.2",
+ "jest-util": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz",
+ "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.35",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
+ "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "node-fetch": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
+ "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^7.0.0",
+ "google-logging-utils": "^1.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz",
+ "integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^7.0.0",
+ "gcp-metadata": "^7.0.0",
+ "google-logging-utils": "^1.0.0",
+ "gtoken": "^8.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
+ "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/gtoken": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
+ "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+ "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz",
+ "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jest": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.3.tgz",
+ "integrity": "sha512-Uy8xfeE/WpT2ZLGDXQmaYNzw2v8NUKuYeKGtkS6sDxwsdQihdgYCXaKIYnph1h95DN5H35ubFDm0dfmsQnjn4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "30.0.3",
+ "@jest/types": "30.0.1",
+ "import-local": "^3.2.0",
+ "jest-cli": "30.0.3"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz",
+ "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.1.1",
+ "jest-util": "30.0.2",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.3.tgz",
+ "integrity": "sha512-rD9qq2V28OASJHJWDRVdhoBdRs6k3u3EmBzDYcyuMby8XCO3Ll1uq9kyqM41ZcC4fMiPulMVh3qMw0cBvDbnyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.2",
+ "@jest/expect": "30.0.3",
+ "@jest/test-result": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "co": "^4.6.0",
+ "dedent": "^1.6.0",
+ "is-generator-fn": "^2.1.0",
+ "jest-each": "30.0.2",
+ "jest-matcher-utils": "30.0.3",
+ "jest-message-util": "30.0.2",
+ "jest-runtime": "30.0.3",
+ "jest-snapshot": "30.0.3",
+ "jest-util": "30.0.2",
+ "p-limit": "^3.1.0",
+ "pretty-format": "30.0.2",
+ "pure-rand": "^7.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.3.tgz",
+ "integrity": "sha512-UWDSj0ayhumEAxpYRlqQLrssEi29kdQ+kddP94AuHhZknrE+mT0cR0J+zMHKFe9XPfX3dKQOc2TfWki3WhFTsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "30.0.3",
+ "@jest/test-result": "30.0.2",
+ "@jest/types": "30.0.1",
+ "chalk": "^4.1.2",
+ "exit-x": "^0.2.2",
+ "import-local": "^3.2.0",
+ "jest-config": "30.0.3",
+ "jest-util": "30.0.2",
+ "jest-validate": "30.0.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.3.tgz",
+ "integrity": "sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@jest/get-type": "30.0.1",
+ "@jest/pattern": "30.0.1",
+ "@jest/test-sequencer": "30.0.2",
+ "@jest/types": "30.0.1",
+ "babel-jest": "30.0.2",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "deepmerge": "^4.3.1",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "jest-circus": "30.0.3",
+ "jest-docblock": "30.0.1",
+ "jest-environment-node": "30.0.2",
+ "jest-regex-util": "30.0.1",
+ "jest-resolve": "30.0.2",
+ "jest-runner": "30.0.3",
+ "jest-util": "30.0.2",
+ "jest-validate": "30.0.2",
+ "micromatch": "^4.0.8",
+ "parse-json": "^5.2.0",
+ "pretty-format": "30.0.2",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "esbuild-register": ">=3.4.0",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "esbuild-register": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz",
+ "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/diff-sequences": "30.0.1",
+ "@jest/get-type": "30.0.1",
+ "chalk": "^4.1.2",
+ "pretty-format": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz",
+ "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz",
+ "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.1",
+ "@jest/types": "30.0.1",
+ "chalk": "^4.1.2",
+ "jest-util": "30.0.2",
+ "pretty-format": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.2.tgz",
+ "integrity": "sha512-XsGtZ0H+a70RsxAQkKuIh0D3ZlASXdZdhpOSBq9WRPq6lhe0IoQHGW0w9ZUaPiZQ/CpkIdprvlfV1QcXcvIQLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.2",
+ "@jest/fake-timers": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "jest-mock": "30.0.2",
+ "jest-util": "30.0.2",
+ "jest-validate": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz",
+ "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "anymatch": "^3.1.3",
+ "fb-watchman": "^2.0.2",
+ "graceful-fs": "^4.2.11",
+ "jest-regex-util": "30.0.1",
+ "jest-util": "30.0.2",
+ "jest-worker": "30.0.2",
+ "micromatch": "^4.0.8",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.3"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz",
+ "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.1",
+ "pretty-format": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz",
+ "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.1",
+ "chalk": "^4.1.2",
+ "jest-diff": "30.0.3",
+ "pretty-format": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
+ "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.0.1",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.0.2",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz",
+ "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "jest-util": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz",
+ "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.2",
+ "jest-pnp-resolver": "^1.2.3",
+ "jest-util": "30.0.2",
+ "jest-validate": "30.0.2",
+ "slash": "^3.0.0",
+ "unrs-resolver": "^1.7.11"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.3.tgz",
+ "integrity": "sha512-FlL6u7LiHbF0Oe27k7DHYMq2T2aNpPhxnNo75F7lEtu4A6sSw+TKkNNUGNcVckdFoL0RCWREJsC1HsKDwKRZzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-regex-util": "30.0.1",
+ "jest-snapshot": "30.0.3"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.3.tgz",
+ "integrity": "sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.2",
+ "@jest/environment": "30.0.2",
+ "@jest/test-result": "30.0.2",
+ "@jest/transform": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "emittery": "^0.13.1",
+ "exit-x": "^0.2.2",
+ "graceful-fs": "^4.2.11",
+ "jest-docblock": "30.0.1",
+ "jest-environment-node": "30.0.2",
+ "jest-haste-map": "30.0.2",
+ "jest-leak-detector": "30.0.2",
+ "jest-message-util": "30.0.2",
+ "jest-resolve": "30.0.2",
+ "jest-runtime": "30.0.3",
+ "jest-util": "30.0.2",
+ "jest-watcher": "30.0.2",
+ "jest-worker": "30.0.2",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.3.tgz",
+ "integrity": "sha512-Xjosq0C48G9XEQOtmgrjXJwPaUPaq3sPJwHDRaiC+5wi4ZWxO6Lx6jNkizK/0JmTulVNuxP8iYwt77LGnfg3/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.2",
+ "@jest/fake-timers": "30.0.2",
+ "@jest/globals": "30.0.3",
+ "@jest/source-map": "30.0.1",
+ "@jest/test-result": "30.0.2",
+ "@jest/transform": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "cjs-module-lexer": "^2.1.0",
+ "collect-v8-coverage": "^1.0.2",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.2",
+ "jest-message-util": "30.0.2",
+ "jest-mock": "30.0.2",
+ "jest-regex-util": "30.0.1",
+ "jest-resolve": "30.0.2",
+ "jest-snapshot": "30.0.3",
+ "jest-util": "30.0.2",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "30.0.3",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz",
+ "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@babel/generator": "^7.27.5",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1",
+ "@babel/types": "^7.27.3",
+ "@jest/expect-utils": "30.0.3",
+ "@jest/get-type": "30.0.1",
+ "@jest/snapshot-utils": "30.0.1",
+ "@jest/transform": "30.0.2",
+ "@jest/types": "30.0.1",
+ "babel-preset-current-node-syntax": "^1.1.0",
+ "chalk": "^4.1.2",
+ "expect": "30.0.3",
+ "graceful-fs": "^4.2.11",
+ "jest-diff": "30.0.3",
+ "jest-matcher-utils": "30.0.3",
+ "jest-message-util": "30.0.2",
+ "jest-util": "30.0.2",
+ "pretty-format": "30.0.2",
+ "semver": "^7.7.2",
+ "synckit": "^0.11.8"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
+ "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz",
+ "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.1",
+ "@jest/types": "30.0.1",
+ "camelcase": "^6.3.0",
+ "chalk": "^4.1.2",
+ "leven": "^3.1.0",
+ "pretty-format": "30.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.2.tgz",
+ "integrity": "sha512-vYO5+E7jJuF+XmONr6CrbXdlYrgvZqtkn6pdkgjt/dU64UAdc0v1cAVaAeWtAfUUMScxNmnUjKPUMdCpNVASwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "30.0.2",
+ "@jest/types": "30.0.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "emittery": "^0.13.1",
+ "jest-util": "30.0.2",
+ "string-length": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz",
+ "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@ungap/structured-clone": "^1.3.0",
+ "jest-util": "30.0.2",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.1.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "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/napi-postinstall": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
+ "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/octokit": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.3.tgz",
+ "integrity": "sha512-+bwYsAIRmYv30NTmBysPIlgH23ekVDriB07oRxlPIAH5PI0yTMSxg5i5Xy0OetcnZw+nk/caD4szD7a9YZ3QyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/app": "^16.0.1",
+ "@octokit/core": "^7.0.2",
+ "@octokit/oauth-app": "^8.0.1",
+ "@octokit/plugin-paginate-graphql": "^6.0.0",
+ "@octokit/plugin-paginate-rest": "^13.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^16.0.0",
+ "@octokit/plugin-retry": "^8.0.1",
+ "@octokit/plugin-throttling": "^11.0.1",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "@octokit/webhooks": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "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/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/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": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
+ "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.1",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
+ "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/retry-request": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.0.tgz",
+ "integrity": "sha512-dJkZNmyV9C8WKUmbdj1xcvVlXBSvsUQCkg89TCK8rD72RdSn9A2jlXlS2VuYSTHoPJjJEfUHhjNYrlvuksF9cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/request": "^2.48.12",
+ "extend": "^3.0.2",
+ "teeny-request": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stream-events": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+ "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+ "license": "MIT",
+ "dependencies": {
+ "stubs": "^3.0.0"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+ "license": "MIT"
+ },
+ "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/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-length/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-length/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stubs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
+ "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.4"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/teeny-request": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz",
+ "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^3.3.2",
+ "stream-events": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/teeny-request/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/teeny-request/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toad-cache": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
+ "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "license": "MIT"
+ },
+ "node_modules/universal-github-app-jwt": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz",
+ "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==",
+ "license": "MIT"
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+ "license": "ISC"
+ },
+ "node_modules/unrs-resolver": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz",
+ "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "napi-postinstall": "^0.2.4"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.9.2",
+ "@unrs/resolver-binding-android-arm64": "1.9.2",
+ "@unrs/resolver-binding-darwin-arm64": "1.9.2",
+ "@unrs/resolver-binding-darwin-x64": "1.9.2",
+ "@unrs/resolver-binding-freebsd-x64": "1.9.2",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.9.2",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-x64-musl": "1.9.2",
+ "@unrs/resolver-binding-wasm32-wasi": "1.9.2",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.9.2"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/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/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/.github/actions/eng-metrics/package.json b/.github/actions/eng-metrics/package.json
new file mode 100644
index 0000000000..a1a01c4f24
--- /dev/null
+++ b/.github/actions/eng-metrics/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "engineering-metrics-collector",
+ "version": "1.0.0",
+ "description": "Comprehensive GitHub engineering metrics collector - tracks time to first review, time to merge, and GitHub Projects workflow metrics",
+ "type": "module",
+ "main": "src/index.js",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "start": "node src/index.js",
+ "start:print": "node src/index.js --print-only",
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
+ "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
+ "test:coverage": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage",
+ "test:ci": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --ci --coverage --watchAll=false",
+ "lint": "./node_modules/.bin/eslint src/**/*.js",
+ "lint:fix": "./node_modules/.bin/eslint src/**/*.js --fix"
+ },
+ "keywords": [
+ "github",
+ "engineering-metrics",
+ "developer-productivity",
+ "time-to-first-review",
+ "time-to-merge",
+ "github-projects",
+ "workflow-metrics",
+ "bigquery",
+ "analytics"
+ ],
+ "dependencies": {
+ "@google-cloud/bigquery": "^8.1.0",
+ "dotenv": "^17.0.0",
+ "octokit": "^5.0.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.30.0",
+ "eslint": "^9.30.0",
+ "jest": "^30.0.3"
+ },
+ "engines": {
+ "node": "20.18.1"
+ }
+}
diff --git a/.github/actions/eng-metrics/src/bigquery-client.js b/.github/actions/eng-metrics/src/bigquery-client.js
new file mode 100644
index 0000000000..5e78802b0a
--- /dev/null
+++ b/.github/actions/eng-metrics/src/bigquery-client.js
@@ -0,0 +1,400 @@
+/**
+ * BigQuery client module for PR pickup time metrics collector
+ * Handles authentication and data upload to BigQuery
+ */
+
+import { BigQuery } from '@google-cloud/bigquery';
+import fs from 'fs';
+import logger from './logger.js';
+
+/**
+ * BigQuery client class
+ */
+export class BigQueryClient {
+ /**
+ * Creates a new BigQuery client
+ * @param {string} keyFilePath - Path to the service account key file
+ */
+ constructor(keyFilePath) {
+ this.bigquery = null;
+ this.projectId = null;
+ this.initialize(keyFilePath);
+ }
+
+ /**
+ * Initializes the BigQuery client
+ * @param {string} keyFilePath - Path to the service account key file
+ */
+ initialize(keyFilePath) {
+ // Check if the key file exists
+ if (!fs.existsSync(keyFilePath)) {
+ const err = new Error(
+ `Service account key file not found at ${keyFilePath}`
+ );
+ logger.error('Failed to initialize BigQuery client', {}, err);
+ throw err;
+ }
+
+ try {
+ // Read and parse the service account key to extract project_id
+ const keyFileContent = fs.readFileSync(keyFilePath, 'utf8');
+ const serviceAccountKey = JSON.parse(keyFileContent);
+
+ if (!serviceAccountKey.project_id) {
+ throw new Error(
+ 'Service account key file must contain a project_id field'
+ );
+ }
+
+ this.projectId = serviceAccountKey.project_id;
+ logger.info(
+ `Extracted project ID from service account key: ${this.projectId}`
+ );
+
+ this.bigquery = new BigQuery({
+ keyFilename: keyFilePath,
+ projectId: this.projectId,
+ });
+
+ logger.info('BigQuery client initialized');
+ } catch (err) {
+ logger.error('Failed to initialize BigQuery client', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Gets the project ID extracted from the service account key
+ * @returns {string} Project ID
+ */
+ getProjectId() {
+ return this.projectId;
+ }
+
+ /**
+ * Creates a table if it doesn't exist with table-specific configuration
+ * @param {string} datasetId - BigQuery dataset ID
+ * @param {string} tableId - BigQuery table ID
+ * @param {Object} schema - BigQuery table schema
+ * @param {string} metricType - Type of metric (e.g., 'time_to_first_review', 'time_to_merge')
+ */
+ async createTableIfNotExists(datasetId, tableId, schema, metricType) {
+ try {
+ // Get a reference to the dataset
+ const dataset = this.bigquery.dataset(datasetId);
+
+ // Check if the dataset exists, create it if it doesn't
+ const [datasetExists] = await dataset.exists();
+
+ if (!datasetExists) {
+ logger.info(`Dataset ${datasetId} does not exist, creating it`);
+ await dataset.create();
+ logger.info(`Dataset ${datasetId} created`);
+ }
+
+ // Get a reference to the table
+ const table = dataset.table(tableId);
+
+ // Check if the table exists, create it if it doesn't
+ const [tableExists] = await table.exists();
+
+ if (!tableExists) {
+ logger.info(`Table ${tableId} does not exist, creating it`);
+
+ // Get table-specific configuration based on metric type
+ const tableConfig = this.getConfigurationForMetricType(metricType);
+
+ const options = {
+ schema,
+ timePartitioning: tableConfig.timePartitioning,
+ clustering: tableConfig.clustering,
+ };
+
+ await table.create(options);
+ logger.info(`Table ${tableId} created`);
+ }
+ } catch (err) {
+ logger.error(`Error creating table ${datasetId}.${tableId}`, {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Gets table-specific configuration for partitioning and clustering based on metric type
+ * @param {string} metricType - Type of metric (e.g., 'time_to_first_review', 'time_to_merge')
+ * @returns {Object} Table configuration
+ */
+ getConfigurationForMetricType(metricType) {
+ switch (metricType) {
+ case 'time_to_first_review':
+ return {
+ timePartitioning: {
+ type: 'DAY',
+ field: 'first_review_time',
+ },
+ clustering: {
+ fields: ['pr_creator', 'pr_number'],
+ },
+ };
+
+ case 'time_to_merge':
+ return {
+ timePartitioning: {
+ type: 'DAY',
+ field: 'merge_time',
+ },
+ clustering: {
+ fields: ['pr_creator', 'pr_number'],
+ },
+ };
+
+ default:
+ throw new Error(
+ `Unknown metric type for table configuration: ${metricType}`
+ );
+ }
+ }
+
+ /**
+ * Gets the BigQuery table schema for a specific metric type
+ * @param {string} metricType - Type of metric (e.g., 'time_to_first_review', 'time_to_merge')
+ * @returns {Object} BigQuery table schema
+ */
+ getSchemaForMetricType(metricType) {
+ switch (metricType) {
+ case 'time_to_first_review':
+ return {
+ fields: [
+ { name: 'review_date', type: 'DATE', mode: 'REQUIRED' },
+ { name: 'pr_creator', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'pr_url', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'pickup_time_seconds', type: 'INTEGER', mode: 'REQUIRED' },
+ { name: 'repository', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'pr_number', type: 'INTEGER', mode: 'REQUIRED' },
+ { name: 'target_branch', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'ready_time', type: 'TIMESTAMP', mode: 'REQUIRED' },
+ { name: 'first_review_time', type: 'TIMESTAMP', mode: 'REQUIRED' },
+ ],
+ };
+
+ case 'time_to_merge':
+ return {
+ fields: [
+ { name: 'merge_date', type: 'DATE', mode: 'REQUIRED' },
+ { name: 'pr_creator', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'pr_url', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'merge_time_seconds', type: 'INTEGER', mode: 'REQUIRED' },
+ { name: 'repository', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'pr_number', type: 'INTEGER', mode: 'REQUIRED' },
+ { name: 'target_branch', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'ready_time', type: 'TIMESTAMP', mode: 'REQUIRED' },
+ { name: 'merge_time', type: 'TIMESTAMP', mode: 'REQUIRED' },
+ ],
+ };
+
+ default:
+ throw new Error(`Unknown metric type: ${metricType}`);
+ }
+ }
+
+ /**
+ * Transforms metrics to BigQuery row format based on metric type
+ * @param {Object} metrics - Metrics object
+ * @returns {Object} BigQuery row
+ */
+ transformMetricsToRow(metrics) {
+ switch (metrics.metricType) {
+ case 'time_to_first_review':
+ return {
+ review_date: metrics.reviewDate,
+ pr_creator: metrics.prCreator,
+ pr_url: metrics.prUrl,
+ pickup_time_seconds: metrics.pickupTimeSeconds,
+ repository: metrics.repository,
+ pr_number: metrics.prNumber,
+ target_branch: metrics.targetBranch,
+ ready_time: metrics.readyTime.toISOString(),
+ first_review_time: metrics.firstReviewTime.toISOString(),
+ };
+
+ case 'time_to_merge':
+ return {
+ merge_date: metrics.mergeDate,
+ pr_creator: metrics.prCreator,
+ pr_url: metrics.prUrl,
+ merge_time_seconds: metrics.mergeTimeSeconds,
+ repository: metrics.repository,
+ pr_number: metrics.prNumber,
+ target_branch: metrics.targetBranch,
+ ready_time: metrics.readyTime.toISOString(),
+ merge_time: metrics.mergeTime.toISOString(),
+ };
+
+ default:
+ throw new Error(`Unknown metric type: ${metrics.metricType}`);
+ }
+ }
+
+ /**
+ * Checks if metrics already exist in BigQuery for the given PR numbers
+ * @param {string} datasetId - BigQuery dataset ID
+ * @param {string} tableId - BigQuery table ID
+ * @param {Array} prNumbers - Array of PR numbers to check
+ * @returns {Object} Object with PR numbers as keys and boolean values indicating if they exist
+ */
+ async checkExistingMetrics(datasetId, tableId, prNumbers) {
+ try {
+ // Get a reference to the table
+ const table = this.bigquery.dataset(datasetId).table(tableId);
+
+ // Check if the table exists
+ const [tableExists] = await table.exists();
+ if (!tableExists) {
+ // If the table doesn't exist, no metrics exist
+ return prNumbers.reduce((acc, prNumber) => {
+ acc[prNumber] = false;
+ return acc;
+ }, {});
+ }
+
+ // Create a parameterized query to check for existing PR numbers
+ const query = `
+ SELECT pr_number
+ FROM \`${this.projectId}.${datasetId}.${tableId}\`
+ WHERE pr_number IN UNNEST(@prNumbers)
+ `;
+
+ const options = {
+ query,
+ params: {
+ prNumbers,
+ },
+ };
+
+ // Run the query
+ const [rows] = await this.bigquery.query(options);
+
+ // Create a map of existing PR numbers
+ const existingPRs = rows.reduce((acc, row) => {
+ acc[row.pr_number] = true;
+ return acc;
+ }, {});
+
+ // Return a map of all PR numbers with their existence status
+ return prNumbers.reduce((acc, prNumber) => {
+ acc[prNumber] = !!existingPRs[prNumber];
+ return acc;
+ }, {});
+ } catch (err) {
+ logger.error(
+ `Error checking existing metrics in BigQuery ${datasetId}.${tableId}`,
+ {},
+ err
+ );
+ // If there's an error, assume no metrics exist
+ return prNumbers.reduce((acc, prNumber) => {
+ acc[prNumber] = false;
+ return acc;
+ }, {});
+ }
+ }
+
+ /**
+ * Uploads metrics to BigQuery
+ * @param {string} datasetId - BigQuery dataset ID
+ * @param {string} tableId - BigQuery table ID
+ * @param {Array} metrics - Array of metrics
+ */
+ async uploadMetrics(datasetId, tableId, metrics) {
+ try {
+ if (!metrics || metrics.length === 0) {
+ logger.warn('No metrics to upload');
+ return;
+ }
+
+ logger.info(
+ `Uploading ${metrics.length} metrics to BigQuery table ${tableId}`
+ );
+
+ // Get metric type from the first metric to determine schema
+ const metricType = metrics[0]?.metricType;
+ if (!metricType) {
+ throw new Error('Metrics must have a metricType field');
+ }
+
+ // Ensure the table exists with the correct schema
+ const schema = this.getSchemaForMetricType(metricType);
+ await this.createTableIfNotExists(datasetId, tableId, schema, metricType);
+
+ // Get all PR numbers from the metrics
+ const prNumbers = metrics.map((metric) => metric.prNumber);
+
+ // Check which PR numbers already exist in BigQuery
+ const existingMetrics = await this.checkExistingMetrics(
+ datasetId,
+ tableId,
+ prNumbers
+ );
+
+ // Filter out metrics that already exist
+ const newMetrics = metrics.filter(
+ (metric) => !existingMetrics[metric.prNumber]
+ );
+
+ if (newMetrics.length === 0) {
+ logger.info('All metrics already exist in BigQuery, nothing to upload');
+ return;
+ }
+
+ logger.info(
+ `Uploading ${newMetrics.length} new metrics to BigQuery (${
+ metrics.length - newMetrics.length
+ } already exist)`
+ );
+
+ // Transform metrics to BigQuery row format
+ const rows = newMetrics.map((metric) =>
+ this.transformMetricsToRow(metric)
+ );
+
+ // Get a reference to the table
+ const table = this.bigquery.dataset(datasetId).table(tableId);
+
+ // Upload the rows to BigQuery
+ const [apiResponse] = await table.insert(rows);
+
+ logger.info(
+ `Successfully uploaded ${newMetrics.length} metrics to BigQuery`,
+ {
+ datasetId,
+ tableId,
+ insertedRows: newMetrics.length,
+ skippedRows: metrics.length - newMetrics.length,
+ }
+ );
+
+ return apiResponse;
+ } catch (err) {
+ logger.error(
+ `Error uploading metrics to BigQuery ${datasetId}.${tableId}`,
+ {},
+ err
+ );
+
+ // Log more details about the error if it's an insertion error
+ if (
+ err.name === 'PartialFailureError' &&
+ err.errors &&
+ err.errors.length > 0
+ ) {
+ err.errors.forEach((error, index) => {
+ logger.error(`Row ${index} error:`, { error });
+ });
+ }
+
+ throw err;
+ }
+ }
+}
+
+export default BigQueryClient;
diff --git a/.github/actions/eng-metrics/src/config.js b/.github/actions/eng-metrics/src/config.js
new file mode 100644
index 0000000000..7075b59671
--- /dev/null
+++ b/.github/actions/eng-metrics/src/config.js
@@ -0,0 +1,273 @@
+/**
+ * Configuration module for engineering metrics collector
+ * Loads and validates configuration from files and environment variables
+ */
+
+import fs from 'fs';
+import path from 'path';
+import dotenv from 'dotenv';
+import logger from './logger.js';
+
+// Load environment variables from .env file
+dotenv.config();
+
+/**
+ * Default configuration values
+ */
+const DEFAULT_CONFIG = {
+ // Default target branch to track PRs for
+ targetBranch: 'main',
+
+ // Default BigQuery dataset ID
+ bigQueryDatasetId: 'github_metrics',
+
+ // Default time window for fetching PRs (in days)
+ lookbackDays: 5,
+
+ // Default print-only mode (false = upload to BigQuery, true = print to console)
+ printOnly: false,
+
+ // User group management configuration
+ userGroupEnabled: true,
+ userGroupFilepath: '../../../handbook/company/product-groups.md',
+
+ // Bot filtering configuration
+ excludeBotReviews: true,
+
+ // Multi-table configuration
+ metrics: {
+ timeToFirstReview: {
+ enabled: true,
+ tableName: 'pr_first_review',
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: 'pr_merge',
+ },
+ },
+};
+
+/**
+ * Loads configuration from a JSON file
+ * @param {string} configPath - Path to the configuration file
+ * @returns {Object} Configuration object
+ */
+const loadConfigFromFile = (configPath) => {
+ try {
+ const resolvedPath = path.resolve(process.cwd(), configPath);
+ logger.info(`Loading configuration from ${resolvedPath}`);
+
+ if (!fs.existsSync(resolvedPath)) {
+ logger.warn(`Configuration file not found at ${resolvedPath}`);
+ return {};
+ }
+
+ const configData = fs.readFileSync(resolvedPath, 'utf8');
+ return JSON.parse(configData);
+ } catch (err) {
+ logger.error(
+ `Error loading configuration from file: ${configPath}`,
+ {},
+ err
+ );
+ return {};
+ }
+};
+
+/**
+ * Loads configuration from environment variables
+ * @returns {Object} Configuration object
+ */
+const loadConfigFromEnv = () => {
+ // Create a config object with only defined values
+ const config = {};
+
+ // Parse repositories from environment variable if provided
+ if (process.env.REPOSITORIES) {
+ config.repositories = process.env.REPOSITORIES.split(',').map((repo) =>
+ repo.trim()
+ );
+ }
+
+ // Add other environment variables if they are defined
+ if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
+ if (process.env.BIGQUERY_DATASET_ID)
+ config.bigQueryDatasetId = process.env.BIGQUERY_DATASET_ID;
+ if (process.env.SERVICE_ACCOUNT_KEY_PATH)
+ config.serviceAccountKeyPath = process.env.SERVICE_ACCOUNT_KEY_PATH;
+ if (process.env.TARGET_BRANCH)
+ config.targetBranch = process.env.TARGET_BRANCH;
+ if (process.env.PRINT_ONLY)
+ config.printOnly = process.env.PRINT_ONLY === 'true';
+ if (process.env.USER_GROUP_ENABLED)
+ config.userGroupEnabled = process.env.USER_GROUP_ENABLED === 'true';
+ if (process.env.USER_GROUP_FILEPATH)
+ config.userGroupFilepath = process.env.USER_GROUP_FILEPATH;
+
+ // Handle metrics configuration from environment variables
+ if (process.env.ENABLED_METRICS) {
+ const enabledMetrics = process.env.ENABLED_METRICS.split(
+ ','
+ ).map((metric) => metric.trim());
+ config.metrics = {
+ timeToFirstReview: {
+ enabled: enabledMetrics.includes('time_to_first_review'),
+ tableName: process.env.TIME_TO_FIRST_REVIEW_TABLE || 'pr_first_review',
+ },
+ timeToMerge: {
+ enabled: enabledMetrics.includes('time_to_merge'),
+ tableName: process.env.TIME_TO_MERGE_TABLE || 'pr_merge',
+ },
+ };
+ }
+
+ return config;
+};
+
+/**
+ * Validates the configuration
+ * @param {Object} config - Configuration object
+ * @returns {boolean} True if configuration is valid, false otherwise
+ */
+const validateConfig = (config) => {
+ // Always required fields
+ const requiredFields = ['repositories', 'githubToken'];
+
+ // Fields required only when not in print-only mode
+ if (!config.printOnly) {
+ requiredFields.push('serviceAccountKeyPath');
+ }
+
+ const missingFields = requiredFields.filter((field) => !config[field]);
+
+ if (missingFields.length > 0) {
+ logger.error(
+ `Missing required configuration fields: ${missingFields.join(', ')}`
+ );
+ return false;
+ }
+
+ // Validate repositories array
+ if (!Array.isArray(config.repositories) || config.repositories.length === 0) {
+ logger.error('Configuration must include at least one repository');
+ return false;
+ }
+
+ // Validate repository format (owner/repo)
+ const invalidRepos = config.repositories.filter((repo) => {
+ return typeof repo !== 'string' || !repo.includes('/');
+ });
+
+ if (invalidRepos.length > 0) {
+ logger.error(`Invalid repository format: ${invalidRepos.join(', ')}`);
+ return false;
+ }
+
+ // Validate metrics configuration
+ if (!config.metrics || typeof config.metrics !== 'object') {
+ logger.error('Configuration must include metrics configuration');
+ return false;
+ }
+
+ // Validate that at least one metric is enabled
+ const enabledMetrics = Object.values(config.metrics).filter(
+ (metric) => metric.enabled
+ );
+ if (enabledMetrics.length === 0) {
+ logger.error('At least one metric must be enabled');
+ return false;
+ }
+
+ // Validate metric configurations
+ for (const [metricName, metricConfig] of Object.entries(config.metrics)) {
+ if (metricConfig.enabled) {
+ if (
+ !metricConfig.tableName ||
+ typeof metricConfig.tableName !== 'string'
+ ) {
+ logger.error(`Metric ${metricName} must have a valid tableName`);
+ return false;
+ }
+ }
+ }
+
+ // Validate userGroupFilepath when userGroupEnabled is true
+ if (config.userGroupEnabled) {
+ if (!config.userGroupFilepath) {
+ logger.error(
+ 'userGroupFilepath must be specified when userGroupEnabled is true'
+ );
+ return false;
+ }
+
+ const resolvedUserGroupPath = path.resolve(
+ process.cwd(),
+ config.userGroupFilepath
+ );
+ if (!fs.existsSync(resolvedUserGroupPath)) {
+ logger.error(`User group file not found at ${resolvedUserGroupPath}`);
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * Loads and validates configuration
+ * @param {string} [configPath='config.json'] - Path to the configuration file
+ * @returns {Object} Configuration object
+ */
+export const loadConfig = (configPath = 'config.json') => {
+ // Load configuration from file
+ const fileConfig = loadConfigFromFile(configPath);
+
+ // Load configuration from environment variables
+ const envConfig = loadConfigFromEnv();
+
+ // Merge configurations with precedence: env > file > default
+ const config = {
+ ...DEFAULT_CONFIG,
+ ...fileConfig,
+ ...envConfig,
+ };
+
+ // Filter out undefined values
+ Object.keys(config).forEach((key) => {
+ if (config[key] === undefined) {
+ delete config[key];
+ }
+ });
+
+ // Validate configuration
+ const isValid = validateConfig(config);
+
+ if (!isValid) {
+ throw new Error('Invalid configuration');
+ }
+
+ logger.info('Configuration loaded successfully', {
+ repositories: config.repositories,
+ targetBranch: config.targetBranch,
+ printOnly: config.printOnly,
+ metrics: Object.fromEntries(
+ Object.entries(config.metrics).map(([key, value]) => [
+ key,
+ { enabled: value.enabled, tableName: value.tableName },
+ ])
+ ),
+ ...(config.printOnly
+ ? {}
+ : {
+ bigQueryDatasetId: config.bigQueryDatasetId,
+ }),
+ });
+
+ return config;
+};
+
+export { validateConfig };
+
+export default {
+ loadConfig,
+};
diff --git a/.github/actions/eng-metrics/src/github-client.js b/.github/actions/eng-metrics/src/github-client.js
new file mode 100644
index 0000000000..683ed34d22
--- /dev/null
+++ b/.github/actions/eng-metrics/src/github-client.js
@@ -0,0 +1,655 @@
+/**
+ * GitHub client module for engineering metrics collector
+ * Handles interactions with the GitHub API using Octokit.js
+ */
+
+import { Octokit } from 'octokit';
+import logger from './logger.js';
+
+/**
+ * Identifies if a GitHub user is likely a bot
+ * @param {Object} user - GitHub user object
+ * @returns {Object} Bot analysis result
+ */
+function identifyBotUser(user) {
+ const botIndicators = {
+ isBot: false,
+ confidence: 'low',
+ reasons: [],
+ };
+
+ // Check GitHub's bot flag (most reliable)
+ if (user.type === 'Bot') {
+ botIndicators.isBot = true;
+ botIndicators.confidence = 'high';
+ botIndicators.reasons.push('GitHub API type is "Bot"');
+ return botIndicators;
+ }
+
+ // Check username patterns
+ const username = user.login.toLowerCase();
+ const botPatterns = [
+ /\[bot]/, // contains '[bot]'
+ /^dependabot/, // dependabot
+ /^renovate/, // renovate bot
+ /^github-actions/, // GitHub Actions
+ /^codecov/, // codecov bot
+ /^coderabbitai/, // coderabbit AI bot
+ /^sonarcloud/, // sonarcloud bot
+ /^snyk/, // snyk bot
+ /^greenkeeper/, // greenkeeper bot
+ /^semantic-release/, // semantic-release bot
+ /^stale/, // stale bot
+ /^imgbot/, // imgbot
+ /^allcontributors/, // all-contributors bot
+ /^whitesource/, // whitesource bot
+ /^deepsource/, // deepsource bot
+ ];
+
+ for (const pattern of botPatterns) {
+ if (pattern.test(username)) {
+ botIndicators.isBot = true;
+ botIndicators.confidence = 'high';
+ botIndicators.reasons.push(`Username matches bot pattern: ${pattern}`);
+ break;
+ }
+ }
+
+ return botIndicators;
+}
+
+/**
+ * GitHub client class
+ */
+export class GitHubClient {
+ /**
+ * Creates a new GitHub client
+ * @param {string} token - GitHub API token
+ */
+ constructor(token) {
+ if (!token) {
+ throw new Error('GitHub token is required');
+ }
+ this.octokit = null;
+ this.initialize(token);
+ }
+
+ /**
+ * Initializes the GitHub client
+ * @param {string} token - GitHub API token
+ */
+ initialize(token) {
+ try {
+ this.octokit = new Octokit({
+ auth: token,
+ });
+ logger.info('GitHub client initialized');
+ } catch (err) {
+ logger.error('Failed to initialize GitHub client', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Fetches pull requests for a repository
+ * @param {string} owner - Repository owner
+ * @param {string} repo - Repository name
+ * @param {string} state - PR state (open, closed, all)
+ * @param {Date} since - Fetch PRs updated since this date
+ * @param {string} targetBranch - Target branch to filter PRs by
+ * @returns {Array} Array of pull requests
+ */
+ async fetchPullRequests(
+ owner,
+ repo,
+ state = 'all',
+ since,
+ targetBranch = 'main'
+ ) {
+ try {
+ logger.info(
+ `Fetching ${state} PRs for ${owner}/${repo} since ${since.toISOString()}`
+ );
+
+ // GitHub API returns paginated results, so we need to fetch all pages
+ const pullRequests = [];
+ let page = 1;
+ let hasMorePages = true;
+
+ while (hasMorePages) {
+ const response = await this.octokit.rest.pulls.list({
+ owner,
+ repo,
+ state,
+ sort: 'updated',
+ direction: 'desc',
+ per_page: 100,
+ page,
+ });
+
+ // Filter PRs by update date and target branch
+ const filteredPRs = response.data.filter((pr) => {
+ const prUpdatedAt = new Date(pr.updated_at);
+ return prUpdatedAt >= since && pr.base.ref === targetBranch;
+ });
+
+ if (filteredPRs.length > 0) {
+ pullRequests.push(...filteredPRs);
+ page++;
+ } else {
+ hasMorePages = false;
+ }
+
+ // If we got fewer results than the page size, there are no more pages
+ if (response.data.length < 100) {
+ hasMorePages = false;
+ }
+ }
+
+ logger.info(`Fetched ${pullRequests.length} PRs for ${owner}/${repo}`);
+ return pullRequests;
+ } catch (err) {
+ logger.error(`Error fetching PRs for ${owner}/${repo}`, {}, err);
+
+ // Implement basic retry for rate limiting
+ if (
+ err.status === 403 &&
+ err.response?.headers?.['x-ratelimit-remaining'] === '0'
+ ) {
+ const resetTime =
+ parseInt(err.response.headers['x-ratelimit-reset'], 10) * 1000;
+ const waitTime = resetTime - Date.now();
+
+ if (waitTime > 0 && waitTime < 3600000) {
+ // Only retry if wait time is less than 1 hour
+ logger.info(
+ `Rate limit exceeded. Retrying in ${Math.ceil(
+ waitTime / 1000
+ )} seconds`
+ );
+ await new Promise((resolve) => setTimeout(resolve, waitTime + 1000));
+ return this.fetchPullRequests(
+ owner,
+ repo,
+ state,
+ since,
+ targetBranch
+ );
+ }
+ }
+
+ throw err;
+ }
+ }
+
+ /**
+ * Fetches PR review events
+ * @param {string} owner - Repository owner
+ * @param {string} repo - Repository name
+ * @param {number} prNumber - PR number
+ * @returns {Array} Array of review events
+ */
+ async fetchPRReviewEvents(owner, repo, prNumber) {
+ try {
+ logger.info(`Fetching review events for ${owner}/${repo}#${prNumber}`);
+
+ const response = await this.octokit.rest.pulls.listReviews({
+ owner,
+ repo,
+ pull_number: prNumber,
+ });
+
+ logger.info(
+ `Fetched ${response.data.length} review events for ${owner}/${repo}#${prNumber}`
+ );
+ return response.data;
+ } catch (err) {
+ logger.error(
+ `Error fetching review events for ${owner}/${repo}#${prNumber}`,
+ {},
+ err
+ );
+
+ // Implement basic retry for rate limiting
+ if (
+ err.status === 403 &&
+ err.response?.headers?.['x-ratelimit-remaining'] === '0'
+ ) {
+ const resetTime =
+ parseInt(err.response.headers['x-ratelimit-reset'], 10) * 1000;
+ const waitTime = resetTime - Date.now();
+
+ if (waitTime > 0 && waitTime < 3600000) {
+ // Only retry if wait time is less than 1 hour
+ logger.info(
+ `Rate limit exceeded. Retrying in ${Math.ceil(
+ waitTime / 1000
+ )} seconds`
+ );
+ await new Promise((resolve) => setTimeout(resolve, waitTime + 1000));
+ return this.fetchPRReviewEvents(owner, repo, prNumber);
+ }
+ }
+
+ throw err;
+ }
+ }
+
+ /**
+ * Filters out bot reviews from review events
+ * @param {Array} reviewEvents - Array of review events
+ * @param {boolean} excludeBots - Whether to exclude bot reviews (default: false)
+ * @returns {Array} Filtered review events
+ */
+ filterBotReviews(reviewEvents, excludeBots = false) {
+ if (!excludeBots) {
+ return reviewEvents;
+ }
+
+ const filteredReviews = reviewEvents.filter((review) => {
+ const botAnalysis = identifyBotUser(review.user);
+ if (botAnalysis.isBot) {
+ logger.debug(`Filtering out bot review from ${review.user.login}`, {
+ confidence: botAnalysis.confidence,
+ reasons: botAnalysis.reasons,
+ });
+ return false;
+ }
+ return true;
+ });
+
+ const botCount = reviewEvents.length - filteredReviews.length;
+ if (botCount > 0) {
+ logger.info(
+ `Filtered out ${botCount} bot reviews from ${reviewEvents.length} total reviews`
+ );
+ }
+
+ return filteredReviews;
+ }
+
+ /**
+ * Fetches PR timeline events
+ * @param {string} owner - Repository owner
+ * @param {string} repo - Repository name
+ * @param {number} prNumber - PR number
+ * @returns {Array} Array of timeline events
+ */
+ async fetchPRTimelineEvents(owner, repo, prNumber) {
+ try {
+ logger.info(`Fetching timeline events for ${owner}/${repo}#${prNumber}`);
+
+ // GitHub API returns paginated results, so we need to fetch all pages
+ const timelineEvents = [];
+ let page = 1;
+ let hasMorePages = true;
+
+ while (hasMorePages) {
+ const response = await this.octokit.rest.issues.listEventsForTimeline({
+ owner,
+ repo,
+ issue_number: prNumber,
+ per_page: 100,
+ page,
+ });
+
+ if (response.data.length > 0) {
+ timelineEvents.push(...response.data);
+ page++;
+ } else {
+ hasMorePages = false;
+ }
+
+ // If we got fewer results than the page size, there are no more pages
+ if (response.data.length < 100) {
+ hasMorePages = false;
+ }
+ }
+
+ logger.info(
+ `Fetched ${timelineEvents.length} timeline events for ${owner}/${repo}#${prNumber}`
+ );
+ return timelineEvents;
+ } catch (err) {
+ logger.error(
+ `Error fetching timeline events for ${owner}/${repo}#${prNumber}`,
+ {},
+ err
+ );
+
+ // Implement basic retry for rate limiting
+ if (
+ err.status === 403 &&
+ err.response?.headers?.['x-ratelimit-remaining'] === '0'
+ ) {
+ const resetTime =
+ parseInt(err.response.headers['x-ratelimit-reset'], 10) * 1000;
+ const waitTime = resetTime - Date.now();
+
+ if (waitTime > 0 && waitTime < 3600000) {
+ // Only retry if wait time is less than 1 hour
+ logger.info(
+ `Rate limit exceeded. Retrying in ${Math.ceil(
+ waitTime / 1000
+ )} seconds`
+ );
+ await new Promise((resolve) => setTimeout(resolve, waitTime + 1000));
+ return this.fetchPRTimelineEvents(owner, repo, prNumber);
+ }
+ }
+
+ throw err;
+ }
+ }
+
+ /**
+ * Calculates pickup time for a PR
+ * @param {Object} pr - Pull request object
+ * @param {Array} timelineEvents - PR timeline events
+ * @param {Array} reviewEvents - PR review events
+ * @returns {Object} Pickup time metrics
+ */
+ calculatePickupTime(pr, timelineEvents, reviewEvents) {
+ try {
+ const result = this.getReadyAndFirstReview(
+ pr,
+ timelineEvents,
+ reviewEvents
+ );
+ if (!result || !result.firstReviewTime) {
+ return null;
+ }
+ const { relevantReadyEvent, firstReviewTime } = result;
+ const readyTime = relevantReadyEvent.time;
+
+ // Calculate pickup time excluding weekends
+ const pickupTimeSeconds = this.calculatePickupTimeExcludingWeekends(
+ readyTime,
+ firstReviewTime
+ );
+
+ // If pickup time is negative, something went wrong
+ if (pickupTimeSeconds < 0) {
+ logger.warn(`Negative pickup time for ${pr.html_url}`, {
+ readyTime,
+ firstReviewTime,
+ pickupTimeSeconds,
+ });
+ return null;
+ }
+
+ // Log which ready event was used
+ const readyEventType =
+ relevantReadyEvent.event.event === 'created_not_draft'
+ ? 'PR creation (not draft)'
+ : 'ready_for_review event';
+
+ logger.info(`Calculated pickup time for ${pr.html_url}`, {
+ pickupTimeSeconds,
+ readyEventType,
+ readyTime: readyTime.toISOString(),
+ firstReviewTime: firstReviewTime.toISOString(),
+ });
+
+ // We already have readyEventType defined above, so we can use it here
+
+ return {
+ metricType: 'time_to_first_review',
+ repository: `${pr.base.repo.owner.login}/${pr.base.repo.name}`,
+ prNumber: pr.number,
+ prUrl: pr.html_url,
+ prCreator: pr.user.login,
+ targetBranch: pr.base.ref,
+ readyTime,
+ firstReviewTime,
+ reviewDate: firstReviewTime.toISOString().split('T')[0], // YYYY-MM-DD
+ pickupTimeSeconds,
+ readyEventType,
+ };
+ } catch (err) {
+ logger.error(`Error calculating pickup time for ${pr.html_url}`, {}, err);
+ return null;
+ }
+ }
+
+ /**
+ * Calculates pickup time for a PR
+ * @param {Object} pr - Pull request object
+ * @param {Array} timelineEvents - PR timeline events
+ * @param {Array} reviewEvents - PR review events
+ * @returns {Object} ready event and first review time
+ */
+ getReadyAndFirstReview(pr, timelineEvents, reviewEvents) {
+ const mergeTime = pr.merged_at ? new Date(pr.merged_at) : null;
+
+ // Find all ready_for_review events that occurred before merge time (if merged)
+ const readyForReviewEvents = timelineEvents
+ .filter((event) => event.event === 'ready_for_review')
+ .map((event) => ({
+ time: new Date(event.created_at),
+ event,
+ }))
+ .filter((readyEvent) => !mergeTime || readyEvent.time <= mergeTime);
+
+ // Add PR creation time as a ready event if PR was not created as draft
+ if (!pr.draft) {
+ readyForReviewEvents.push({
+ time: new Date(pr.created_at),
+ event: { event: 'created_not_draft', created_at: pr.created_at },
+ });
+ }
+
+ // Sort ready events by time (ascending)
+ readyForReviewEvents.sort((a, b) => a.time - b.time);
+
+ // If we couldn't find any ready events, return null
+ if (readyForReviewEvents.length === 0) {
+ logger.warn(`No ready_for_review events found for ${pr.html_url}`);
+ return null;
+ }
+
+ // If there is no review events, the PR may have been merged without a review.
+ if (reviewEvents.length === 0) {
+ const relevantReadyEvent =
+ readyForReviewEvents[readyForReviewEvents.length - 1];
+ return {
+ relevantReadyEvent,
+ firstReviewTime: null,
+ };
+ }
+
+ // Sort review events by submitted_at (ascending)
+ const sortedReviewEvents = [...reviewEvents].sort(
+ (a, b) => new Date(a.submitted_at) - new Date(b.submitted_at)
+ );
+
+ const firstReview = sortedReviewEvents[0];
+ const firstReviewTime = new Date(firstReview.submitted_at);
+
+ // Find the most recent ready event that occurred before the first review
+ const relevantReadyEvent = readyForReviewEvents
+ .filter((readyEvent) => readyEvent.time < firstReviewTime)
+ .pop();
+
+ // If no ready event occurred before the first review, return null
+ if (!relevantReadyEvent) {
+ logger.warn(
+ `No ready_for_review event found before first review for ${pr.html_url}`
+ );
+ return null;
+ }
+
+ return {
+ relevantReadyEvent,
+ firstReviewTime,
+ };
+ }
+
+ /**
+ * Calculates pickup time excluding weekends
+ * @param {Date} readyTimeOrig - Time when PR was marked as ready for review
+ * @param {Date} reviewTimeOrig - Time when the first review occurred
+ * @returns {number} Pickup time in seconds, excluding weekends
+ */
+ calculatePickupTimeExcludingWeekends(readyTimeOrig, reviewTimeOrig) {
+ const readyTime = new Date(readyTimeOrig);
+ const reviewTime = new Date(reviewTimeOrig);
+
+ // Get day of week (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
+ const readyDay = readyTime.getUTCDay();
+ const reviewDay = reviewTime.getUTCDay();
+
+ // Case: Both ready time and review time are on the same weekend
+ if (
+ (readyDay === 0 || readyDay === 6) &&
+ (reviewDay === 0 || reviewDay === 6) &&
+ Math.floor(reviewTime / (24 * 60 * 60 * 1000)) -
+ Math.floor(readyTime / (24 * 60 * 60 * 1000)) <=
+ 2
+ ) {
+ // Return 0 seconds pickup time
+ return 0;
+ }
+
+ // Set to start of Monday if ready time is on weekend
+ if (readyDay === 0) {
+ // Sunday
+ readyTime.setUTCDate(readyTime.getUTCDate() + 1);
+ readyTime.setUTCHours(0, 0, 0, 0);
+ } else if (readyDay === 6) {
+ // Saturday
+ readyTime.setUTCDate(readyTime.getUTCDate() + 2);
+ readyTime.setUTCHours(0, 0, 0, 0);
+ }
+ // Set to start of Saturday if review time is on Sunday
+ if (reviewDay === 0) {
+ // Sunday
+ reviewTime.setUTCDate(reviewTime.getUTCDate() - 1);
+ reviewTime.setUTCHours(0, 0, 0, 0);
+ } else if (reviewDay === 6) {
+ // Saturday
+ reviewTime.setUTCHours(0, 0, 0, 0);
+ }
+
+ // Calculate raw time difference in milliseconds
+ const weekendDays = countWeekendDays(readyTime, reviewTime);
+ const diffMs = reviewTime - readyTime - weekendDays * 24 * 60 * 60 * 1000;
+
+ // Ensure we don't return negative values
+ return Math.max(0, Math.floor(diffMs / 1000));
+ }
+
+ /**
+ * Calculate time to merge metrics
+ * @param {Object} pr - Pull request object
+ * @param {Array} timelineEvents - Timeline events
+ * @param {Array} reviewEvents - PR review events
+ * @returns {Object|null} Time to merge metrics or null if not applicable
+ */
+ calculateTimeToMerge(pr, timelineEvents, reviewEvents) {
+ try {
+ // Only process merged PRs
+ if (!pr.merged_at) {
+ return null;
+ }
+
+ // Find the ready time using the same algorithm as we use for Time to First Review
+ const result = this.getReadyAndFirstReview(
+ pr,
+ timelineEvents,
+ reviewEvents
+ );
+ if (!result) {
+ return null;
+ }
+ const relevantReadyEvent = result.relevantReadyEvent;
+ const readyTime = relevantReadyEvent.time;
+ const mergeTime = new Date(pr.merged_at);
+
+ // Calculate merge time excluding weekends
+ const mergeTimeSeconds = this.calculatePickupTimeExcludingWeekends(
+ readyTime,
+ mergeTime
+ );
+
+ // If merge time is negative, something went wrong
+ if (mergeTimeSeconds < 0) {
+ logger.warn(`Negative merge time for ${pr.html_url}`, {
+ readyTime,
+ mergeTime,
+ mergeTimeSeconds,
+ });
+ return null;
+ }
+
+ // Log which ready event was used
+ const readyEventType =
+ relevantReadyEvent.event.event === 'created_not_draft'
+ ? 'PR creation (not draft)'
+ : 'ready_for_review event';
+
+ logger.info(`Calculated merge time for ${pr.html_url}`, {
+ mergeTimeSeconds,
+ readyEventType,
+ readyTime: readyTime.toISOString(),
+ mergeTime: mergeTime.toISOString(),
+ });
+
+ return {
+ metricType: 'time_to_merge',
+ repository: `${pr.base.repo.owner.login}/${pr.base.repo.name}`,
+ prNumber: pr.number,
+ prUrl: pr.html_url,
+ prCreator: pr.user.login,
+ targetBranch: pr.base.ref,
+ readyTime,
+ mergeTime,
+ mergeDate: mergeTime.toISOString().split('T')[0], // YYYY-MM-DD
+ mergeTimeSeconds,
+ readyEventType,
+ };
+ } catch (err) {
+ logger.error(`Error calculating merge time for ${pr.html_url}`, {}, err);
+ return null;
+ }
+ }
+}
+
+function countWeekendDays(startDate, endDate) {
+ // Make local copies of dates
+ startDate = new Date(startDate);
+ endDate = new Date(endDate);
+
+ // Ensure startDate is before endDate
+ if (startDate > endDate) {
+ [startDate, endDate] = [endDate, startDate];
+ }
+
+ // Make sure start dates and end dates are not on weekends. We just want to count the weekend days between them.
+ if (startDate.getUTCDay() === 0) {
+ startDate.setUTCDate(startDate.getUTCDate() + 1);
+ } else if (startDate.getUTCDay() === 6) {
+ startDate.setUTCDate(startDate.getUTCDate() + 2);
+ }
+ if (endDate.getUTCDay() === 0) {
+ endDate.setUTCDate(endDate.getUTCDate() - 2);
+ } else if (endDate.getUTCDay() === 6) {
+ endDate.setUTCDate(endDate.getUTCDate() - 1);
+ }
+
+ let count = 0;
+ const current = new Date(startDate);
+
+ while (current <= endDate) {
+ const day = current.getUTCDay();
+ if (day === 0 || day === 6) {
+ // Sunday (0) or Saturday (6)
+ count++;
+ }
+ current.setUTCDate(current.getUTCDate() + 1);
+ }
+
+ return count;
+}
+
+export default GitHubClient;
diff --git a/.github/actions/eng-metrics/src/github-validator.js b/.github/actions/eng-metrics/src/github-validator.js
new file mode 100644
index 0000000000..7d09d320ce
--- /dev/null
+++ b/.github/actions/eng-metrics/src/github-validator.js
@@ -0,0 +1,109 @@
+/**
+ * GitHub username validator
+ * Validates that extracted usernames are real GitHub accounts
+ */
+
+import { Octokit } from 'octokit';
+import logger from './logger.js';
+
+/**
+ * Validates a single GitHub username
+ * @param {Octokit} octokit - GitHub API client
+ * @param {string} username - GitHub username to validate
+ * @returns {Promise} True if username exists, false otherwise
+ */
+const validateUsername = async (octokit, username) => {
+ try {
+ await octokit.rest.users.getByUsername({ username });
+ return true;
+ } catch (error) {
+ if (error.status === 404) {
+ logger.warn(`GitHub username not found: ${username}`);
+ return false;
+ }
+
+ // For other errors (rate limiting, network issues), log but assume valid
+ logger.warn(`Error validating username ${username}: ${error.message}`);
+ return true; // Assume valid to avoid false negatives
+ }
+};
+
+/**
+ * Validates multiple GitHub usernames
+ * @param {string} githubToken - GitHub API token
+ * @param {Array} usernames - Array of usernames to validate
+ * @returns {Promise>} Array of valid usernames
+ */
+export const validateUsernames = async (githubToken, usernames) => {
+ if (!githubToken) {
+ throw new Error('GitHub token is required for username validation');
+ }
+
+ const octokit = new Octokit({ auth: githubToken });
+ const validUsernames = [];
+ const invalidUsernames = [];
+
+ logger.info(`Validating ${usernames.length} GitHub usernames...`);
+
+ // Process usernames with a small delay to respect rate limits
+ for (const username of usernames) {
+ const isValid = await validateUsername(octokit, username);
+
+ if (isValid) {
+ validUsernames.push(username);
+ } else {
+ invalidUsernames.push(username);
+ }
+
+ // Small delay to avoid hitting rate limits too aggressively.
+ // 2025/07/03: GitHub's authenticated rate limit is 5000 requests/hour (~1.4 requests/second). This could lead to rate limit errors with larger username lists.
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+
+ if (invalidUsernames.length > 0) {
+ logger.warn(
+ `Found ${
+ invalidUsernames.length
+ } invalid GitHub usernames: ${invalidUsernames.join(', ')}`
+ );
+ }
+
+ logger.info(
+ `Validated ${validUsernames.length} out of ${usernames.length} GitHub usernames`
+ );
+ return validUsernames;
+};
+
+/**
+ * Filters user groups to only include valid usernames
+ * @param {string} githubToken - GitHub API token
+ * @param {Array<{group: string, username: string}>} userGroups - Array of user group mappings
+ * @returns {Promise>} Array of user group mappings with valid usernames only
+ */
+export const filterValidUserGroups = async (githubToken, userGroups) => {
+ // Get unique usernames for validation
+ const uniqueUsernames = [...new Set(userGroups.map((ug) => ug.username))];
+
+ // Validate usernames
+ const validUsernames = await validateUsernames(githubToken, uniqueUsernames);
+ const validUsernameSet = new Set(validUsernames);
+
+ // Filter user groups to only include valid usernames
+ const validUserGroups = userGroups.filter((ug) =>
+ validUsernameSet.has(ug.username)
+ );
+
+ const removedCount = userGroups.length - validUserGroups.length;
+ if (removedCount > 0) {
+ logger.info(
+ `Removed ${removedCount} user group mappings due to invalid usernames`
+ );
+ }
+
+ return validUserGroups;
+};
+
+export default {
+ validateUsernames,
+ filterValidUserGroups,
+};
diff --git a/.github/actions/eng-metrics/src/index.js b/.github/actions/eng-metrics/src/index.js
new file mode 100755
index 0000000000..5f27a5d28b
--- /dev/null
+++ b/.github/actions/eng-metrics/src/index.js
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+
+/**
+ * Main entry point for engineering metrics collector
+ * Collects comprehensive GitHub engineering metrics including:
+ * - Time to First Review (currently implemented)
+ * - Time to Merge (planned)
+ * - Time to QA Ready (planned)
+ * - Time to Production Ready (planned)
+ */
+
+import { loadConfig } from './config.js';
+import { MetricsCollector } from './metrics-collector.js';
+import logger from './logger.js';
+import { fileURLToPath } from 'url';
+import { dirname, join, resolve } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * Main function
+ */
+async function main() {
+ try {
+ // Set the working directory to one level up from the current file location, which is the root of the action.
+ process.chdir(resolve(__dirname, '..'));
+
+ // Parse command line arguments
+ const args = process.argv.slice(2);
+
+ // Check for print-only flag in command line arguments
+ const printOnlyFlag = args.includes('--print-only');
+
+ // Get the configuration path from command line arguments
+ // Filter out the --print-only flag if present
+ const configPath =
+ args.filter((arg) => arg !== '--print-only')[0] ||
+ join(__dirname, '..', 'config.json');
+
+ // Load configuration
+ const config = loadConfig(configPath);
+
+ // Override printOnly setting if flag is provided
+ if (printOnlyFlag) {
+ config.printOnly = true;
+ }
+
+ // Create and run metrics collector
+ const metricsCollector = new MetricsCollector(config);
+ const metrics = await metricsCollector.run();
+
+ if (config.printOnly) {
+ logger.info(
+ `Successfully collected and printed ${metrics.length} engineering metrics`
+ );
+ } else {
+ logger.info(
+ `Successfully collected and uploaded ${metrics.length} engineering metrics to BigQuery`
+ );
+ }
+
+ // Exit with success
+ process.exit(0);
+ } catch (err) {
+ logger.error('Error running engineering metrics collector', {}, err);
+
+ // Exit with error
+ process.exit(1);
+ }
+}
+
+// Run the main function
+await main();
diff --git a/.github/actions/eng-metrics/src/logger.js b/.github/actions/eng-metrics/src/logger.js
new file mode 100644
index 0000000000..29cc3f06d8
--- /dev/null
+++ b/.github/actions/eng-metrics/src/logger.js
@@ -0,0 +1,112 @@
+/**
+ * Logger module for engineering metrics collector
+ * Provides structured JSON logging
+ */
+
+/**
+ * Log levels
+ */
+const LOG_LEVELS = {
+ DEBUG: 'debug',
+ INFO: 'info',
+ WARN: 'warn',
+ ERROR: 'error',
+};
+
+/**
+ * Creates a log entry with the specified level, message, and optional data
+ * @param {string} level - Log level
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ * @param {Error} [error] - Optional error object
+ * @returns {Object} Log entry object
+ */
+const createLogEntry = (level, message, data = {}, error = null) => {
+ const logEntry = {
+ timestamp: new Date().toISOString(),
+ level,
+ message,
+ ...(Object.keys(data).length > 0 && { data }),
+ };
+
+ if (error) {
+ logEntry.error = {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ };
+ }
+
+ return logEntry;
+};
+
+/**
+ * Logs a message at the specified level
+ * @param {string} level - Log level
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ * @param {Error} [error] - Optional error object
+ */
+const log = (level, message, data = {}, error = null) => {
+ const logEntry = createLogEntry(level, message, data, error);
+ try {
+ console.log(JSON.stringify(logEntry));
+ } catch (serializationError) {
+ console.log(
+ JSON.stringify({
+ timestamp: new Date().toISOString(),
+ level: 'error',
+ message: 'Failed to serialize log entry',
+ originalMessage: message,
+ error: {
+ name: serializationError.name,
+ message: serializationError.message,
+ },
+ })
+ );
+ }
+};
+
+/**
+ * Logs a debug message
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ */
+export const debug = (message, data = {}) => {
+ log(LOG_LEVELS.DEBUG, message, data);
+};
+
+/**
+ * Logs an info message
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ */
+export const info = (message, data = {}) => {
+ log(LOG_LEVELS.INFO, message, data);
+};
+
+/**
+ * Logs a warning message
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ */
+export const warn = (message, data = {}) => {
+ log(LOG_LEVELS.WARN, message, data);
+};
+
+/**
+ * Logs an error message
+ * @param {string} message - Log message
+ * @param {Object} [data] - Optional data to include in the log
+ * @param {Error} [error] - Optional error object
+ */
+export const error = (message, data = {}, error) => {
+ log(LOG_LEVELS.ERROR, message, data, error);
+};
+
+export default {
+ debug,
+ info,
+ warn,
+ error,
+};
diff --git a/.github/actions/eng-metrics/src/markdown-parser.js b/.github/actions/eng-metrics/src/markdown-parser.js
new file mode 100644
index 0000000000..a3183c51fd
--- /dev/null
+++ b/.github/actions/eng-metrics/src/markdown-parser.js
@@ -0,0 +1,156 @@
+/**
+ * Markdown parser for extracting GitHub usernames from product groups
+ * Parses the product-groups.md file to extract developer usernames by group
+ */
+
+import fs from 'fs';
+import path from 'path';
+import logger from './logger.js';
+
+/**
+ * Parses the product groups markdown file and extracts GitHub usernames
+ * @param {string} filePath - Path to the product-groups.md file
+ * @returns {Array<{group: string, username: string}>} Array of user group mappings
+ */
+export const parseProductGroups = (filePath) => {
+ try {
+ const resolvedPath = path.resolve(process.cwd(), filePath);
+ logger.info(`Parsing product groups from ${resolvedPath}`);
+
+ if (!fs.existsSync(resolvedPath)) {
+ logger.error(`Product groups file not found at ${resolvedPath}`);
+ return [];
+ }
+
+ const content = fs.readFileSync(resolvedPath, 'utf8');
+ return extractUsernamesFromMarkdown(content);
+ } catch (err) {
+ logger.error(`Error parsing product groups file: ${filePath}`, {}, err);
+ return [];
+ }
+};
+
+/**
+ * Extracts usernames from markdown content
+ * @param {string} content - Markdown content
+ * @returns {Array<{group: string, username: string}>} Array of user group mappings
+ */
+const extractUsernamesFromMarkdown = (content) => {
+ const userGroups = [];
+
+ // Define the groups we're looking for and their corresponding database group names
+ const groupMappings = {
+ 'MDM group': 'mdm',
+ 'Orchestration group': 'orchestration',
+ 'Software group': 'software',
+ };
+
+ // For each group, find its section and extract usernames
+ for (const [sectionName, groupName] of Object.entries(groupMappings)) {
+ // Find the section for this group
+ const sectionRegex = new RegExp(
+ `### ${sectionName}([\\s\\S]*?)(?=### |$)`,
+ 'i'
+ );
+ const sectionMatch = content.match(sectionRegex);
+
+ if (sectionMatch) {
+ const sectionContent = sectionMatch[1];
+ const usernames = extractUsernamesFromSection(sectionContent, groupName);
+ userGroups.push(...usernames);
+ } else {
+ logger.warn(`Section not found: ${sectionName}`);
+ }
+ }
+
+ logger.info(
+ `Extracted ${userGroups.length} user-group mappings from markdown`
+ );
+ return userGroups;
+};
+
+/**
+ * Extracts usernames from a specific section
+ * @param {string} sectionContent - Content of the section
+ * @param {string} groupName - Name of the group (mdm, orchestration, software)
+ * @returns {Array<{group: string, username: string}>} Array of user group mappings
+ */
+const extractUsernamesFromSection = (sectionContent, groupName) => {
+ const userGroups = [];
+
+ // Look for the Developer row in the table
+ // The pattern needs to handle multi-line content in the cell
+ const developerRowMatch = sectionContent.match(
+ /\|\s*Developer\s*\|\s*([\s\S]*?)(?=\n\||\n\n|$)/
+ );
+
+ if (!developerRowMatch) {
+ logger.warn(`No Developer row found in ${groupName} group section`);
+ return userGroups;
+ }
+
+ const developerCell = developerRowMatch[1];
+
+ // Extract GitHub usernames from the developer cell
+ // Look for patterns like [@username](https://github.com/username)
+ // Note: This match could fail with slight variations in formatting (extra spaces, different brackets, etc.).
+ const usernameMatches = developerCell.match(/\[@([a-zA-Z0-9-]+)]\([^)]+\)/g);
+
+ if (!usernameMatches) {
+ logger.warn(
+ `No GitHub usernames found in ${groupName} group Developer row`
+ );
+ return userGroups;
+ }
+
+ const usernames = usernameMatches
+ .map((match) => {
+ // Extract username from _([@username](url))_ format
+ const usernameMatch = match.match(/\[@([a-zA-Z0-9-]+)]/);
+ return usernameMatch ? usernameMatch[1] : null;
+ })
+ .filter(Boolean);
+
+ logger.info(
+ `Found ${usernames.length
+ } developers in ${groupName} group: ${usernames.join(', ')}`
+ );
+
+ // Create user group mappings for both the specific group and engineering
+ for (const username of usernames) {
+ // Add to specific group (mdm, orchestration, software)
+ userGroups.push({ group: groupName, username });
+
+ // Add to engineering group (all developers are in engineering)
+ userGroups.push({ group: 'engineering', username });
+ }
+
+ return userGroups;
+};
+
+/**
+ * Validates the structure of the markdown content
+ * @param {string} content - Markdown content to validate
+ * @returns {boolean} True if structure is valid, false otherwise
+ */
+export const validateMarkdownStructure = (content) => {
+ const requiredSections = [
+ 'MDM group',
+ 'Orchestration group',
+ 'Software group',
+ ];
+
+ for (const section of requiredSections) {
+ if (!content.includes(`### ${section}`)) {
+ logger.warn(`Missing required section: ${section}`);
+ return false;
+ }
+ }
+
+ return true;
+};
+
+export default {
+ parseProductGroups,
+ validateMarkdownStructure,
+};
diff --git a/.github/actions/eng-metrics/src/metrics-collector.js b/.github/actions/eng-metrics/src/metrics-collector.js
new file mode 100644
index 0000000000..ea4fd7ced9
--- /dev/null
+++ b/.github/actions/eng-metrics/src/metrics-collector.js
@@ -0,0 +1,494 @@
+/**
+ * Engineering metrics collector module
+ * Orchestrates the collection and uploading of comprehensive GitHub engineering metrics:
+ * - Time to First Review (currently implemented)
+ * - Time to Merge (planned)
+ * - Time to QA Ready (planned)
+ * - Time to Production Ready (planned)
+ */
+
+import GitHubClient from './github-client.js';
+import BigQueryClient from './bigquery-client.js';
+import { UserGroupClient } from './user-group-client.js';
+import { parseProductGroups } from './markdown-parser.js';
+import { filterValidUserGroups } from './github-validator.js';
+import logger from './logger.js';
+
+/**
+ * Metrics collector class
+ */
+export class MetricsCollector {
+ /**
+ * Creates a new metrics collector
+ * @param {Object} config - Configuration object
+ */
+ constructor(config) {
+ this.config = config;
+ this.githubClient = null;
+ this.bigqueryClient = null;
+ this.userGroupClient = null;
+ }
+
+ /**
+ * Initializes the metrics collector
+ */
+ async initialize() {
+ try {
+ logger.info('Initializing metrics collector');
+
+ // Initialize GitHub client
+ this.githubClient = new GitHubClient(this.config.githubToken);
+
+ // Initialize BigQuery client only if not in print-only mode
+ if (!this.config.printOnly) {
+ this.bigqueryClient = new BigQueryClient(this.config.serviceAccountKeyPath);
+ } else {
+ logger.info('Running in print-only mode, BigQuery client not initialized');
+ }
+
+ // Initialize User Group client if user group processing is enabled
+ if (this.config.userGroupEnabled) {
+ if (!this.config.printOnly) {
+ // Get project ID from BigQuery client
+ const projectId = this.bigqueryClient.getProjectId();
+ this.userGroupClient = new UserGroupClient(
+ projectId,
+ this.config.bigQueryDatasetId,
+ this.config.serviceAccountKeyPath,
+ this.config.printOnly
+ );
+ } else {
+ // For print-only mode, we don't need a real project ID
+ this.userGroupClient = new UserGroupClient(
+ 'print-only-project',
+ this.config.bigQueryDatasetId,
+ this.config.serviceAccountKeyPath,
+ this.config.printOnly
+ );
+ }
+ logger.info('User group client initialized');
+ }
+
+ logger.info('Metrics collector initialized');
+ } catch (err) {
+ logger.error('Failed to initialize metrics collector', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Collects metrics for a single repository
+ * @param {string} repository - Repository in the format owner/repo
+ * @returns {Array} Array of engineering metrics
+ */
+ async collectRepositoryMetrics(repository) {
+ const [owner, repo] = repository.split('/');
+ if (!owner || !repo) {
+ const err = new Error(`Invalid repository format: ${repository}`);
+ logger.error(`Error collecting metrics for ${repository}`, {}, err);
+ return [];
+ }
+ logger.info(`Collecting metrics for ${repository}`);
+
+ try {
+ // Calculate the date to fetch PRs from (lookbackDays ago)
+ const since = new Date();
+ since.setDate(since.getDate() - this.config.lookbackDays);
+
+ // Fetch PRs updated since the lookback date
+ const pullRequests = await this.githubClient.fetchPullRequests(
+ owner,
+ repo,
+ 'all',
+ since,
+ this.config.targetBranch
+ );
+
+ logger.info(`Found ${pullRequests.length} PRs for ${repository}`);
+
+ // Collect metrics for each PR
+ const metrics = [];
+
+ for (const pr of pullRequests) {
+ try {
+ // Fetch PR timeline events (shared for all metrics)
+ const timelineEvents = await this.githubClient.fetchPRTimelineEvents(
+ owner,
+ repo,
+ pr.number
+ );
+
+ // Fetch PR review events (needed for Time to First Review)
+ const rawReviewEvents = await this.githubClient.fetchPRReviewEvents(
+ owner,
+ repo,
+ pr.number
+ );
+
+ // Filter bot reviews if configured
+ const reviewEvents = this.githubClient.filterBotReviews(
+ rawReviewEvents,
+ this.config.excludeBotReviews
+ );
+
+ // Collect enabled metrics for this PR
+ const prMetrics = await this.collectPRMetrics(pr, timelineEvents, reviewEvents);
+ metrics.push(...prMetrics);
+ } catch (err) {
+ logger.error(`Error collecting metrics for PR ${repository}#${pr.number}`, {}, err);
+ }
+ }
+
+ logger.info(`Collected ${metrics.length} metrics for ${repository}`);
+ return metrics;
+ } catch (err) {
+ logger.error(`Error collecting metrics for ${repository}`, {}, err);
+ return [];
+ }
+ }
+
+ /**
+ * Collects enabled metrics for a single PR
+ * @param {Object} pr - Pull request object
+ * @param {Array} timelineEvents - PR timeline events
+ * @param {Array} reviewEvents - PR review events
+ * @returns {Array} Array of metrics for this PR
+ */
+ async collectPRMetrics(pr, timelineEvents, reviewEvents) {
+ const metrics = [];
+
+ // Collect Time to First Review if enabled
+ if (this.config.metrics.timeToFirstReview.enabled) {
+ try {
+ const pickupTimeMetrics = this.githubClient.calculatePickupTime(
+ pr,
+ timelineEvents,
+ reviewEvents
+ );
+
+ if (pickupTimeMetrics) {
+ metrics.push(pickupTimeMetrics);
+ }
+ } catch (err) {
+ logger.error(`Error calculating Time to First Review for PR #${pr.number}`, {}, err);
+ }
+ }
+
+ // Collect Time to Merge if enabled
+ if (this.config.metrics.timeToMerge.enabled) {
+ try {
+ const mergeTimeMetrics = this.githubClient.calculateTimeToMerge(
+ pr,
+ timelineEvents,
+ reviewEvents
+ );
+
+ if (mergeTimeMetrics) {
+ metrics.push(mergeTimeMetrics);
+ }
+ } catch (err) {
+ logger.error(`Error calculating Time to Merge for PR #${pr.number}`, {}, err);
+ }
+ }
+
+ return metrics;
+ }
+
+ /**
+ * Collects metrics for all repositories
+ * @returns {Array} Array of engineering metrics
+ */
+ async collectMetrics() {
+ try {
+ logger.info('Collecting metrics for all repositories');
+
+ const allMetrics = [];
+
+ // Collect metrics for each repository
+ for (const repository of this.config.repositories) {
+ const metrics = await this.collectRepositoryMetrics(repository);
+ allMetrics.push(...metrics);
+ }
+
+ logger.info(`Collected ${allMetrics.length} metrics in total`);
+ return allMetrics;
+ } catch (err) {
+ logger.error('Error collecting metrics', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Processes user groups from the markdown file
+ * @returns {Promise}
+ */
+ async processUserGroups() {
+ if (!this.config.userGroupEnabled) {
+ logger.info('User group processing is disabled');
+ return;
+ }
+
+ try {
+ logger.info('Processing user groups');
+
+ // Parse user groups from markdown file
+ const userGroups = parseProductGroups(this.config.userGroupFilepath);
+
+ if (userGroups.length === 0) {
+ logger.warn('No user groups found in markdown file');
+ return;
+ }
+
+ // Validate GitHub usernames
+ const validUserGroups = await filterValidUserGroups(
+ this.config.githubToken,
+ userGroups
+ );
+
+ if (validUserGroups.length === 0) {
+ logger.warn('No valid user groups found after validation');
+ return;
+ }
+
+ // Sync user groups to BigQuery
+ await this.userGroupClient.syncUserGroups(validUserGroups);
+
+ logger.info(`Successfully processed ${validUserGroups.length} user group mappings`);
+ } catch (err) {
+ logger.error('Error processing user groups', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Prints metrics to the console in a readable format
+ * @param {Array} metrics - Array of engineering metrics
+ */
+ printMetrics(metrics) {
+ try {
+ if (!metrics || metrics.length === 0) {
+ logger.warn('No metrics to print');
+ return;
+ }
+
+ logger.info(`Printing ${metrics.length} metrics to console`);
+
+ // Group metrics by type for organized display
+ const metricsByType = this.groupMetricsByType(metrics);
+
+ console.log('\n=== Engineering Metrics ===\n');
+
+ // Print each metric type separately
+ for (const [metricType, typeMetrics] of Object.entries(metricsByType)) {
+ if (typeMetrics.length === 0) continue;
+
+ console.log(`--- ${this.getMetricTypeDisplayName(metricType)} (${typeMetrics.length} metrics) ---\n`);
+
+ // Sort metrics by time (descending)
+ const sortedMetrics = [...typeMetrics].sort((a, b) => {
+ const timeFieldA = this.getTimeFieldForMetricType(metricType, a);
+ const timeFieldB = this.getTimeFieldForMetricType(metricType, b);
+ return timeFieldB - timeFieldA;
+ });
+
+ // Print each metric
+ sortedMetrics.forEach((metric, index) => {
+ this.printSingleMetric(metric, index + 1);
+ });
+
+ // Print summary statistics for this metric type
+ this.printMetricTypeSummary(metricType, typeMetrics);
+ console.log('');
+ }
+
+ logger.info('Metrics printed successfully');
+ } catch (err) {
+ logger.error('Error printing metrics', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Prints a single metric to the console
+ * @param {Object} metric - Single metric object
+ * @param {number} index - Index for display
+ */
+ printSingleMetric(metric, index) {
+ console.log(`[${index}] PR: ${metric.repository}#${metric.prNumber}`);
+ console.log(` URL: ${metric.prUrl}`);
+ console.log(` Creator: ${metric.prCreator}`);
+ console.log(` Ready Time: ${metric.readyTime.toISOString()}${metric.readyEventType ? ` (${metric.readyEventType})` : ''}`);
+
+ if (metric.metricType === 'time_to_first_review') {
+ const hours = Math.floor(metric.pickupTimeSeconds / 3600);
+ const minutes = Math.floor((metric.pickupTimeSeconds % 3600) / 60);
+ const seconds = metric.pickupTimeSeconds % 60;
+
+ console.log(` First Review Time: ${metric.firstReviewTime.toISOString()}`);
+ console.log(` Pickup Time: ${hours}h ${minutes}m ${seconds}s (${metric.pickupTimeSeconds} seconds)`);
+ } else if (metric.metricType === 'time_to_merge') {
+ const hours = Math.floor(metric.mergeTimeSeconds / 3600);
+ const minutes = Math.floor((metric.mergeTimeSeconds % 3600) / 60);
+ const seconds = metric.mergeTimeSeconds % 60;
+
+ console.log(` Merge Time: ${metric.mergeTime.toISOString()}`);
+ console.log(` Time to Merge: ${hours}h ${minutes}m ${seconds}s (${metric.mergeTimeSeconds} seconds)`);
+ }
+
+ console.log('');
+ }
+
+ /**
+ * Prints summary statistics for a metric type
+ * @param {string} metricType - Type of metric
+ * @param {Array} metrics - Array of metrics of this type
+ */
+ printMetricTypeSummary(metricType, metrics) {
+ const timeField = metricType === 'time_to_first_review' ? 'pickupTimeSeconds' : 'mergeTimeSeconds';
+ const totalTime = metrics.reduce((sum, metric) => sum + metric[timeField], 0);
+ const avgTime = totalTime / metrics.length;
+ const avgHours = Math.floor(avgTime / 3600);
+ const avgMinutes = Math.floor((avgTime % 3600) / 60);
+ const avgSeconds = Math.floor(avgTime % 60);
+
+ console.log(`=== ${this.getMetricTypeDisplayName(metricType)} Summary ===`);
+ console.log(`Total PRs: ${metrics.length}`);
+ console.log(`Average Time: ${avgHours}h ${avgMinutes}m ${avgSeconds}s (${Math.floor(avgTime)} seconds)`);
+ }
+
+ /**
+ * Gets display name for metric type
+ * @param {string} metricType - Type of metric
+ * @returns {string} Display name
+ */
+ getMetricTypeDisplayName(metricType) {
+ switch (metricType) {
+ case 'time_to_first_review':
+ return 'Time to First Review';
+ case 'time_to_merge':
+ return 'Time to Merge';
+ default:
+ return metricType;
+ }
+ }
+
+ /**
+ * Gets the time field value for sorting metrics
+ * @param {string} metricType - Type of metric
+ * @param {Object} metric - Metric object
+ * @returns {number} Time value in seconds
+ */
+ getTimeFieldForMetricType(metricType, metric) {
+ switch (metricType) {
+ case 'time_to_first_review':
+ return metric.pickupTimeSeconds;
+ case 'time_to_merge':
+ return metric.mergeTimeSeconds;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Uploads metrics to BigQuery, grouped by metric type
+ * @param {Array} metrics - Array of engineering metrics
+ */
+ async uploadMetrics(metrics) {
+ try {
+ if (!metrics || metrics.length === 0) {
+ logger.warn('No metrics to upload');
+ return;
+ }
+
+ logger.info(`Uploading ${metrics.length} metrics to BigQuery`);
+
+ // Group metrics by type
+ const metricsByType = this.groupMetricsByType(metrics);
+
+ // Upload each metric type to its respective table
+ for (const [metricType, typeMetrics] of Object.entries(metricsByType)) {
+ if (typeMetrics.length === 0) continue;
+
+ const tableName = this.getTableNameForMetricType(metricType);
+ logger.info(`Uploading ${typeMetrics.length} ${metricType} metrics to table ${tableName}`);
+
+ await this.bigqueryClient.uploadMetrics(
+ this.config.bigQueryDatasetId,
+ tableName,
+ typeMetrics
+ );
+ }
+
+ logger.info('All metrics uploaded successfully');
+ } catch (err) {
+ logger.error('Error uploading metrics to BigQuery', {}, err);
+ throw err;
+ }
+ }
+
+ /**
+ * Groups metrics by their type
+ * @param {Array} metrics - Array of metrics
+ * @returns {Object} Object with metric types as keys and arrays of metrics as values
+ */
+ groupMetricsByType(metrics) {
+ return metrics.reduce((groups, metric) => {
+ const type = metric.metricType;
+ if (!groups[type]) {
+ groups[type] = [];
+ }
+ groups[type].push(metric);
+ return groups;
+ }, {});
+ }
+
+ /**
+ * Gets the table name for a specific metric type
+ * @param {string} metricType - Type of metric
+ * @returns {string} Table name
+ */
+ getTableNameForMetricType(metricType) {
+ switch (metricType) {
+ case 'time_to_first_review':
+ return this.config.metrics.timeToFirstReview.tableName;
+ case 'time_to_merge':
+ return this.config.metrics.timeToMerge.tableName;
+ default:
+ throw new Error(`Unknown metric type: ${metricType}`);
+ }
+ }
+
+ /**
+ * Runs the metrics collection and upload process
+ */
+ async run() {
+ try {
+ logger.info('Starting engineering metrics collection');
+
+ // Initialize the metrics collector
+ await this.initialize();
+
+ // Process user groups if enabled
+ await this.processUserGroups();
+
+ // Collect metrics
+ const metrics = await this.collectMetrics();
+
+ if (this.config.printOnly) {
+ // Print metrics to console
+ this.printMetrics(metrics);
+ } else {
+ // Upload metrics to BigQuery
+ await this.uploadMetrics(metrics);
+ }
+
+ logger.info('Engineering metrics collection completed successfully');
+ return metrics;
+ } catch (err) {
+ logger.error('Error running engineering metrics collection', {}, err);
+ throw err;
+ }
+ }
+}
+
+export default MetricsCollector;
diff --git a/.github/actions/eng-metrics/src/user-group-client.js b/.github/actions/eng-metrics/src/user-group-client.js
new file mode 100644
index 0000000000..f9121c54a0
--- /dev/null
+++ b/.github/actions/eng-metrics/src/user-group-client.js
@@ -0,0 +1,374 @@
+/**
+ * User Group BigQuery Client
+ * Manages the user_group BigQuery table for team organization tracking
+ */
+
+import { BigQuery } from '@google-cloud/bigquery';
+import logger from './logger.js';
+
+/**
+ * User Group BigQuery Client class
+ */
+export class UserGroupClient {
+ /**
+ * Creates a new UserGroupClient instance
+ * @param {string} projectId - Google Cloud project ID
+ * @param {string} datasetId - BigQuery dataset ID
+ * @param {string} serviceAccountKeyPath - Path to service account key file
+ * @param {boolean} printOnly - If true, print operations instead of executing them
+ */
+ constructor(projectId, datasetId, serviceAccountKeyPath, printOnly = false) {
+ this.projectId = projectId;
+ this.datasetId = datasetId;
+ this.tableId = 'user_group';
+ this.printOnly = printOnly;
+
+ if (!printOnly) {
+ this.bigquery = new BigQuery({
+ projectId,
+ keyFilename: serviceAccountKeyPath,
+ });
+ this.dataset = this.bigquery.dataset(datasetId);
+ this.table = this.dataset.table(this.tableId);
+ }
+ }
+
+ /**
+ * Creates the user_group table if it doesn't exist
+ * @returns {Promise}
+ */
+ async createUserGroupTable() {
+ if (this.printOnly) {
+ logger.info(
+ '[USER GROUPS] Print-only mode: would create user_group table with schema:'
+ );
+ logger.info('[USER GROUPS] - group (STRING, cluster key)');
+ logger.info('[USER GROUPS] - username (STRING)');
+ return;
+ }
+
+ try {
+ const [exists] = await this.table.exists();
+
+ if (exists) {
+ logger.info('user_group table already exists');
+ return;
+ }
+
+ const schema = [
+ { name: 'group', type: 'STRING', mode: 'REQUIRED' },
+ { name: 'username', type: 'STRING', mode: 'REQUIRED' },
+ ];
+
+ const options = {
+ schema,
+ clustering: {
+ fields: ['group'],
+ },
+ };
+
+ await this.table.create(options);
+ logger.info('Created user_group table with clustering on group field');
+ } catch (error) {
+ logger.error('Error creating user_group table:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Syncs user groups to the BigQuery table (replaces all data)
+ * @param {Array<{group: string, username: string}>} userGroups - Array of user group mappings
+ * @returns {Promise}
+ */
+ async syncUserGroups(userGroups) {
+ if (this.printOnly) {
+ this.printUserGroupsData(userGroups);
+ return;
+ }
+
+ try {
+ if (userGroups.length === 0) {
+ logger.warn('No user groups to sync');
+ return;
+ }
+
+ // Ensure table exists first
+ await this.createUserGroupTable();
+
+ // Small delay to ensure table is ready
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ // Refresh table reference
+ this.table = this.dataset.table(this.tableId);
+
+ // Get existing entries and perform differential sync
+ await this.syncUserGroupsDifferential(userGroups);
+
+ logger.info(
+ `Successfully synced ${userGroups.length} user group mappings to BigQuery`
+ );
+ } catch (error) {
+ logger.error('Error syncing user groups:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Performs differential sync of user groups (only insert new, delete removed)
+ * @param {Array<{group: string, username: string}>} newUserGroups - New user group mappings
+ * @returns {Promise}
+ */
+ async syncUserGroupsDifferential(newUserGroups) {
+ if (this.printOnly) {
+ logger.info(
+ '[USER GROUPS] Print-only mode: would perform differential sync'
+ );
+ this.printDifferentialSync(newUserGroups);
+ return;
+ }
+
+ try {
+ // Get existing entries from the table
+ const existingUserGroups = await this.getExistingUserGroups();
+
+ // Create sets for comparison
+ const existingSet = new Set(
+ existingUserGroups.map((ug) => `${ug.group}:${ug.username}`)
+ );
+ const newSet = new Set(
+ newUserGroups.map((ug) => `${ug.group}:${ug.username}`)
+ );
+
+ // Find entries to insert (in new but not in existing)
+ const toInsert = newUserGroups.filter(
+ (ug) => !existingSet.has(`${ug.group}:${ug.username}`)
+ );
+
+ // Find entries to delete (in existing but not in new)
+ const toDelete = existingUserGroups.filter(
+ (ug) => !newSet.has(`${ug.group}:${ug.username}`)
+ );
+
+ logger.info(
+ `Differential sync: ${toInsert.length} to insert, ${toDelete.length} to delete`
+ );
+
+ // Handle deletions with streaming buffer awareness
+ if (toDelete.length > 0) {
+ try {
+ await this.deleteUserGroups(toDelete);
+ } catch (error) {
+ if (error.message?.includes('streaming buffer')) {
+ logger.warn(
+ 'Streaming buffer prevents DELETE - this is expected during testing. In production (daily runs), this won\'t occur.'
+ );
+ logger.info(
+ 'Skipping deletions for now. Data will be consistent on next daily run.'
+ );
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ // Insert new entries using load job (no streaming buffer issues for inserts)
+ if (toInsert.length > 0) {
+ await this.insertUserGroups(toInsert);
+ }
+
+ if (toInsert.length === 0 && toDelete.length === 0) {
+ logger.info('No changes needed - user groups are already up to date');
+ }
+ } catch (error) {
+ logger.error('Error performing differential sync:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Gets existing user groups from the BigQuery table
+ * @returns {Promise>}
+ */
+ async getExistingUserGroups() {
+ try {
+ const query = `SELECT \`group\`, username FROM \`${this.projectId}.${this.datasetId}.${this.tableId}\``;
+ const [rows] = await this.bigquery.query({ query });
+
+ logger.info(`Found ${rows.length} existing user group entries`);
+ return rows.map((row) => ({
+ group: row.group,
+ username: row.username,
+ }));
+ } catch (error) {
+ if (error.message?.includes('not found')) {
+ logger.info('Table does not exist yet, no existing entries');
+ return [];
+ }
+ logger.error('Error getting existing user groups:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Deletes specific user groups from the BigQuery table
+ * @param {Array<{group: string, username: string}>} userGroupsToDelete
+ * @returns {Promise}
+ */
+ async deleteUserGroups(userGroupsToDelete) {
+ try {
+ const groups = userGroupsToDelete.map((ug) => ug.group);
+ const usernames = userGroupsToDelete.map((ug) => ug.username);
+
+ const query = `
+ DELETE
+ FROM \`${this.projectId}.${this.datasetId}.${this.tableId}\`
+ WHERE EXISTS (SELECT 1
+ FROM UNNEST(@groups) AS g WITH OFFSET AS pos
+ JOIN UNNEST(@usernames) AS u WITH OFFSET AS upos
+ ON pos = upos
+ WHERE \`group\` = g AND username = u
+ )
+ `;
+
+ const options = {
+ query,
+ params: { groups, usernames },
+ };
+
+ await this.bigquery.query(options);
+
+ logger.info(`Deleted ${userGroupsToDelete.length} user group entries`);
+ } catch (error) {
+ logger.error('Error deleting user groups:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Prints differential sync information for print-only mode
+ * @param {Array<{group: string, username: string}>} newUserGroups
+ */
+ async printDifferentialSync(newUserGroups) {
+ logger.info(
+ '[USER GROUPS] Print-only mode: would perform differential sync'
+ );
+ logger.info('[USER GROUPS] Would check existing entries in table');
+ logger.info(
+ `[USER GROUPS] Would compare with ${newUserGroups.length} new entries`
+ );
+ logger.info(
+ '[USER GROUPS] Would insert only new entries and delete removed entries'
+ );
+
+ // Group users by their groups for better display
+ const groupedUsers = newUserGroups.reduce((acc, ug) => {
+ if (!acc[ug.group]) {
+ acc[ug.group] = [];
+ }
+ acc[ug.group].push(ug.username);
+ return acc;
+ }, {});
+
+ logger.info('[USER GROUPS] New user groups to sync:');
+ for (const [group, usernames] of Object.entries(groupedUsers)) {
+ logger.info(` ${group}: ${usernames.join(', ')}`);
+ }
+ }
+
+ /**
+ * Inserts user groups into the BigQuery table using load job (avoids streaming buffer)
+ * @param {Array<{group: string, username: string}>} userGroups - Array of user group mappings
+ * @returns {Promise}
+ */
+ async insertUserGroups(userGroups) {
+ if (this.printOnly) {
+ logger.info(
+ `[USER GROUPS] Print-only mode: would insert ${userGroups.length} records`
+ );
+ return;
+ }
+
+ try {
+ // Transform data for BigQuery streaming insert
+ const rows = userGroups.map((ug) => ({
+ group: ug.group,
+ username: ug.username,
+ }));
+
+ // Use streaming insert - simpler and appropriate for small user group datasets
+ // Streaming buffer issues only occur during rapid testing, not daily production runs
+ await this.table.insert(rows);
+
+ logger.info(
+ `Inserted ${rows.length} user group mappings into BigQuery using streaming insert`
+ );
+ } catch (error) {
+ logger.error('Error inserting user groups:', {}, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Prints user groups data in a readable format (for print-only mode)
+ * @param {Array<{group: string, username: string}>} userGroups - Array of user group mappings
+ */
+ printUserGroupsData(userGroups) {
+ logger.info(
+ '[USER GROUPS] Print-only mode enabled - no BigQuery updates will be performed'
+ );
+ logger.info('');
+
+ if (userGroups.length === 0) {
+ logger.info('[USER GROUPS] No user groups found to process');
+ return;
+ }
+
+ // Group users by their groups for better display
+ const groupedUsers = userGroups.reduce((acc, ug) => {
+ if (!acc[ug.group]) {
+ acc[ug.group] = [];
+ }
+ acc[ug.group].push(ug.username);
+ return acc;
+ }, {});
+
+ logger.info('[USER GROUPS] Extracted user groups:');
+ for (const [group, usernames] of Object.entries(groupedUsers)) {
+ logger.info(` ${group}: ${usernames.join(', ')}`);
+ }
+
+ logger.info('');
+ logger.info(
+ `[USER GROUPS] Would insert ${userGroups.length} records into user_group table:`
+ );
+
+ // Count users per group
+ const groupCounts = Object.entries(groupedUsers).map(
+ ([group, usernames]) => ` ${group}: ${usernames.length} users`
+ );
+ logger.info(groupCounts.join('\n'));
+
+ logger.info('');
+ logger.info('[USER GROUPS] Sample records that would be inserted:');
+ logger.info('| group | username |');
+ logger.info('|---------------|-----------------|');
+
+ // Show first few records from each group
+ const sampleRecords = [];
+ for (const [group, usernames] of Object.entries(groupedUsers)) {
+ const sampleUser = usernames[0];
+ sampleRecords.push(`| ${group.padEnd(13)} | ${sampleUser.padEnd(15)} |`);
+ if (sampleRecords.length >= 6) break; // Limit sample output
+ }
+
+ sampleRecords.forEach((record) => logger.info(record));
+
+ if (userGroups.length > sampleRecords.length) {
+ logger.info(
+ `... and ${userGroups.length - sampleRecords.length} more records`
+ );
+ }
+ }
+}
+
+export default UserGroupClient;
diff --git a/.github/actions/eng-metrics/test/bigquery-client.test.js b/.github/actions/eng-metrics/test/bigquery-client.test.js
new file mode 100644
index 0000000000..c635ce026f
--- /dev/null
+++ b/.github/actions/eng-metrics/test/bigquery-client.test.js
@@ -0,0 +1,275 @@
+/**
+ * Tests for BigQuery client module
+ */
+
+import { jest } from '@jest/globals';
+import { BigQueryClient } from '../src/bigquery-client.js';
+
+// Mock the logger
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+}));
+
+// Mock fs
+jest.mock('fs', () => ({
+ existsSync: jest.fn(() => true),
+ readFileSync: jest.fn(() => JSON.stringify({
+ project_id: 'test-project-id',
+ type: 'service_account',
+ private_key_id: 'key-id',
+ private_key: '-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----\n',
+ client_email: 'test@test-project-id.iam.gserviceaccount.com',
+ client_id: '123456789',
+ auth_uri: 'https://accounts.google.com/o/oauth2/auth',
+ token_uri: 'https://oauth2.googleapis.com/token'
+ }))
+}));
+
+// Mock @google-cloud/bigquery
+jest.mock('@google-cloud/bigquery', () => ({
+ BigQuery: jest.fn(() => ({
+ dataset: jest.fn(),
+ query: jest.fn()
+ }))
+}));
+
+describe('BigQueryClient', () => {
+ let bigqueryClient;
+ let mockBigQuery;
+ let mockDataset;
+ let mockTable;
+
+ beforeEach(() => {
+ mockTable = {
+ exists: jest.fn(() => [true]),
+ create: jest.fn(),
+ insert: jest.fn(() => [{}])
+ };
+
+ mockDataset = {
+ exists: jest.fn(() => [true]),
+ create: jest.fn(),
+ table: jest.fn(() => mockTable)
+ };
+
+ mockBigQuery = {
+ dataset: jest.fn(() => mockDataset),
+ query: jest.fn(() => [[]])
+ };
+
+ // Create client without calling constructor to avoid file check
+ bigqueryClient = Object.create(BigQueryClient.prototype);
+ bigqueryClient.bigquery = mockBigQuery;
+ bigqueryClient.projectId = 'test-project-id';
+ });
+
+
+ describe('getProjectId', () => {
+ test('should return the extracted project ID', () => {
+ expect(bigqueryClient.getProjectId()).toBe('test-project-id');
+ });
+ });
+
+ describe('getSchemaForMetricType', () => {
+ test('should return first_review table schema', () => {
+ const schema = bigqueryClient.getSchemaForMetricType('time_to_first_review');
+ expect(schema.fields).toBeTruthy();
+ });
+
+ test('should return pr_merge table schema', () => {
+ const schema = bigqueryClient.getSchemaForMetricType('time_to_merge');
+ expect(schema.fields).toBeTruthy();
+ });
+
+ test('should throw error for unknown metric type', () => {
+ expect(() => {
+ bigqueryClient.getSchemaForMetricType('unknown_metric');
+ }).toThrow('Unknown metric type: unknown_metric');
+ });
+ });
+
+ describe('getConfigurationForMetricType', () => {
+ test('should return first_review table configuration', () => {
+ const config = bigqueryClient.getConfigurationForMetricType('time_to_first_review');
+ expect(config).toBeTruthy();
+ });
+
+ test('should return pr_merge table configuration', () => {
+ const config = bigqueryClient.getConfigurationForMetricType('time_to_merge');
+ expect(config).toBeTruthy();
+ });
+
+ test('should throw error for unknown metric type configuration', () => {
+ expect(() => {
+ bigqueryClient.getConfigurationForMetricType('unknown_metric');
+ }).toThrow('Unknown metric type for table configuration: unknown_metric');
+ });
+ });
+
+ describe('transformMetricsToRow', () => {
+ test('should transform time_to_first_review metrics', () => {
+ const metrics = {
+ metricType: 'time_to_first_review',
+ reviewDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ pickupTimeSeconds: 7200,
+ repository: 'owner/repo',
+ prNumber: 123,
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ firstReviewTime: new Date('2023-06-15T12:00:00Z')
+ };
+
+ const row = bigqueryClient.transformMetricsToRow(metrics);
+
+ expect(row).toEqual({
+ review_date: '2023-06-15',
+ pr_creator: 'testuser',
+ pr_url: 'https://github.com/owner/repo/pull/123',
+ pickup_time_seconds: 7200,
+ repository: 'owner/repo',
+ pr_number: 123,
+ target_branch: 'main',
+ ready_time: '2023-06-15T10:00:00.000Z',
+ first_review_time: '2023-06-15T12:00:00.000Z'
+ });
+ });
+
+ test('should transform time_to_merge metrics', () => {
+ const metrics = {
+ metricType: 'time_to_merge',
+ mergeDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ mergeTimeSeconds: 16200,
+ repository: 'owner/repo',
+ prNumber: 123,
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ mergeTime: new Date('2023-06-15T14:30:00Z')
+ };
+
+ const row = bigqueryClient.transformMetricsToRow(metrics);
+
+ expect(row).toEqual({
+ merge_date: '2023-06-15',
+ pr_creator: 'testuser',
+ pr_url: 'https://github.com/owner/repo/pull/123',
+ merge_time_seconds: 16200,
+ repository: 'owner/repo',
+ pr_number: 123,
+ target_branch: 'main',
+ ready_time: '2023-06-15T10:00:00.000Z',
+ merge_time: '2023-06-15T14:30:00.000Z'
+ });
+ });
+
+ test('should throw error for unknown metric type', () => {
+ const metrics = {
+ metricType: 'unknown_type'
+ };
+
+ expect(() => {
+ bigqueryClient.transformMetricsToRow(metrics);
+ }).toThrow('Unknown metric type: unknown_type');
+ });
+ });
+
+ describe('createTableIfNotExists', () => {
+ test('should create table with correct configuration for first_review', async () => {
+ mockTable.exists.mockResolvedValue([false]);
+ const schema = { fields: [] };
+
+ await bigqueryClient.createTableIfNotExists('test_dataset', 'first_review', schema, 'time_to_first_review');
+
+ expect(mockTable.create).toHaveBeenCalled();
+ });
+
+ test('should create table with correct configuration for pr_merge', async () => {
+ mockTable.exists.mockResolvedValue([false]);
+ const schema = { fields: [] };
+
+ await bigqueryClient.createTableIfNotExists('test_dataset', 'pr_merge', schema, 'time_to_merge');
+
+ expect(mockTable.create).toHaveBeenCalled();
+ });
+
+ test('should not create table if it already exists', async () => {
+ mockTable.exists.mockResolvedValue([true]);
+ const schema = { fields: [] };
+
+ await bigqueryClient.createTableIfNotExists('test_dataset', 'first_review', schema, 'time_to_first_review');
+
+ expect(mockTable.create).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('uploadMetrics', () => {
+ test('should upload metrics with correct schema', async () => {
+ const metrics = [
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ reviewDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ pickupTimeSeconds: 7200,
+ repository: 'owner/repo',
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ firstReviewTime: new Date('2023-06-15T12:00:00Z')
+ }
+ ];
+
+ await bigqueryClient.uploadMetrics('test_dataset', 'pr_first_review', metrics);
+
+ expect(mockTable.insert).toHaveBeenCalledWith([
+ {
+ review_date: '2023-06-15',
+ pr_creator: 'testuser',
+ pr_url: 'https://github.com/owner/repo/pull/123',
+ pickup_time_seconds: 7200,
+ repository: 'owner/repo',
+ pr_number: 123,
+ target_branch: 'main',
+ ready_time: '2023-06-15T10:00:00.000Z',
+ first_review_time: '2023-06-15T12:00:00.000Z'
+ }
+ ]);
+ });
+
+ test('should handle empty metrics array', async () => {
+ await bigqueryClient.uploadMetrics('test_dataset', 'pr_first_review', []);
+
+ expect(mockTable.insert).not.toHaveBeenCalled();
+ });
+
+ test('should filter out existing metrics', async () => {
+ const metrics = [
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ reviewDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ pickupTimeSeconds: 7200,
+ repository: 'owner/repo',
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ firstReviewTime: new Date('2023-06-15T12:00:00Z')
+ }
+ ];
+
+ // Mock existing metrics check to return that PR 123 already exists
+ mockBigQuery.query.mockResolvedValue([[{ pr_number: 123 }]]);
+
+ await bigqueryClient.uploadMetrics('test_dataset', 'pr_first_review', metrics);
+
+ expect(mockTable.insert).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/.github/actions/eng-metrics/test/bot-detection.test.js b/.github/actions/eng-metrics/test/bot-detection.test.js
new file mode 100644
index 0000000000..ac7b1612af
--- /dev/null
+++ b/.github/actions/eng-metrics/test/bot-detection.test.js
@@ -0,0 +1,210 @@
+/**
+ * Tests for bot detection functionality
+ */
+
+import { jest } from '@jest/globals';
+
+// Mock the logger
+const mockLogger = {
+ info: jest.fn(),
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn()
+};
+
+jest.unstable_mockModule('../src/logger.js', () => ({
+ default: mockLogger
+}));
+
+const { GitHubClient } = await import('../src/github-client.js');
+
+describe('Bot Detection', () => {
+ let githubClient;
+
+ beforeEach(() => {
+ githubClient = new GitHubClient('fake-token');
+ jest.clearAllMocks();
+ });
+
+ describe('identifyBotUser', () => {
+ test('should identify GitHub API Bot type', () => {
+ const botUser = {
+ login: 'coderabbitai[bot]',
+ type: 'Bot',
+ name: 'CodeRabbit AI',
+ bio: 'AI-powered code review assistant'
+ };
+
+ // Access the function through the module (it's not exported as a method)
+ // We'll test it indirectly through filterBotReviews
+ const reviews = [
+ {
+ user: botUser,
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, true);
+ expect(filtered).toHaveLength(0);
+ });
+
+ test('should identify bot by username patterns', () => {
+ const testCases = [
+ { login: 'dependabot[bot]', type: 'User' },
+ { login: 'renovate[bot]', type: 'User' },
+ { login: 'github-actions[bot]', type: 'User' },
+ { login: 'codecov-commenter', type: 'User' },
+ { login: 'coderabbitai', type: 'User' },
+ { login: 'sonarcloud[bot]', type: 'User' },
+ { login: 'snyk-bot', type: 'User' },
+ { login: 'greenkeeper[bot]', type: 'User' },
+ { login: 'semantic-release-bot', type: 'User' },
+ { login: 'stale[bot]', type: 'User' },
+ { login: 'imgbot[bot]', type: 'User' },
+ { login: 'allcontributors[bot]', type: 'User' },
+ { login: 'whitesource-bolt', type: 'User' },
+ { login: 'deepsource-autofix[bot]', type: 'User' }
+ ];
+
+ testCases.forEach(({ login, type }) => {
+ const reviews = [
+ {
+ user: { login, type },
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, true);
+ expect(filtered).toHaveLength(0);
+ });
+ });
+
+ test('should not filter human users', () => {
+ const humanUser = {
+ login: 'johndoe',
+ type: 'User',
+ name: 'John Doe',
+ bio: 'Software engineer'
+ };
+
+ const reviews = [
+ {
+ user: humanUser,
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, true);
+ expect(filtered).toHaveLength(1);
+ expect(filtered[0].user.login).toBe('johndoe');
+ });
+
+ test('should not filter when excludeBots is false', () => {
+ const botUser = {
+ login: 'coderabbitai[bot]',
+ type: 'Bot'
+ };
+
+ const humanUser = {
+ login: 'johndoe',
+ type: 'User'
+ };
+
+ const reviews = [
+ {
+ user: botUser,
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ },
+ {
+ user: humanUser,
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T13:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, false);
+ expect(filtered).toHaveLength(2);
+ });
+
+ test('should handle mixed bot and human reviews', () => {
+ const reviews = [
+ {
+ user: { login: 'coderabbitai[bot]', type: 'Bot' },
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ },
+ {
+ user: { login: 'johndoe', type: 'User' },
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T13:00:00Z'
+ },
+ {
+ user: { login: 'dependabot[bot]', type: 'User' },
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T14:00:00Z'
+ },
+ {
+ user: { login: 'janedoe', type: 'User' },
+ state: 'CHANGES_REQUESTED',
+ submitted_at: '2023-06-15T15:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, true);
+ expect(filtered).toHaveLength(2);
+ expect(filtered[0].user.login).toBe('johndoe');
+ expect(filtered[1].user.login).toBe('janedoe');
+ });
+
+ test('should log filtering activity', () => {
+ const reviews = [
+ {
+ user: { login: 'coderabbitai[bot]', type: 'Bot' },
+ state: 'COMMENTED',
+ submitted_at: '2023-06-15T12:00:00Z'
+ },
+ {
+ user: { login: 'johndoe', type: 'User' },
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T13:00:00Z'
+ }
+ ];
+
+ githubClient.filterBotReviews(reviews, true);
+
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ 'Filtered out 1 bot reviews from 2 total reviews'
+ );
+ });
+
+ test('should handle empty review array', () => {
+ const filtered = githubClient.filterBotReviews([], true);
+ expect(filtered).toHaveLength(0);
+ });
+
+ test('should handle reviews with no bots', () => {
+ const reviews = [
+ {
+ user: { login: 'johndoe', type: 'User' },
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T13:00:00Z'
+ },
+ {
+ user: { login: 'janedoe', type: 'User' },
+ state: 'CHANGES_REQUESTED',
+ submitted_at: '2023-06-15T15:00:00Z'
+ }
+ ];
+
+ const filtered = githubClient.filterBotReviews(reviews, true);
+ expect(filtered).toHaveLength(2);
+ expect(mockLogger.info).not.toHaveBeenCalledWith(
+ expect.stringContaining('Filtered out')
+ );
+ });
+ });
+});
\ No newline at end of file
diff --git a/.github/actions/eng-metrics/test/config.test.js b/.github/actions/eng-metrics/test/config.test.js
new file mode 100644
index 0000000000..33d4f2d4d2
--- /dev/null
+++ b/.github/actions/eng-metrics/test/config.test.js
@@ -0,0 +1,421 @@
+/**
+ * Tests for configuration module
+ */
+
+import { jest } from "@jest/globals";
+
+// Mock the logger
+const mockLogger = {
+ default: {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ },
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+};
+
+// Mock fs
+const mockFs = {
+ default: {
+ existsSync: jest.fn(),
+ readFileSync: jest.fn(),
+ },
+ existsSync: jest.fn(),
+ readFileSync: jest.fn(),
+};
+
+// Mock dotenv
+const mockDotenv = {
+ default: {
+ config: jest.fn(),
+ },
+ config: jest.fn(),
+};
+
+// Mock modules before importing
+jest.unstable_mockModule("../src/logger.js", () => mockLogger);
+jest.unstable_mockModule("fs", () => mockFs);
+jest.unstable_mockModule("dotenv", () => mockDotenv);
+
+// Now import the module under test
+const { loadConfig, validateConfig } = await import("../src/config.js");
+
+describe("Config", () => {
+ let originalEnv;
+
+ beforeEach(() => {
+ // Save original environment
+ originalEnv = { ...process.env };
+
+ // Clear environment variables. This is the recommended approach to prevent performance hit.
+ Reflect.deleteProperty(process.env, "GITHUB_TOKEN");
+ Reflect.deleteProperty(process.env, "SERVICE_ACCOUNT_KEY_PATH");
+ Reflect.deleteProperty(process.env, "BIGQUERY_DATASET_ID");
+ Reflect.deleteProperty(process.env, "REPOSITORIES");
+ Reflect.deleteProperty(process.env, "LOOKBACK_DAYS");
+ Reflect.deleteProperty(process.env, "TARGET_BRANCH");
+ Reflect.deleteProperty(process.env, "PRINT_ONLY");
+ Reflect.deleteProperty(process.env, "ENABLED_METRICS");
+ Reflect.deleteProperty(process.env, "TIME_TO_FIRST_REVIEW_TABLE");
+ Reflect.deleteProperty(process.env, "TIME_TO_MERGE_TABLE");
+
+ // Reset all mocks
+ jest.clearAllMocks();
+
+ // Mock fs.existsSync to return true by default (for config.json and user group file)
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.default.existsSync.mockReturnValue(true);
+
+ // Mock fs.readFileSync to return a default config
+ mockFs.readFileSync.mockReturnValue(
+ JSON.stringify({
+ repositories: ["owner/repo1", "owner/repo2"],
+ targetBranch: "main",
+ bigQueryDatasetId: "test_dataset",
+ lookbackDays: 5,
+ serviceAccountKeyPath: "./service-account-key.json",
+ printOnly: false,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ })
+ );
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe("loadConfig", () => {
+ test("should load default configuration", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = "owner/repo1,owner/repo2";
+
+ const config = loadConfig();
+
+ expect(config).toEqual({
+ githubToken: "test-token",
+ serviceAccountKeyPath: "/path/to/key.json",
+ bigQueryDatasetId: "test_dataset",
+ repositories: ["owner/repo1", "owner/repo2"],
+ lookbackDays: 5,
+ targetBranch: "main",
+ printOnly: false,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ excludeBotReviews: true,
+ metrics: {
+ timeToFirstReview: {
+ enabled: true,
+ tableName: "pr_first_review",
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: "pr_merge",
+ },
+ },
+ });
+ });
+
+ test("should override defaults with environment variables", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = "owner/repo";
+ process.env.LOOKBACK_DAYS = "14";
+ process.env.TARGET_BRANCH = "develop";
+ process.env.PRINT_ONLY = "true";
+ process.env.ENABLED_METRICS = "time_to_first_review";
+ process.env.TIME_TO_FIRST_REVIEW_TABLE = "custom_first_review";
+ process.env.TIME_TO_MERGE_TABLE = "custom_pr_merge";
+
+ const config = loadConfig();
+
+ expect(config).toEqual({
+ githubToken: "test-token",
+ serviceAccountKeyPath: "/path/to/key.json",
+ bigQueryDatasetId: "test_dataset",
+ repositories: ["owner/repo"],
+ lookbackDays: 5,
+ targetBranch: "develop",
+ printOnly: true,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ excludeBotReviews: true,
+ metrics: {
+ timeToFirstReview: {
+ enabled: true,
+ tableName: "custom_first_review",
+ },
+ timeToMerge: {
+ enabled: false,
+ tableName: "custom_pr_merge",
+ },
+ },
+ });
+ });
+
+ test("should handle multiple enabled metrics", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = "owner/repo";
+ process.env.ENABLED_METRICS = "time_to_first_review,time_to_merge";
+
+ const config = loadConfig();
+
+ expect(config.metrics).toEqual({
+ timeToFirstReview: {
+ enabled: true,
+ tableName: "pr_first_review",
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: "pr_merge",
+ },
+ });
+ });
+
+ test("should handle only time_to_merge enabled", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = "owner/repo";
+ process.env.ENABLED_METRICS = "time_to_merge";
+
+ const config = loadConfig();
+
+ expect(config.metrics).toEqual({
+ timeToFirstReview: {
+ enabled: false,
+ tableName: "pr_first_review",
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: "pr_merge",
+ },
+ });
+ });
+
+ test("should trim whitespace from repositories", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = " owner/repo1 , owner/repo2 , owner/repo3 ";
+
+ const config = loadConfig();
+
+ expect(config.repositories).toEqual([
+ "owner/repo1",
+ "owner/repo2",
+ "owner/repo3",
+ ]);
+ });
+
+ test("should trim whitespace from enabled metrics", () => {
+ process.env.GITHUB_TOKEN = "test-token";
+ process.env.SERVICE_ACCOUNT_KEY_PATH = "/path/to/key.json";
+ process.env.BIGQUERY_DATASET_ID = "test_dataset";
+ process.env.REPOSITORIES = "owner/repo";
+ process.env.ENABLED_METRICS = " time_to_first_review , time_to_merge ";
+
+ const config = loadConfig();
+
+ expect(config.metrics).toEqual({
+ timeToFirstReview: {
+ enabled: true,
+ tableName: "pr_first_review",
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: "pr_merge",
+ },
+ });
+ });
+ });
+
+ describe("validateConfig", () => {
+ const baseValidConfig = {
+ githubToken: "test-token",
+ serviceAccountKeyPath: "/path/to/key.json",
+ bigQueryDatasetId: "test_dataset",
+ repositories: ["owner/repo"],
+ lookbackDays: 30,
+ targetBranch: "main",
+ printOnly: false,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ excludeBotReviews: true,
+ metrics: {
+ timeToFirstReview: {
+ enabled: true,
+ tableName: "pr_first_review",
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: "pr_merge",
+ },
+ },
+ };
+
+ test("should validate correct configuration", () => {
+ expect(validateConfig(baseValidConfig)).toBe(true);
+ });
+
+ test("should return false for missing GitHub token", () => {
+ const config = { ...baseValidConfig, githubToken: "" };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should return false for missing service account key path in non-print mode", () => {
+ const config = { ...baseValidConfig, serviceAccountKeyPath: "" };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should not require service account key path in print-only mode", () => {
+ const config = {
+ ...baseValidConfig,
+ serviceAccountKeyPath: "",
+ printOnly: true,
+ };
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should not require BigQuery dataset ID in print-only mode", () => {
+ const config = {
+ ...baseValidConfig,
+ bigQueryDatasetId: "",
+ printOnly: true,
+ };
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should return false for empty repositories array", () => {
+ const config = { ...baseValidConfig, repositories: [] };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should return false for invalid repository format", () => {
+ const config = { ...baseValidConfig, repositories: ["invalid-repo"] };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should validate lookback days correctly", () => {
+ const config = { ...baseValidConfig, lookbackDays: 5 };
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should return false when no metrics are enabled", () => {
+ const config = {
+ ...baseValidConfig,
+ metrics: {
+ timeToFirstReview: { enabled: false, tableName: "pr_first_review" },
+ timeToMerge: { enabled: false, tableName: "pr_merge" },
+ },
+ };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should return false for missing table name when metric is enabled", () => {
+ const config = {
+ ...baseValidConfig,
+ metrics: {
+ timeToFirstReview: { enabled: true, tableName: "" },
+ timeToMerge: { enabled: false, tableName: "pr_merge" },
+ },
+ };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should allow missing table name when metric is disabled", () => {
+ const config = {
+ ...baseValidConfig,
+ metrics: {
+ timeToFirstReview: { enabled: false, tableName: "" },
+ timeToMerge: { enabled: true, tableName: "pr_merge" },
+ },
+ };
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should validate multiple valid repositories", () => {
+ const config = {
+ ...baseValidConfig,
+ repositories: ["owner1/repo1", "owner2/repo2", "owner3/repo3"],
+ };
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should throw error for mixed valid and invalid repositories", () => {
+ const config = {
+ ...baseValidConfig,
+ repositories: ["owner1/repo1", "invalid-repo", "owner3/repo3"],
+ };
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should validate when userGroupEnabled is true and file exists", () => {
+ const config = {
+ ...baseValidConfig,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ };
+
+ // Mock file exists - need to reset and set up the mock properly
+ mockFs.existsSync.mockReset();
+ mockFs.default.existsSync.mockReset();
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.default.existsSync.mockReturnValue(true);
+
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should return false when userGroupEnabled is true but file does not exist", () => {
+ const config = {
+ ...baseValidConfig,
+ userGroupEnabled: true,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ };
+
+ // Mock file does not exist
+ mockFs.existsSync.mockReset();
+ mockFs.default.existsSync.mockReset();
+ mockFs.existsSync.mockReturnValue(false);
+ mockFs.default.existsSync.mockReturnValue(false);
+
+ expect(validateConfig(config)).toBe(false);
+ });
+
+ test("should validate when userGroupEnabled is false regardless of file existence", () => {
+ const config = {
+ ...baseValidConfig,
+ userGroupEnabled: false,
+ userGroupFilepath: "../../../handbook/company/product-groups.md",
+ };
+
+ // Mock file does not exist
+ mockFs.existsSync.mockReset();
+ mockFs.default.existsSync.mockReset();
+ mockFs.existsSync.mockReturnValue(false);
+ mockFs.default.existsSync.mockReturnValue(false);
+
+ expect(validateConfig(config)).toBe(true);
+ });
+
+ test("should return false when userGroupEnabled is true but userGroupFilepath is not set", () => {
+ const config = {
+ ...baseValidConfig,
+ userGroupEnabled: true,
+ userGroupFilepath: undefined,
+ };
+
+ expect(validateConfig(config)).toBe(false);
+ });
+ });
+});
diff --git a/.github/actions/eng-metrics/test/end-to-end.test.js b/.github/actions/eng-metrics/test/end-to-end.test.js
new file mode 100644
index 0000000000..d7f098919c
--- /dev/null
+++ b/.github/actions/eng-metrics/test/end-to-end.test.js
@@ -0,0 +1,232 @@
+/**
+ * Tests the complete workflow from configuration to metric collection
+ */
+
+import { jest } from '@jest/globals';
+import { loadConfig } from '../src/config.js';
+import { MetricsCollector } from '../src/metrics-collector.js';
+
+// Mock the logger
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+}));
+
+// Mock GitHubClient
+jest.mock('../src/github-client.js', () => {
+ return jest.fn().mockImplementation(() => ({
+ fetchPullRequests: jest.fn(),
+ fetchPRTimelineEvents: jest.fn(),
+ fetchPRReviewEvents: jest.fn(),
+ filterBotReviews: jest.fn((reviews, excludeBots) => excludeBots ? [] : reviews),
+ calculatePickupTime: jest.fn(),
+ calculateTimeToMerge: jest.fn()
+ }));
+});
+
+// Mock BigQueryClient
+jest.mock('../src/bigquery-client.js', () => ({
+ BigQueryClient: jest.fn().mockImplementation(() => ({
+ uploadMetrics: jest.fn()
+ }))
+}));
+
+describe('End-to-End Time to Merge Workflow', () => {
+ beforeEach(() => {
+ // Set environment variables for testing
+ process.env.GITHUB_TOKEN = 'test-token';
+ process.env.SERVICE_ACCOUNT_KEY_PATH = '/fake/path';
+ process.env.BIGQUERY_DATASET_ID = 'test_dataset';
+ process.env.REPOSITORIES = 'owner/repo';
+ process.env.PRINT_ONLY = 'true';
+ });
+
+ test('should collect both Time to First Review and Time to Merge metrics', async () => {
+ // Load configuration
+ const config = loadConfig();
+
+ // Verify both metrics are enabled
+ expect(config.metrics.timeToFirstReview.enabled).toBe(true);
+ expect(config.metrics.timeToMerge.enabled).toBe(true);
+
+ // Create metrics collector
+ const metricsCollector = new MetricsCollector(config);
+
+ // Mock the GitHub client methods
+ metricsCollector.githubClient = {
+ fetchPullRequests: jest.fn().mockResolvedValue([
+ {
+ number: 123,
+ html_url: 'https://github.com/owner/repo/pull/123',
+ user: { login: 'testuser' },
+ base: {
+ ref: 'main',
+ repo: {
+ owner: { login: 'owner' },
+ name: 'repo'
+ }
+ },
+ head: { repo: { full_name: 'owner/repo' } },
+ state: 'closed',
+ merged_at: '2023-06-15T14:30:00Z'
+ }
+ ]),
+ fetchPRTimelineEvents: jest.fn().mockResolvedValue([
+ {
+ event: 'ready_for_review',
+ created_at: '2023-06-15T10:00:00Z'
+ }
+ ]),
+ fetchPRReviewEvents: jest.fn().mockResolvedValue([
+ {
+ submitted_at: '2023-06-15T12:00:00Z',
+ state: 'approved',
+ user: { login: 'reviewer1' }
+ }
+ ]),
+ calculatePickupTime: jest.fn().mockReturnValue({
+ metricType: 'time_to_first_review',
+ reviewDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ pickupTimeSeconds: 7200,
+ repository: 'owner/repo',
+ prNumber: 123,
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ firstReviewTime: new Date('2023-06-15T12:00:00Z')
+ }),
+ calculateTimeToMerge: jest.fn().mockReturnValue({
+ metricType: 'time_to_merge',
+ mergeDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ mergeTimeSeconds: 16200,
+ repository: 'owner/repo',
+ prNumber: 123,
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ mergeTime: new Date('2023-06-15T14:30:00Z')
+ }),
+ filterBotReviews: jest.fn((reviews, excludeBots) => excludeBots ? [] : reviews)
+ };
+
+ // Collect metrics for a single repository
+ const metrics = await metricsCollector.collectRepositoryMetrics('owner/repo');
+
+ // Verify that both metrics were collected
+ expect(metrics).toHaveLength(2);
+
+ const firstReviewMetric = metrics.find(m => m.metricType === 'time_to_first_review');
+ const mergeMetric = metrics.find(m => m.metricType === 'time_to_merge');
+
+ expect(firstReviewMetric).toBeDefined();
+ expect(firstReviewMetric.pickupTimeSeconds).toBe(7200);
+ expect(firstReviewMetric.prNumber).toBe(123);
+
+ expect(mergeMetric).toBeDefined();
+ expect(mergeMetric.mergeTimeSeconds).toBe(16200);
+ expect(mergeMetric.prNumber).toBe(123);
+
+ // Verify that both calculation methods were called
+ expect(metricsCollector.githubClient.calculatePickupTime).toHaveBeenCalled();
+ expect(metricsCollector.githubClient.calculateTimeToMerge).toHaveBeenCalled();
+ });
+
+ test('should handle configuration with only Time to Merge enabled', async () => {
+ // Override environment to enable only Time to Merge
+ process.env.ENABLED_METRICS = 'time_to_merge';
+
+ const config = loadConfig();
+
+ expect(config.metrics.timeToFirstReview.enabled).toBe(false);
+ expect(config.metrics.timeToMerge.enabled).toBe(true);
+
+ const metricsCollector = new MetricsCollector(config);
+
+ // Mock the GitHub client
+ metricsCollector.githubClient = {
+ calculateTimeToMerge: jest.fn().mockReturnValue({
+ metricType: 'time_to_merge',
+ mergeDate: '2023-06-15',
+ prCreator: 'testuser',
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ mergeTimeSeconds: 16200,
+ repository: 'owner/repo',
+ prNumber: 123,
+ targetBranch: 'main',
+ readyTime: new Date('2023-06-15T10:00:00Z'),
+ mergeTime: new Date('2023-06-15T14:30:00Z')
+ }),
+ calculatePickupTime: jest.fn().mockReturnValue(null) // Should not be called
+ };
+
+ // Mock PR data
+ const mockPR = {
+ number: 123,
+ html_url: 'https://github.com/owner/repo/pull/123',
+ user: { login: 'testuser' },
+ base: {
+ ref: 'main',
+ repo: {
+ owner: { login: 'owner' },
+ name: 'repo'
+ }
+ },
+ head: { repo: { full_name: 'owner/repo' } },
+ state: 'closed',
+ merged_at: '2023-06-15T14:30:00Z'
+ };
+
+ const mockTimelineEvents = [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-06-15T10:00:00Z'
+ }
+ ];
+
+ const mockReviewEvents = [];
+
+ // Test collectPRMetrics with only merge enabled
+ const metrics = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ // Should only collect Time to Merge metric
+ expect(metrics).toHaveLength(1);
+ expect(metrics[0].metricType).toBe('time_to_merge');
+ });
+
+ test('should group and route metrics to correct tables', () => {
+ const config = loadConfig();
+ const metricsCollector = new MetricsCollector(config);
+
+ const mixedMetrics = [
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ pickupTimeSeconds: 7200
+ },
+ {
+ metricType: 'time_to_merge',
+ prNumber: 123,
+ mergeTimeSeconds: 16200
+ },
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 124,
+ pickupTimeSeconds: 3600
+ }
+ ];
+
+ // Test grouping
+ const grouped = metricsCollector.groupMetricsByType(mixedMetrics);
+
+ expect(grouped.time_to_first_review).toHaveLength(2);
+ expect(grouped.time_to_merge).toHaveLength(1);
+
+ // Test table name mapping
+ expect(metricsCollector.getTableNameForMetricType('time_to_first_review')).toBe('pr_first_review');
+ expect(metricsCollector.getTableNameForMetricType('time_to_merge')).toBe('pr_merge');
+ });
+});
diff --git a/.github/actions/eng-metrics/test/github-client-first-review.test.js b/.github/actions/eng-metrics/test/github-client-first-review.test.js
new file mode 100644
index 0000000000..19e3c8a4bd
--- /dev/null
+++ b/.github/actions/eng-metrics/test/github-client-first-review.test.js
@@ -0,0 +1,547 @@
+import { jest } from '@jest/globals';
+import GitHubClient from '../src/github-client.js';
+
+// Mock the logger to avoid console output during tests
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ default: {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ }
+}));
+
+describe('GitHubClient - Time to first review (pickup time)', () => {
+ let githubClient;
+
+ beforeEach(() => {
+ // Create a new instance of GitHubClient for each test
+ githubClient = new GitHubClient('fake-token');
+
+ // Mock the Octokit instance
+ githubClient.octokit = {
+ rest: {
+ pulls: {
+ list: jest.fn(),
+ listReviews: jest.fn(),
+ },
+ issues: {
+ listEventsForTimeline: jest.fn(),
+ }
+ }
+ };
+ });
+
+ describe('calculatePickupTime', () => {
+ // Table-driven test cases for calculatePickupTime
+ const testCases = [
+ {
+ name: 'PR created as non-draft with one review',
+ pr: {
+ number: 123,
+ html_url: 'https://github.com/owner/repo/pull/123',
+ draft: false,
+ created_at: '2023-05-10T10:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-10T11:30:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 123,
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-10T10:00:00Z'),
+ firstReviewTime: new Date('2023-05-10T11:30:00Z'),
+ reviewDate: '2023-05-10',
+ pickupTimeSeconds: 5400, // 1.5 hours = 5400 seconds
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR created as draft, then marked as ready for review, then reviewed',
+ pr: {
+ number: 124,
+ html_url: 'https://github.com/owner/repo/pull/124',
+ draft: true,
+ created_at: '2023-05-11T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-11T10:00:00Z'
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-11T11:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 124,
+ prUrl: 'https://github.com/owner/repo/pull/124',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-11T10:00:00Z'),
+ firstReviewTime: new Date('2023-05-11T11:00:00Z'),
+ reviewDate: '2023-05-11',
+ pickupTimeSeconds: 3600, // 1 hour = 3600 seconds
+ readyEventType: 'ready_for_review event'
+ }
+ },
+ {
+ name: 'PR with multiple ready_for_review events - should use the latest one',
+ pr: {
+ number: 125,
+ html_url: 'https://github.com/owner/repo/pull/125',
+ draft: true,
+ created_at: '2023-05-12T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-12T10:00:00Z'
+ },
+ {
+ event: 'convert_to_draft',
+ created_at: '2023-05-12T11:00:00Z'
+ },
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-12T12:00:00Z'
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-12T13:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 125,
+ prUrl: 'https://github.com/owner/repo/pull/125',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-12T12:00:00Z'),
+ firstReviewTime: new Date('2023-05-12T13:00:00Z'),
+ reviewDate: '2023-05-12',
+ pickupTimeSeconds: 3600, // 1 hour = 3600 seconds
+ readyEventType: 'ready_for_review event'
+ }
+ },
+ {
+ name: 'PR with ready_for_review event after the first review',
+ pr: {
+ number: 126,
+ html_url: 'https://github.com/owner/repo/pull/126',
+ draft: false,
+ created_at: '2023-05-16T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'convert_to_draft',
+ created_at: '2023-05-16T10:00:00Z'
+ },
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-16T12:00:00Z'
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-16T11:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 126,
+ prUrl: 'https://github.com/owner/repo/pull/126',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-16T09:00:00Z'),
+ firstReviewTime: new Date('2023-05-16T11:00:00Z'),
+ reviewDate: '2023-05-16',
+ pickupTimeSeconds: 7200, // 2 hours = 7200 seconds
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR with no ready_for_review events and created as draft',
+ pr: {
+ number: 127,
+ html_url: 'https://github.com/owner/repo/pull/127',
+ draft: true,
+ created_at: '2023-05-14T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-14T11:00:00Z' }
+ ],
+ expected: null // Should return null because no ready event was found
+ },
+ {
+ name: 'PR with no reviews',
+ pr: {
+ number: 128,
+ html_url: 'https://github.com/owner/repo/pull/128',
+ draft: false,
+ created_at: '2023-05-15T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: null // Should return null because no reviews were found
+ },
+ {
+ name: 'PR with multiple reviews - only first one should be counted',
+ pr: {
+ number: 129,
+ html_url: 'https://github.com/owner/repo/pull/129',
+ draft: false,
+ created_at: '2023-05-16T09:00:00Z',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-16T10:00:00Z' }, // First review - should be used
+ { submitted_at: '2023-05-16T11:00:00Z' }, // Second review - should be ignored
+ { submitted_at: '2023-05-16T12:00:00Z' } // Third review - should be ignored
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 129,
+ prUrl: 'https://github.com/owner/repo/pull/129',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-16T09:00:00Z'),
+ firstReviewTime: new Date('2023-05-16T10:00:00Z'),
+ reviewDate: '2023-05-16',
+ pickupTimeSeconds: 3600, // 1 hour = 3600 seconds
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Saturday, reviewed on Sunday 3 weeks later (should exclude weekend days)',
+ pr: {
+ number: 136,
+ html_url: 'https://github.com/owner/repo/pull/136',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-06-11T14:00:00Z' } // Sunday, 3 weeks later
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 136,
+ prUrl: 'https://github.com/owner/repo/pull/136',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ firstReviewTime: new Date('2023-06-11T14:00:00Z'),
+ reviewDate: '2023-06-11',
+ pickupTimeSeconds: 1296000, // 15 days = 1296000 seconds (3 weeks of 5 working days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Sunday, reviewed on Monday (should use end of Sunday as ready time)',
+ pr: {
+ number: 134,
+ html_url: 'https://github.com/owner/repo/pull/134',
+ draft: false,
+ created_at: '2023-05-21T14:00:00Z', // Sunday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-22T10:00:00Z' } // Monday
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 134,
+ prUrl: 'https://github.com/owner/repo/pull/134',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-21T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-22T10:00:00Z'),
+ reviewDate: '2023-05-22',
+ pickupTimeSeconds: 36000, // 10 hours = 36000 seconds (from end of Sunday to Monday 10am)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Sunday, reviewed on next Saturday (should exclude weekend days)',
+ pr: {
+ number: 135,
+ html_url: 'https://github.com/owner/repo/pull/135',
+ draft: false,
+ created_at: '2023-05-21T14:00:00Z', // Sunday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-27T14:00:00Z' } // Saturday
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 135,
+ prUrl: 'https://github.com/owner/repo/pull/135',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-21T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-27T14:00:00Z'),
+ reviewDate: '2023-05-27',
+ pickupTimeSeconds: 432000, // 5 days = 432000 seconds (6 days - 1 weekend day)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Friday, reviewed on Monday (should subtract weekend days)',
+ pr: {
+ number: 130,
+ html_url: 'https://github.com/owner/repo/pull/130',
+ draft: false,
+ created_at: '2023-05-19T14:00:00Z', // Friday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-22T10:00:00Z' } // Monday
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 130,
+ prUrl: 'https://github.com/owner/repo/pull/130',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-19T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-22T10:00:00Z'),
+ reviewDate: '2023-05-22',
+ pickupTimeSeconds: 72000, // 20 hours = 72000 seconds (3 days - 2 weekend days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Saturday, reviewed on Monday (should use end of Sunday as ready time)',
+ pr: {
+ number: 131,
+ html_url: 'https://github.com/owner/repo/pull/131',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-22T10:00:00Z' } // Monday
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 131,
+ prUrl: 'https://github.com/owner/repo/pull/131',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-22T10:00:00Z'),
+ reviewDate: '2023-05-22',
+ pickupTimeSeconds: 36000, // 10 hours = 36000 seconds (from end of Sunday to Monday 10am)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Saturday, reviewed on Sunday (should have 0 pickup time)',
+ pr: {
+ number: 132,
+ html_url: 'https://github.com/owner/repo/pull/132',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-21T14:00:00Z' } // Sunday
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 132,
+ prUrl: 'https://github.com/owner/repo/pull/132',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-21T14:00:00Z'),
+ reviewDate: '2023-05-21',
+ pickupTimeSeconds: 0, // 0 seconds (both on weekend)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on weekday, reviewed after multiple weekends (should subtract weekend days)',
+ pr: {
+ number: 133,
+ html_url: 'https://github.com/owner/repo/pull/133',
+ draft: false,
+ created_at: '2023-05-17T14:00:00Z', // Wednesday
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-29T14:00:00Z' } // Monday, 12 days later
+ ],
+ expected: {
+ metricType: 'time_to_first_review',
+ repository: 'owner/repo',
+ prNumber: 133,
+ prUrl: 'https://github.com/owner/repo/pull/133',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-17T14:00:00Z'),
+ firstReviewTime: new Date('2023-05-29T14:00:00Z'),
+ reviewDate: '2023-05-29',
+ pickupTimeSeconds: 691200, // 8 days = 691200 seconds (12 days - 4 weekend days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ }
+ ];
+
+ // Run each test case
+ test.each(testCases)('$name', ({ pr, timelineEvents, reviewEvents, expected }) => {
+ const result = githubClient.calculatePickupTime(pr, timelineEvents, reviewEvents);
+
+ if (expected === null) {
+ expect(result).toBeNull();
+ } else {
+ // Compare date objects separately
+ expect(result.readyTime).toEqual(expected.readyTime);
+ expect(result.firstReviewTime).toEqual(expected.firstReviewTime);
+
+ // Compare the rest of the properties
+ expect({
+ ...result,
+ readyTime: undefined,
+ firstReviewTime: undefined
+ }).toEqual({
+ ...expected,
+ readyTime: undefined,
+ firstReviewTime: undefined
+ });
+ }
+ });
+ });
+});
diff --git a/.github/actions/eng-metrics/test/github-client-general.test.js b/.github/actions/eng-metrics/test/github-client-general.test.js
new file mode 100644
index 0000000000..df1353b4e4
--- /dev/null
+++ b/.github/actions/eng-metrics/test/github-client-general.test.js
@@ -0,0 +1,136 @@
+/**
+ * General tests for GitHub client functionality
+ */
+
+import { jest } from '@jest/globals';
+import GitHubClient from '../src/github-client.js';
+
+// Mock the logger
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+}));
+
+describe('GitHubClient - General Functionality', () => {
+ let githubClient;
+ let mockOctokit;
+
+ beforeEach(() => {
+ // Mock Octokit
+ mockOctokit = {
+ rest: {
+ pulls: {
+ list: jest.fn(),
+ listReviews: jest.fn()
+ },
+ issues: {
+ listEventsForTimeline: jest.fn()
+ }
+ }
+ };
+
+ githubClient = new GitHubClient('fake-token');
+ githubClient.octokit = mockOctokit;
+ });
+
+ describe('constructor', () => {
+ test('should initialize with token', () => {
+ const client = new GitHubClient('test-token');
+ expect(client).toBeInstanceOf(GitHubClient);
+ });
+
+ test('should throw error without token', () => {
+ expect(() => new GitHubClient()).toThrow('GitHub token is required');
+ });
+ });
+
+ describe('fetchPullRequests', () => {
+ test('should fetch pull requests successfully', async () => {
+ const mockResponse = {
+ data: [
+ {
+ number: 1,
+ title: 'Test PR',
+ state: 'closed',
+ merged_at: '2023-06-15T14:30:00Z',
+ updated_at: '2023-06-15T14:30:00Z',
+ base: { ref: 'main' }
+ }
+ ]
+ };
+
+ mockOctokit.rest.pulls.list.mockResolvedValue(mockResponse);
+
+ const result = await githubClient.fetchPullRequests('owner', 'repo', 'all', new Date('2023-01-01'));
+ expect(result).toHaveLength(1);
+ expect(result[0].number).toBe(1);
+ });
+
+ test('should handle API errors gracefully', async () => {
+ const mockError = new Error('API Error');
+ mockOctokit.rest.pulls.list.mockRejectedValue(mockError);
+
+ await expect(githubClient.fetchPullRequests('owner', 'repo', 'all', new Date('2023-01-01')))
+ .rejects.toThrow('API Error');
+ });
+ });
+
+ describe('fetchPRTimelineEvents', () => {
+ test('should fetch timeline events successfully', async () => {
+ const mockResponse = {
+ data: [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-06-15T10:00:00Z'
+ }
+ ]
+ };
+
+ mockOctokit.rest.issues.listEventsForTimeline.mockResolvedValue(mockResponse);
+
+ const result = await githubClient.fetchPRTimelineEvents('owner', 'repo', 123);
+ expect(result).toHaveLength(1);
+ expect(result[0].event).toBe('ready_for_review');
+ });
+
+ test('should handle API errors gracefully', async () => {
+ const mockError = new Error('Timeline API Error');
+ mockOctokit.rest.issues.listEventsForTimeline.mockRejectedValue(mockError);
+
+ await expect(githubClient.fetchPRTimelineEvents('owner', 'repo', 123))
+ .rejects.toThrow('Timeline API Error');
+ });
+ });
+
+ describe('fetchPRReviewEvents', () => {
+ test('should fetch reviews successfully', async () => {
+ const mockResponse = {
+ data: [
+ {
+ id: 1,
+ state: 'APPROVED',
+ submitted_at: '2023-06-15T12:00:00Z',
+ user: { login: 'reviewer1' }
+ }
+ ]
+ };
+
+ mockOctokit.rest.pulls.listReviews.mockResolvedValue(mockResponse);
+
+ const result = await githubClient.fetchPRReviewEvents('owner', 'repo', 123);
+ expect(result).toHaveLength(1);
+ expect(result[0].state).toBe('APPROVED');
+ });
+
+ test('should handle API errors gracefully', async () => {
+ const mockError = new Error('Reviews API Error');
+ mockOctokit.rest.pulls.listReviews.mockRejectedValue(mockError);
+
+ await expect(githubClient.fetchPRReviewEvents('owner', 'repo', 123))
+ .rejects.toThrow('Reviews API Error');
+ });
+ });
+
+});
diff --git a/.github/actions/eng-metrics/test/github-client-merge.test.js b/.github/actions/eng-metrics/test/github-client-merge.test.js
new file mode 100644
index 0000000000..36f11686b2
--- /dev/null
+++ b/.github/actions/eng-metrics/test/github-client-merge.test.js
@@ -0,0 +1,547 @@
+import { jest } from '@jest/globals';
+import GitHubClient from '../src/github-client.js';
+
+// Mock the logger to avoid console output during tests
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ default: {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ }
+}));
+
+describe('GitHubClient - Time to Merge', () => {
+ let githubClient;
+
+ beforeEach(() => {
+ // Create a new instance of GitHubClient for each test
+ githubClient = new GitHubClient('fake-token');
+
+ // Mock the Octokit instance
+ githubClient.octokit = {
+ rest: {
+ pulls: {
+ list: jest.fn(),
+ listReviews: jest.fn(),
+ },
+ issues: {
+ listEventsForTimeline: jest.fn(),
+ }
+ }
+ };
+ });
+
+ describe('calculateTimeToMerge', () => {
+ // Table-driven test cases for calculateTimeToMerge
+ const testCases = [
+ {
+ name: 'PR created as non-draft and merged',
+ pr: {
+ number: 123,
+ html_url: 'https://github.com/owner/repo/pull/123',
+ draft: false,
+ created_at: '2023-05-10T10:00:00Z',
+ merged_at: '2023-05-10T11:30:00Z',
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-10T11:30:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 123,
+ prUrl: 'https://github.com/owner/repo/pull/123',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-10T10:00:00Z'),
+ mergeTime: new Date('2023-05-10T11:30:00Z'),
+ mergeDate: '2023-05-10',
+ mergeTimeSeconds: 5400, // 1.5 hours = 5400 seconds
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR created as draft, then marked as ready for review, then merged',
+ pr: {
+ number: 124,
+ html_url: 'https://github.com/owner/repo/pull/124',
+ draft: true,
+ created_at: '2023-05-11T09:00:00Z',
+ merged_at: '2023-05-11T11:00:00Z',
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-11T10:00:00Z'
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-11T11:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 124,
+ prUrl: 'https://github.com/owner/repo/pull/124',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-11T10:00:00Z'),
+ mergeTime: new Date('2023-05-11T11:00:00Z'),
+ mergeDate: '2023-05-11',
+ mergeTimeSeconds: 3600, // 1 hour = 3600 seconds
+ readyEventType: 'ready_for_review event'
+ }
+ },
+ {
+ name: 'PR with multiple ready_for_review events - should use the latest one',
+ pr: {
+ number: 125,
+ html_url: 'https://github.com/owner/repo/pull/125',
+ draft: true,
+ created_at: '2023-05-12T09:00:00Z',
+ merged_at: '2023-05-12T13:00:00Z',
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-12T10:00:00Z'
+ },
+ {
+ event: 'convert_to_draft',
+ created_at: '2023-05-12T11:00:00Z'
+ },
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-12T12:00:00Z'
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-12T13:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 125,
+ prUrl: 'https://github.com/owner/repo/pull/125',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-12T12:00:00Z'),
+ mergeTime: new Date('2023-05-12T13:00:00Z'),
+ mergeDate: '2023-05-12',
+ mergeTimeSeconds: 3600, // 1 hour = 3600 seconds
+ readyEventType: 'ready_for_review event'
+ }
+ },
+ {
+ name: 'PR with ready_for_review event after merge time - should use PR creation',
+ pr: {
+ number: 126,
+ html_url: 'https://github.com/owner/repo/pull/126',
+ draft: false,
+ created_at: '2023-05-16T09:00:00Z',
+ merged_at: '2023-05-16T11:00:00Z',
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [
+ {
+ event: 'convert_to_draft',
+ created_at: '2023-05-16T10:00:00Z'
+ },
+ {
+ event: 'ready_for_review',
+ created_at: '2023-05-16T12:00:00Z' // After merge
+ }
+ ],
+ reviewEvents: [
+ { submitted_at: '2023-05-16T11:00:00Z' }
+ ],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 126,
+ prUrl: 'https://github.com/owner/repo/pull/126',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-16T09:00:00Z'),
+ mergeTime: new Date('2023-05-16T11:00:00Z'),
+ mergeDate: '2023-05-16',
+ mergeTimeSeconds: 7200, // 2 hours = 7200 seconds
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR with no ready_for_review events and created as draft',
+ pr: {
+ number: 127,
+ html_url: 'https://github.com/owner/repo/pull/127',
+ draft: true,
+ created_at: '2023-05-14T09:00:00Z',
+ merged_at: '2023-05-14T11:00:00Z',
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-05-14T11:00:00Z' }
+ ],
+ expected: null // Should return null because no ready event was found
+ },
+ {
+ name: 'PR that was never merged',
+ pr: {
+ number: 128,
+ html_url: 'https://github.com/owner/repo/pull/128',
+ draft: false,
+ created_at: '2023-05-15T09:00:00Z',
+ merged_at: null,
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: null // Should return null because PR was not merged
+ },
+ {
+ name: 'PR that is still open',
+ pr: {
+ number: 129,
+ html_url: 'https://github.com/owner/repo/pull/129',
+ draft: false,
+ created_at: '2023-05-16T09:00:00Z',
+ merged_at: null,
+ state: 'open',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: null // Should return null because PR is not merged
+ },
+ {
+ name: 'PR ready on Saturday, merged on Sunday 3 weeks later (should exclude weekend days)',
+ pr: {
+ number: 136,
+ html_url: 'https://github.com/owner/repo/pull/136',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ merged_at: '2023-06-11T14:00:00Z', // Sunday, 3 weeks later
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [
+ { submitted_at: '2023-06-11T13:00:00Z' } // Sunday, 3 weeks later
+ ],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 136,
+ prUrl: 'https://github.com/owner/repo/pull/136',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ mergeTime: new Date('2023-06-11T14:00:00Z'),
+ mergeDate: '2023-06-11',
+ mergeTimeSeconds: 1296000, // 15 days = 1296000 seconds (3 weeks of 5 working days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Sunday, merged on Monday (should use end of Sunday as ready time)',
+ pr: {
+ number: 134,
+ html_url: 'https://github.com/owner/repo/pull/134',
+ draft: false,
+ created_at: '2023-05-21T14:00:00Z', // Sunday
+ merged_at: '2023-05-22T10:00:00Z', // Monday
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 134,
+ prUrl: 'https://github.com/owner/repo/pull/134',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-21T14:00:00Z'),
+ mergeTime: new Date('2023-05-22T10:00:00Z'),
+ mergeDate: '2023-05-22',
+ mergeTimeSeconds: 36000, // 10 hours = 36000 seconds (from end of Sunday to Monday 10am)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Sunday, merged on next Saturday (should exclude weekend days)',
+ pr: {
+ number: 135,
+ html_url: 'https://github.com/owner/repo/pull/135',
+ draft: false,
+ created_at: '2023-05-21T14:00:00Z', // Sunday
+ merged_at: '2023-05-27T14:00:00Z', // Saturday
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 135,
+ prUrl: 'https://github.com/owner/repo/pull/135',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-21T14:00:00Z'),
+ mergeTime: new Date('2023-05-27T14:00:00Z'),
+ mergeDate: '2023-05-27',
+ mergeTimeSeconds: 432000, // 5 days = 432000 seconds (6 days - 1 weekend day)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Friday, merged on Monday (should subtract weekend days)',
+ pr: {
+ number: 130,
+ html_url: 'https://github.com/owner/repo/pull/130',
+ draft: false,
+ created_at: '2023-05-19T14:00:00Z', // Friday
+ merged_at: '2023-05-22T10:00:00Z', // Monday
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 130,
+ prUrl: 'https://github.com/owner/repo/pull/130',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-19T14:00:00Z'),
+ mergeTime: new Date('2023-05-22T10:00:00Z'),
+ mergeDate: '2023-05-22',
+ mergeTimeSeconds: 72000, // 20 hours = 72000 seconds (3 days - 2 weekend days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Saturday, merged on Monday (should use end of Sunday as ready time)',
+ pr: {
+ number: 131,
+ html_url: 'https://github.com/owner/repo/pull/131',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ merged_at: '2023-05-22T10:00:00Z', // Monday
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 131,
+ prUrl: 'https://github.com/owner/repo/pull/131',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ mergeTime: new Date('2023-05-22T10:00:00Z'),
+ mergeDate: '2023-05-22',
+ mergeTimeSeconds: 36000, // 10 hours = 36000 seconds (from end of Sunday to Monday 10am)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on Saturday, merged on Sunday (should have 0 merge time)',
+ pr: {
+ number: 132,
+ html_url: 'https://github.com/owner/repo/pull/132',
+ draft: false,
+ created_at: '2023-05-20T14:00:00Z', // Saturday
+ merged_at: '2023-05-21T14:00:00Z', // Sunday
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 132,
+ prUrl: 'https://github.com/owner/repo/pull/132',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-20T14:00:00Z'),
+ mergeTime: new Date('2023-05-21T14:00:00Z'),
+ mergeDate: '2023-05-21',
+ mergeTimeSeconds: 0, // 0 seconds (both on weekend)
+ readyEventType: 'PR creation (not draft)'
+ }
+ },
+ {
+ name: 'PR ready on weekday, merged after multiple weekends (should subtract weekend days)',
+ pr: {
+ number: 133,
+ html_url: 'https://github.com/owner/repo/pull/133',
+ draft: false,
+ created_at: '2023-05-17T14:00:00Z', // Wednesday
+ merged_at: '2023-05-29T14:00:00Z', // Monday, 12 days later
+ state: 'closed',
+ user: { login: 'author' },
+ base: {
+ ref: 'main',
+ repo: {
+ name: 'repo',
+ owner: { login: 'owner' }
+ }
+ }
+ },
+ timelineEvents: [],
+ reviewEvents: [],
+ expected: {
+ metricType: 'time_to_merge',
+ repository: 'owner/repo',
+ prNumber: 133,
+ prUrl: 'https://github.com/owner/repo/pull/133',
+ prCreator: 'author',
+ targetBranch: 'main',
+ readyTime: new Date('2023-05-17T14:00:00Z'),
+ mergeTime: new Date('2023-05-29T14:00:00Z'),
+ mergeDate: '2023-05-29',
+ mergeTimeSeconds: 691200, // 8 days = 691200 seconds (12 days - 4 weekend days)
+ readyEventType: 'PR creation (not draft)'
+ }
+ }
+ ];
+
+ // Run each test case
+ test.each(testCases)('$name', ({ pr, timelineEvents, reviewEvents, expected }) => {
+ const result = githubClient.calculateTimeToMerge(pr, timelineEvents, reviewEvents);
+
+ if (expected === null) {
+ expect(result).toBeNull();
+ } else {
+ // Compare date objects separately
+ expect(result.readyTime).toEqual(expected.readyTime);
+ expect(result.mergeTime).toEqual(expected.mergeTime);
+
+ // Compare the rest of the properties
+ expect({
+ ...result,
+ readyTime: undefined,
+ mergeTime: undefined
+ }).toEqual({
+ ...expected,
+ readyTime: undefined,
+ mergeTime: undefined
+ });
+ }
+ });
+ });
+});
diff --git a/.github/actions/eng-metrics/test/markdown-parser.test.js b/.github/actions/eng-metrics/test/markdown-parser.test.js
new file mode 100644
index 0000000000..27cf51d679
--- /dev/null
+++ b/.github/actions/eng-metrics/test/markdown-parser.test.js
@@ -0,0 +1,354 @@
+/**
+ * Tests for markdown parser module
+ */
+
+import { jest } from '@jest/globals';
+
+// Mock the logger
+const mockLogger = {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+};
+
+// Mock fs
+const mockFs = {
+ existsSync: jest.fn(),
+ readFileSync: jest.fn()
+};
+
+// Mock path
+const mockPath = {
+ resolve: jest.fn()
+};
+
+// Set up module mocks
+jest.unstable_mockModule('../src/logger.js', () => ({
+ default: mockLogger,
+ ...mockLogger
+}));
+
+jest.unstable_mockModule('fs', () => ({
+ default: mockFs,
+ ...mockFs
+}));
+
+jest.unstable_mockModule('path', () => ({
+ default: mockPath,
+ ...mockPath
+}));
+
+// Import the module after mocking
+const { parseProductGroups, validateMarkdownStructure } = await import('../src/markdown-parser.js');
+
+describe('MarkdownParser', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockPath.resolve.mockImplementation((cwd, filePath) => `/resolved/${filePath}`);
+ });
+
+ describe('parseProductGroups', () => {
+ it('should parse valid markdown with all groups correctly', () => {
+ const mockMarkdown = `
+# Product Groups
+
+## Some other content
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Product Manager | _([@testpm1](https://github.com/testpm1))_ |
+| Developer | _([@testdev1](https://github.com/testdev1))_, _([@testdev2](https://github.com/testdev2))_, _([@testdev3](https://github.com/testdev3))_ |
+| Quality Assurance | _([@testqa1](https://github.com/testqa1))_ |
+
+### Orchestration group
+
+| Role | Contributor |
+|------|-------------|
+| Product Manager | _([@testpm2](https://github.com/testpm2))_ |
+| Developer | _([@orchdev1](https://github.com/orchdev1))_, _([@orchdev2](https://github.com/orchdev2))_, _([@orchdev3](https://github.com/orchdev3))_ |
+
+### Software group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@softdev1](https://github.com/softdev1))_, _([@softdev2](https://github.com/softdev2))_ |
+| Quality Assurance | _([@testqa2](https://github.com/testqa2))_ |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(mockFs.existsSync).toHaveBeenCalledWith('/resolved/test-file.md');
+ expect(mockFs.readFileSync).toHaveBeenCalledWith('/resolved/test-file.md', 'utf8');
+
+ // Should extract usernames and create dual group membership
+ expect(result).toEqual([
+ // MDM group users
+ { group: 'mdm', username: 'testdev1' },
+ { group: 'engineering', username: 'testdev1' },
+ { group: 'mdm', username: 'testdev2' },
+ { group: 'engineering', username: 'testdev2' },
+ { group: 'mdm', username: 'testdev3' },
+ { group: 'engineering', username: 'testdev3' },
+ // Orchestration group users
+ { group: 'orchestration', username: 'orchdev1' },
+ { group: 'engineering', username: 'orchdev1' },
+ { group: 'orchestration', username: 'orchdev2' },
+ { group: 'engineering', username: 'orchdev2' },
+ { group: 'orchestration', username: 'orchdev3' },
+ { group: 'engineering', username: 'orchdev3' },
+ // Software group users
+ { group: 'software', username: 'softdev1' },
+ { group: 'engineering', username: 'softdev1' },
+ { group: 'software', username: 'softdev2' },
+ { group: 'engineering', username: 'softdev2' }
+ ]);
+
+ expect(mockLogger.info).toHaveBeenCalledWith('Parsing product groups from /resolved/test-file.md');
+ expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developers in mdm group: testdev1, testdev2, testdev3');
+ expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developers in orchestration group: orchdev1, orchdev2, orchdev3');
+ expect(mockLogger.info).toHaveBeenCalledWith('Found 2 developers in software group: softdev1, softdev2');
+ expect(mockLogger.info).toHaveBeenCalledWith('Extracted 16 user-group mappings from markdown');
+ });
+
+ it('should handle missing sections gracefully', () => {
+ const mockMarkdown = `
+# Product Groups
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@testdev1](https://github.com/testdev1))_ |
+
+### Software group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@softdev1](https://github.com/softdev1))_ |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(result).toEqual([
+ { group: 'mdm', username: 'testdev1' },
+ { group: 'engineering', username: 'testdev1' },
+ { group: 'software', username: 'softdev1' },
+ { group: 'engineering', username: 'softdev1' }
+ ]);
+
+ expect(mockLogger.warn).toHaveBeenCalledWith('Section not found: Orchestration group');
+ });
+
+ it('should handle missing Developer rows', () => {
+ const mockMarkdown = `
+# Product Groups
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Product Manager | _([@testpm1](https://github.com/testpm1))_ |
+| Quality Assurance | _([@testqa1](https://github.com/testqa1))_ |
+
+### Orchestration group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(result).toEqual([
+ { group: 'orchestration', username: 'orchdev1' },
+ { group: 'engineering', username: 'orchdev1' }
+ ]);
+
+ expect(mockLogger.warn).toHaveBeenCalledWith('No Developer row found in mdm group section');
+ });
+
+ it('should handle malformed GitHub username patterns', () => {
+ const mockMarkdown = `
+# Product Groups
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | Some text without proper format, _([@testvaliduser](https://github.com/testvaliduser))_, invalid format here |
+
+### Orchestration group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | No valid usernames here at all |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(result).toEqual([
+ { group: 'mdm', username: 'testvaliduser' },
+ { group: 'engineering', username: 'testvaliduser' }
+ ]);
+
+ expect(mockLogger.warn).toHaveBeenCalledWith('No GitHub usernames found in orchestration group Developer row');
+ });
+
+ it('should handle multi-line developer cells', () => {
+ const mockMarkdown = `
+# Product Groups
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@testuser1](https://github.com/testuser1))_,
+_([@testuser2](https://github.com/testuser2))_,
+_([@testuser3](https://github.com/testuser3))_ |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(result).toEqual([
+ { group: 'mdm', username: 'testuser1' },
+ { group: 'engineering', username: 'testuser1' },
+ { group: 'mdm', username: 'testuser2' },
+ { group: 'engineering', username: 'testuser2' },
+ { group: 'mdm', username: 'testuser3' },
+ { group: 'engineering', username: 'testuser3' }
+ ]);
+ });
+
+ it('should handle file not found', () => {
+ mockFs.existsSync.mockReturnValue(false);
+
+ const result = parseProductGroups('nonexistent-file.md');
+
+ expect(result).toEqual([]);
+ expect(mockLogger.error).toHaveBeenCalledWith('Product groups file not found at /resolved/nonexistent-file.md');
+ });
+
+ it('should handle file read errors', () => {
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockImplementation(() => {
+ throw new Error('Permission denied');
+ });
+
+ const result = parseProductGroups('error-file.md');
+
+ expect(result).toEqual([]);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'Error parsing product groups file: error-file.md',
+ {},
+ expect.any(Error)
+ );
+ });
+
+ it('should handle usernames with hyphens and numbers', () => {
+ const mockMarkdown = `
+# Product Groups
+
+### MDM group
+
+| Role | Contributor |
+|------|-------------|
+| Developer | _([@testuser-123](https://github.com/testuser-123))_, _([@fakeuser-456](https://github.com/fakeuser-456))_ |
+`;
+
+ mockFs.existsSync.mockReturnValue(true);
+ mockFs.readFileSync.mockReturnValue(mockMarkdown);
+
+ const result = parseProductGroups('test-file.md');
+
+ expect(result).toEqual([
+ { group: 'mdm', username: 'testuser-123' },
+ { group: 'engineering', username: 'testuser-123' },
+ { group: 'mdm', username: 'fakeuser-456' },
+ { group: 'engineering', username: 'fakeuser-456' }
+ ]);
+ });
+ });
+
+ describe('validateMarkdownStructure', () => {
+ it('should return true for valid markdown with all required sections', () => {
+ const validMarkdown = `
+# Product Groups
+
+### MDM group
+Some content
+
+### Orchestration group
+Some content
+
+### Software group
+Some content
+`;
+
+ const result = validateMarkdownStructure(validMarkdown);
+ expect(result).toBe(true);
+ });
+
+ it('should return false and warn for missing sections', () => {
+ const invalidMarkdown = `
+# Product Groups
+
+### MDM group
+Some content
+
+### Software group
+Some content
+`;
+
+ const result = validateMarkdownStructure(invalidMarkdown);
+ expect(result).toBe(false);
+ expect(mockLogger.warn).toHaveBeenCalledWith('Missing required section: Orchestration group');
+ });
+
+ it('should return false for completely empty content', () => {
+ const result = validateMarkdownStructure('');
+ expect(result).toBe(false);
+ expect(mockLogger.warn).toHaveBeenCalledWith('Missing required section: MDM group');
+ // The function returns false on first missing section, so other warnings may not be called
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle case-sensitive section matching', () => {
+ const invalidMarkdown = `
+# Product Groups
+
+### mdm group
+Some content
+
+### orchestration group
+Some content
+
+### software group
+Some content
+`;
+
+ const result = validateMarkdownStructure(invalidMarkdown);
+ expect(result).toBe(false);
+ expect(mockLogger.warn).toHaveBeenCalledWith('Missing required section: MDM group');
+ // The function returns false on first missing section, so other warnings may not be called
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1);
+ });
+ });
+});
\ No newline at end of file
diff --git a/.github/actions/eng-metrics/test/metrics-collector.test.js b/.github/actions/eng-metrics/test/metrics-collector.test.js
new file mode 100644
index 0000000000..19b076aad8
--- /dev/null
+++ b/.github/actions/eng-metrics/test/metrics-collector.test.js
@@ -0,0 +1,321 @@
+/**
+ * Tests for metrics collector module
+ */
+
+import { jest } from '@jest/globals';
+import { MetricsCollector } from '../src/metrics-collector.js';
+
+// Mock the logger
+jest.mock('../src/logger.js', () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+}));
+
+// Mock GitHubClient
+jest.mock('../src/github-client.js', () => {
+ return jest.fn().mockImplementation(() => ({
+ fetchPullRequests: jest.fn(),
+ fetchPRTimelineEvents: jest.fn(),
+ fetchPRReviewEvents: jest.fn(),
+ calculatePickupTime: jest.fn(),
+ calculateTimeToMerge: jest.fn()
+ }));
+});
+
+// Mock BigQueryClient
+jest.mock('../src/bigquery-client.js', () => ({
+ BigQueryClient: jest.fn().mockImplementation(() => ({
+ uploadMetrics: jest.fn()
+ }))
+}));
+
+describe('MetricsCollector', () => {
+ let metricsCollector;
+ let mockConfig;
+ let mockGitHubClient;
+ let mockBigQueryClient;
+
+ beforeEach(() => {
+ mockConfig = {
+ githubToken: 'fake-token',
+ serviceAccountKeyPath: '/fake/path',
+ repositories: ['owner/repo'],
+ lookbackDays: 7,
+ targetBranch: 'main',
+ bigQueryDatasetId: 'test_dataset',
+ printOnly: false,
+ metrics: {
+ timeToFirstReview: {
+ enabled: true,
+ tableName: 'pr_first_review'
+ },
+ timeToMerge: {
+ enabled: true,
+ tableName: 'pr_merge'
+ }
+ }
+ };
+
+ metricsCollector = new MetricsCollector(mockConfig);
+
+ // Mock the clients
+ mockGitHubClient = {
+ fetchPullRequests: jest.fn(),
+ fetchPRTimelineEvents: jest.fn(),
+ fetchPRReviewEvents: jest.fn(),
+ calculatePickupTime: jest.fn(),
+ calculateTimeToMerge: jest.fn()
+ };
+
+ mockBigQueryClient = {
+ uploadMetrics: jest.fn()
+ };
+
+ metricsCollector.githubClient = mockGitHubClient;
+ metricsCollector.bigqueryClient = mockBigQueryClient;
+ });
+
+ describe('collectPRMetrics', () => {
+ const mockPR = {
+ number: 123,
+ html_url: 'https://github.com/owner/repo/pull/123',
+ user: { login: 'testuser' },
+ base: { ref: 'main' },
+ head: { repo: { full_name: 'owner/repo' } }
+ };
+
+ const mockTimelineEvents = [
+ { event: 'ready_for_review', created_at: '2023-06-15T10:00:00Z' }
+ ];
+
+ const mockReviewEvents = [
+ { submitted_at: '2023-06-15T12:00:00Z', state: 'approved', user: { login: 'reviewer1' } }
+ ];
+
+ test('should collect both metrics when both are enabled', async () => {
+ const firstReviewMetric = {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ pickupTimeSeconds: 7200
+ };
+
+ const mergeMetric = {
+ metricType: 'time_to_merge',
+ prNumber: 123,
+ mergeTimeSeconds: 16200
+ };
+
+ mockGitHubClient.calculatePickupTime.mockReturnValue(firstReviewMetric);
+ mockGitHubClient.calculateTimeToMerge.mockReturnValue(mergeMetric);
+
+ const result = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ expect(result).toEqual([firstReviewMetric, mergeMetric]);
+ expect(mockGitHubClient.calculatePickupTime).toHaveBeenCalledWith(mockPR, mockTimelineEvents, mockReviewEvents);
+ expect(mockGitHubClient.calculateTimeToMerge).toHaveBeenCalledWith(mockPR, mockTimelineEvents, mockReviewEvents);
+ });
+
+ test('should only collect first review metric when merge is disabled', async () => {
+ metricsCollector.config.metrics.timeToMerge.enabled = false;
+
+ const firstReviewMetric = {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ pickupTimeSeconds: 7200
+ };
+
+ mockGitHubClient.calculatePickupTime.mockReturnValue(firstReviewMetric);
+
+ const result = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ expect(result).toEqual([firstReviewMetric]);
+ expect(mockGitHubClient.calculatePickupTime).toHaveBeenCalled();
+ expect(mockGitHubClient.calculateTimeToMerge).not.toHaveBeenCalled();
+ });
+
+ test('should only collect merge metric when first review is disabled', async () => {
+ metricsCollector.config.metrics.timeToFirstReview.enabled = false;
+
+ const mergeMetric = {
+ metricType: 'time_to_merge',
+ prNumber: 123,
+ mergeTimeSeconds: 16200
+ };
+
+ mockGitHubClient.calculateTimeToMerge.mockReturnValue(mergeMetric);
+
+ const result = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ expect(result).toEqual([mergeMetric]);
+ expect(mockGitHubClient.calculatePickupTime).not.toHaveBeenCalled();
+ expect(mockGitHubClient.calculateTimeToMerge).toHaveBeenCalled();
+ });
+
+ test('should handle null metrics gracefully', async () => {
+ mockGitHubClient.calculatePickupTime.mockReturnValue(null);
+ mockGitHubClient.calculateTimeToMerge.mockReturnValue(null);
+
+ const result = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ expect(result).toEqual([]);
+ });
+
+ test('should handle errors in metric calculation', async () => {
+ mockGitHubClient.calculatePickupTime.mockImplementation(() => {
+ throw new Error('Calculation error');
+ });
+
+ const mergeMetric = {
+ metricType: 'time_to_merge',
+ prNumber: 123,
+ mergeTimeSeconds: 16200
+ };
+
+ mockGitHubClient.calculateTimeToMerge.mockReturnValue(mergeMetric);
+
+ const result = await metricsCollector.collectPRMetrics(mockPR, mockTimelineEvents, mockReviewEvents);
+
+ expect(result).toEqual([mergeMetric]);
+ });
+ });
+
+ describe('groupMetricsByType', () => {
+ test('should group metrics by type correctly', () => {
+ const metrics = [
+ { metricType: 'time_to_first_review', prNumber: 123 },
+ { metricType: 'time_to_merge', prNumber: 123 },
+ { metricType: 'time_to_first_review', prNumber: 124 },
+ { metricType: 'time_to_merge', prNumber: 124 }
+ ];
+
+ const grouped = metricsCollector.groupMetricsByType(metrics);
+
+ expect(grouped).toEqual({
+ time_to_first_review: [
+ { metricType: 'time_to_first_review', prNumber: 123 },
+ { metricType: 'time_to_first_review', prNumber: 124 }
+ ],
+ time_to_merge: [
+ { metricType: 'time_to_merge', prNumber: 123 },
+ { metricType: 'time_to_merge', prNumber: 124 }
+ ]
+ });
+ });
+
+ test('should handle empty metrics array', () => {
+ const grouped = metricsCollector.groupMetricsByType([]);
+ expect(grouped).toEqual({});
+ });
+ });
+
+ describe('getTableNameForMetricType', () => {
+ test('should return correct table name for time_to_first_review', () => {
+ const tableName = metricsCollector.getTableNameForMetricType('time_to_first_review');
+ expect(tableName).toBe('pr_first_review');
+ });
+
+ test('should return correct table name for time_to_merge', () => {
+ const tableName = metricsCollector.getTableNameForMetricType('time_to_merge');
+ expect(tableName).toBe('pr_merge');
+ });
+
+ test('should throw error for unknown metric type', () => {
+ expect(() => {
+ metricsCollector.getTableNameForMetricType('unknown_type');
+ }).toThrow('Unknown metric type: unknown_type');
+ });
+ });
+
+ describe('uploadMetrics', () => {
+ test('should upload metrics to correct tables', async () => {
+ const metrics = [
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ pickupTimeSeconds: 7200
+ },
+ {
+ metricType: 'time_to_merge',
+ prNumber: 123,
+ mergeTimeSeconds: 16200
+ },
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 124,
+ pickupTimeSeconds: 3600
+ }
+ ];
+
+ await metricsCollector.uploadMetrics(metrics);
+
+ expect(mockBigQueryClient.uploadMetrics).toHaveBeenCalledTimes(2);
+
+ // Check first call (time_to_first_review)
+ expect(mockBigQueryClient.uploadMetrics).toHaveBeenNthCalledWith(
+ 1,
+ 'test_dataset',
+ 'pr_first_review',
+ [
+ { metricType: 'time_to_first_review', prNumber: 123, pickupTimeSeconds: 7200 },
+ { metricType: 'time_to_first_review', prNumber: 124, pickupTimeSeconds: 3600 }
+ ]
+ );
+
+ // Check second call (time_to_merge)
+ expect(mockBigQueryClient.uploadMetrics).toHaveBeenNthCalledWith(
+ 2,
+ 'test_dataset',
+ 'pr_merge',
+ [
+ { metricType: 'time_to_merge', prNumber: 123, mergeTimeSeconds: 16200 }
+ ]
+ );
+ });
+
+ test('should handle empty metrics array', async () => {
+ await metricsCollector.uploadMetrics([]);
+
+ expect(mockBigQueryClient.uploadMetrics).not.toHaveBeenCalled();
+ });
+
+ test('should skip empty metric groups', async () => {
+ const metrics = [
+ {
+ metricType: 'time_to_first_review',
+ prNumber: 123,
+ pickupTimeSeconds: 7200
+ }
+ ];
+
+ await metricsCollector.uploadMetrics(metrics);
+
+ expect(mockBigQueryClient.uploadMetrics).toHaveBeenCalledTimes(1);
+ expect(mockBigQueryClient.uploadMetrics).toHaveBeenCalledWith(
+ 'test_dataset',
+ 'pr_first_review',
+ [{ metricType: 'time_to_first_review', prNumber: 123, pickupTimeSeconds: 7200 }]
+ );
+ });
+ });
+
+ describe('getMetricTypeDisplayName', () => {
+ test('should return correct display names', () => {
+ expect(metricsCollector.getMetricTypeDisplayName('time_to_first_review')).toBe('Time to First Review');
+ expect(metricsCollector.getMetricTypeDisplayName('time_to_merge')).toBe('Time to Merge');
+ expect(metricsCollector.getMetricTypeDisplayName('unknown_type')).toBe('unknown_type');
+ });
+ });
+
+ describe('getTimeFieldForMetricType', () => {
+ test('should return correct time fields', () => {
+ const firstReviewMetric = { pickupTimeSeconds: 7200, mergeTimeSeconds: 16200 };
+ const mergeMetric = { pickupTimeSeconds: 7200, mergeTimeSeconds: 16200 };
+
+ expect(metricsCollector.getTimeFieldForMetricType('time_to_first_review', firstReviewMetric)).toBe(7200);
+ expect(metricsCollector.getTimeFieldForMetricType('time_to_merge', mergeMetric)).toBe(16200);
+ expect(metricsCollector.getTimeFieldForMetricType('unknown_type', {})).toBe(0);
+ });
+ });
+});
diff --git a/.github/actions/eng-metrics/test/setup.js b/.github/actions/eng-metrics/test/setup.js
new file mode 100644
index 0000000000..00a88c4a96
--- /dev/null
+++ b/.github/actions/eng-metrics/test/setup.js
@@ -0,0 +1,16 @@
+/**
+ * Jest setup file for engineering metrics tests
+ */
+import { jest } from "@jest/globals";
+
+// Set test environment variables
+process.env.NODE_ENV = "test";
+
+// Mock Date.now for consistent testing
+beforeAll(() => {
+ jest.useFakeTimers().setSystemTime(new Date("2023-06-15T12:00:00Z"));
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
diff --git a/.github/workflows/collect-eng-metrics-test.yml b/.github/workflows/collect-eng-metrics-test.yml
new file mode 100644
index 0000000000..2f39a6e0c4
--- /dev/null
+++ b/.github/workflows/collect-eng-metrics-test.yml
@@ -0,0 +1,57 @@
+name: Collect engineering metrics test
+
+on:
+ push:
+ branches:
+ - main
+ - patch-*
+ - prepare-*
+ paths:
+ - '.github/actions/eng-metrics/**'
+ pull_request:
+ paths:
+ - '.github/actions/eng-metrics/**'
+ workflow_dispatch:
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Test Engineering Metrics Action
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
+
+ - name: Setup Node.js 20
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0
+ with:
+ node-version: 20
+ cache: 'npm'
+ cache-dependency-path: '.github/actions/eng-metrics/package-lock.json'
+
+ - name: Install dependencies
+ run: npm ci
+ working-directory: .github/actions/eng-metrics
+
+ - name: Run linting
+ run: npm run lint
+ working-directory: .github/actions/eng-metrics
+
+ - name: Run tests
+ run: npm test
+ working-directory: .github/actions/eng-metrics
+ env:
+ NODE_ENV: test
diff --git a/.github/workflows/collect-eng-metrics.yml b/.github/workflows/collect-eng-metrics.yml
new file mode 100644
index 0000000000..f02d8f1c32
--- /dev/null
+++ b/.github/workflows/collect-eng-metrics.yml
@@ -0,0 +1,55 @@
+name: Collect engineering metrics
+
+on:
+ schedule:
+ - cron: '0 9 * * *' # Run at 4am CDT (9am UTC)
+ workflow_dispatch: # Allow manual triggering
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+permissions:
+ contents: read # fetch repo metadata
+ pull-requests: read # read PR timelines
+
+jobs:
+ collect-metrics:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout code
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0
+ with:
+ node-version: '20.18.1'
+ cache: 'npm'
+ cache-dependency-path: .github/actions/eng-metrics/package-lock.json
+
+ - name: Install dependencies
+ working-directory: .github/actions/eng-metrics
+ run: npm ci
+
+ - name: Create service account key file
+ working-directory: .github/actions/eng-metrics
+ run: |
+ echo '${{ secrets.ENG_METRICS_GCP_SERVICE_ACCOUNT_KEY }}' > service-account-key.json
+ # Verify the file is valid JSON and is a top-level object
+ if ! jq 'type == "object"' service-account-key.json | grep -q true; then
+ echo "Error: service-account-key.json is either invalid JSON or not a JSON object"
+ echo "Is ENG_METRICS_GCP_SERVICE_ACCOUNT_KEY secret properly set?"
+ exit 1
+ fi
+
+ - name: Collect and upload engineering metrics
+ uses: ./.github/actions/eng-metrics
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SERVICE_ACCOUNT_KEY_PATH: './service-account-key.json'