diff --git a/.gitignore b/.gitignore index c472c0de..2008089b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ dist/ .idea/ plan.md coverage/ +tests/integration/resources/sonar-secrets* .eslintcache diff --git a/CLAUDE.md b/CLAUDE.md index e51d8467..082419fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,11 @@ Use `runCommand()` from `src/lib/run-command.ts` to wrap command handlers — it ## Tests +Please try to create integration tests in priority. If the test is too complicated to set up, write unit tests. +Try to get inspiration from other tests to follow the same structure. + - Unit tests: `tests/unit/` — run with `bun test` -- Integration tests: `tests/integration/` — require env vars, skipped locally by default +- Integration tests: `tests/integration/` — require env vars. They are using a harness to help set up tests and make assertions. - The UI module has a built-in mock system (`src/ui/mock.ts`) — use it instead of mocking stdout directly. ## Documentation diff --git a/build-scripts/setup-integration-resources.ts b/build-scripts/setup-integration-resources.ts new file mode 100644 index 00000000..1d3f5f7d --- /dev/null +++ b/build-scripts/setup-integration-resources.ts @@ -0,0 +1,89 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Downloads the sonar-secrets binary and its PGP signature for the current + * platform from binaries.sonarsource.com and places them in + * tests/integration/resources/ using the original versioned filenames + * (e.g. sonar-secrets-2.41.0.10709-linux-x86-64.exe). + * + * Run via: bun build-scripts/setup-integration-resources.ts + * Or via: bun run test:integration:prepare + */ + +import { existsSync, mkdirSync } from 'node:fs'; +import { chmod } from 'node:fs/promises'; +import { join } from 'node:path'; +import { detectPlatform } from '../src/lib/platform-detector.js'; +import { + buildDownloadUrl, + downloadBinary, + verifyBinarySignature, +} from '../src/lib/sonarsource-releases.js'; +import { + SONAR_SECRETS_VERSION, + SONAR_SECRETS_SIGNATURES, + SONARSOURCE_PUBLIC_KEY, +} from '../src/lib/signatures.js'; + +const RESOURCES_DIR = join(import.meta.dir, '..', 'tests', 'integration', 'resources'); +const platform = detectPlatform(); +const downloadUrl = buildDownloadUrl(SONAR_SECRETS_VERSION, platform); +const signatureUrl = `${downloadUrl}.asc`; +// Keep the original versioned filename so the fake binaries server can match requests exactly +const downloadFilename = downloadUrl.split('/').at(-1)!; +const destPath = join(RESOURCES_DIR, downloadFilename); +const ascDestPath = join(RESOURCES_DIR, `${downloadFilename}.asc`); + +const binaryExists = existsSync(destPath); +const ascExists = existsSync(ascDestPath); + +if (binaryExists && ascExists) { + console.log(`Resources already present at ${RESOURCES_DIR} — skipping download.`); + process.exit(0); +} + +mkdirSync(RESOURCES_DIR, { recursive: true }); + +if (!binaryExists) { + console.log( + `Downloading sonar-secrets ${SONAR_SECRETS_VERSION} for ${platform.os}-${platform.arch}`, + ); + console.log(` from ${downloadUrl}`); + await downloadBinary(downloadUrl, destPath); + console.log(' Download complete.'); + + console.log('Verifying PGP signature...'); + await verifyBinarySignature(destPath, platform, SONAR_SECRETS_SIGNATURES, SONARSOURCE_PUBLIC_KEY); + console.log(' Signature verified.'); + + if (platform.os !== 'windows') { + await chmod(destPath, 0o755); + } + + console.log(`sonar-secrets ready at ${destPath}`); +} + +if (!ascExists) { + console.log(`Downloading PGP signature file...`); + console.log(` from ${signatureUrl}`); + await downloadBinary(signatureUrl, ascDestPath); + console.log(` Signature file ready at ${ascDestPath}`); +} diff --git a/build-scripts/update-version.sh b/build-scripts/update-version.sh index 99277523..22e827af 100644 --- a/build-scripts/update-version.sh +++ b/build-scripts/update-version.sh @@ -42,11 +42,12 @@ fi echo "🔄 Updating version to $NEW_VERSION..." -# Update package.json (single source of truth) +# Update package.json (single source of truth — only top-level version field) echo " 📝 Updating package.json..." -sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$NEW_VERSION\"/" package.json +npm pkg set version="$NEW_VERSION" --no-git-tag-version 2>/dev/null || \ + node -e "const fs=require('fs'),p='package.json',j=JSON.parse(fs.readFileSync(p,'utf8'));j.version='$NEW_VERSION';fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" -# Update VERSION in src/version.ts (preserve license header) +# Update src/version.ts (single-line export, safe to replace) echo " 📝 Updating src/version.ts..." sed -i '' "s/export const VERSION = '[^']*';/export const VERSION = '$NEW_VERSION';/" src/version.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..11c18272 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4317 @@ +{ + "name": "sonarqube-cli", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sonarqube-cli", + "version": "0.4.0", + "dependencies": { + "@clack/core": "^1.0.1", + "@toon-format/toon": "^2.1.0", + "commander": "^12.1.0", + "keytar": "^7.9.0", + "openpgp": "^5.11.2", + "picocolors": "^1.1.1" + }, + "bin": { + "sonar": "dist/index.js" + }, + "devDependencies": { + "@sonar/scan": "^4.3.0", + "@types/bun": "^1.3.9", + "@types/node": "^22.0.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "ajv": "^8.18.0", + "eslint": "^10.0.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-headers": "^1.3.4", + "husky": "9.1.7", + "js-yaml": "^4.1.1", + "lint-staged": "16.2.7", + "plop": "^4.0.5", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@clack/core": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@clack/core/-/core-1.0.1.tgz", + "integrity": "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.2", + "debug": "^4.3.1", + "minimatch": "^10.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@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.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@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://repox.jfrog.io/artifactory/api/npm/npm/@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/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sonar/scan": { + "version": "4.3.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sonar/scan/-/@sonar/scan-4.3.4.tgz", + "integrity": "sha512-Xa1Ln9Onmmgwidi9EQljPkWDqFnVX+DhJbTW16NHFMccCas2PXyyVsBezp873i4YgsYYyV6Yn59Ipm4eMVDV5Q==", + "dev": true, + "dependencies": { + "adm-zip": "0.5.16", + "axios": "1.13.2", + "commander": "13.1.0", + "fs-extra": "11.3.3", + "hpagent": "1.2.0", + "node-forge": "1.3.3", + "properties-file": "3.6.3", + "proxy-from-env": "1.1.0", + "semver": "7.7.3", + "slugify": "1.6.6", + "tar-stream": "3.1.7" + }, + "bin": { + "sonar": "bin/sonar-scanner.js", + "sonar-scanner": "bin/sonar-scanner.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sonar/scan/node_modules/b4a": { + "version": "1.7.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/b4a/-/b4a-1.7.5.tgz", + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/@sonar/scan/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sonar/scan/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sonar/scan/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@toon-format/toon": { + "version": "2.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@toon-format/toon/-/toon-2.1.0.tgz", + "integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg==", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.3.9", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/bun/-/bun-1.3.9.tgz", + "integrity": "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.9" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fined": { + "version": "1.1.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/fined/-/fined-1.1.5.tgz", + "integrity": "sha512-2N93vadEGDFhASTIRbizbl4bNqpMOId5zZfj6hHqYZfEzEfO9onnU4Im8xvzo8uudySDveDHBOOSlTWf38ErfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/inquirer": { + "version": "9.0.9", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/inquirer/-/inquirer-9.0.9.tgz", + "integrity": "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/liftoff": { + "version": "4.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/liftoff/-/liftoff-4.0.3.tgz", + "integrity": "sha512-UgbL2kR5pLrWICvr8+fuSg0u43LY250q7ZMkC+XKC3E+rs/YBDEnQIzsnhU5dYsLlwMi3R75UvCL87pObP1sxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fined": "*", + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/picomatch": { + "version": "4.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/adm-zip": { + "version": "0.5.16", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/bl": { + "version": "4.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/buffer": { + "version": "5.7.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bun-types": { + "version": "1.3.9", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/bun-types/-/bun-types-1.3.9.tgz", + "integrity": "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/debug": { + "version": "4.4.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/environment": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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": "10.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.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", + "minimatch": "^10.2.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-headers": { + "version": "1.3.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-plugin-headers/-/eslint-plugin-headers-1.3.4.tgz", + "integrity": "sha512-sfgmrq+geMaB7yilx1wbbzSHTmLRPm+hX6kxJhb6LQQwmF00CqZ8d3ks2ZADlDdREhP31THPHgRhcQuDogVZ4w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.0.0 || >= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ajv": { + "version": "6.14.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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/findup-sync": { + "version": "5.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/global-modules": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/handlebars": { + "version": "4.7.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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": "5.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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-interactive": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/jsonfile": { + "version": "6.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/levn": { + "version": "0.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/liftoff": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/log-symbols": { + "version": "4.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/log-update": { + "version": "6.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-plop": { + "version": "0.32.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-plop/-/node-plop-0.32.3.tgz", + "integrity": "sha512-tn+OxutdqhvoByKJ7p84FZBSUDfUB76bcvj0ugLBvgE9V52LFcnz8cauCDKi6otnctvFCqa9XkrU35pBY5Baig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/inquirer": "^9.0.9", + "@types/picomatch": "^4.0.2", + "change-case": "^5.4.4", + "dlv": "^1.1.3", + "handlebars": "^4.7.8", + "inquirer": "^9.3.8", + "isbinaryfile": "^5.0.6", + "resolve": "^1.22.10", + "tinyglobby": "^0.2.15", + "title-case": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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/openpgp": { + "version": "5.11.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/openpgp/-/openpgp-5.11.3.tgz", + "integrity": "sha512-jXOPfIteBUQ2zSmRG4+Y6PNntIIDEAvoM/lOYCnvpXAByJEruzrHQZWE/0CGOKHbubwUuty2HoPHsqBzyKHOpA==", + "deprecated": "This version is deprecated and will no longer receive security patches. Please refer to https://github.com/openpgpjs/openpgpjs/wiki/Updating-from-previous-versions for details on how to upgrade to a newer supported version.", + "license": "LGPL-3.0+", + "dependencies": { + "asn1.js": "^5.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ora": { + "version": "5.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/p-limit": { + "version": "3.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/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/parse-filepath": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/pidtree": { + "version": "0.6.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/plop": { + "version": "4.0.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/plop/-/plop-4.0.5.tgz", + "integrity": "sha512-pJz6oWC9LyBp5mBrRp8AUV2RNiuGW+t/HOs4zwN+b/3YxoObZOOFvjn1mJMpAeKi2pbXADMFOOVQVTVXEdDHDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/liftoff": "^4.0.3", + "interpret": "^3.1.1", + "liftoff": "^5.0.1", + "nanospinner": "^1.2.2", + "node-plop": "^0.32.3", + "picocolors": "^1.1.1", + "v8flags": "^4.0.1" + }, + "bin": { + "plop": "bin/plop.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/properties-file": { + "version": "3.6.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/properties-file/-/properties-file-3.6.3.tgz", + "integrity": "sha512-T0BLq5U7vMtfoHMAyirR386h/PkS9rva/EQIvy3yFmkYIjc485tgxN7eHgbtJx48O28A94MUYuRGF/iyC/L54A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/rechoir": { + "version": "0.8.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/rfdc": { + "version": "1.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/streamx": { + "version": "2.23.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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-argv": { + "version": "0.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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-json-comments": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/b4a/-/b4a-1.7.5.tgz", + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/title-case": { + "version": "4.3.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/title-case/-/title-case-4.3.2.tgz", + "integrity": "sha512-I/nkcBo73mO42Idfv08jhInV61IMb61OdIFxk+B4Gu1oBjWBPOLmhZdsli+oJCVaD+86pYQA93cJfFt224ZFAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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/typescript": { + "version": "5.9.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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://repox.jfrog.io/artifactory/api/npm/npm/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/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" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 0643faca..0d4205ce 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,14 @@ "sonar": "./dist/index.js" }, "scripts": { - "build": "bun run validate && tsc", + "build": "tsc", "build:binary": "bun build src/index.ts --compile --outfile dist/sonarqube-cli", "dev": "bun run src/index.ts", - "test": "bun test ./tests/unit/", - "test:integration": "bun test ./tests/integration/", - "test:scripts": "bun test tests/build-scripts/", - "test:all": "bun test ./tests/", - "test:coverage": "bun test ./tests/ --coverage --coverage-reporter=lcov", + "test:unit": "bun test ./tests/unit/", + "test:integration:prepare": "bun build:binary && bun build-scripts/setup-integration-resources.ts", + "test:integration": "bun test:integration:prepare && bun test ./tests/integration/", + "test:all": "bun test:unit && bun test:integration", + "test:coverage": "bun test:integration:prepare && bun test --coverage --coverage-reporter=lcov", "lint": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", @@ -27,7 +27,7 @@ }, "externalBinaries": { "sonar-secrets": { - "version": "2.41.0.10709", + "version": "0.5.3", "binaryPath": "CommercialDistribution/sonar-secrets", "platforms": [ { diff --git a/src/cli/commands/list.ts b/src/cli/commands/list.ts index 1c123891..e2fe4647 100644 --- a/src/cli/commands/list.ts +++ b/src/cli/commands/list.ts @@ -53,8 +53,6 @@ export interface ListIssuesOptions { * Issues search command handler */ export async function listIssues(options: ListIssuesOptions): Promise { - const resolvedAuth = await resolveAuth({ org: options.org }); - const format = options.format ?? 'json'; if (!VALID_FORMATS.includes(format.toLowerCase())) { throw new Error(`Invalid format: '${format}'. Must be one of: ${VALID_FORMATS.join(', ')}`); @@ -83,6 +81,7 @@ export async function listIssues(options: ListIssuesOptions): Promise { throw new Error('--project is required'); } + const resolvedAuth = await resolveAuth({ org: options.org }); const client = new SonarQubeClient(resolvedAuth.serverUrl, resolvedAuth.token); const issuesClient = new IssuesClient(client); @@ -136,8 +135,6 @@ export interface ListProjectsOptions { * Projects search command handler */ export async function listProjects(options: ListProjectsOptions): Promise { - const resolvedAuth = await resolveAuth({ org: options.org }); - const pageSize = options.pageSize; if (pageSize < 1 || pageSize > MAX_PAGE_SIZE) { throw new Error( @@ -150,6 +147,7 @@ export async function listProjects(options: ListProjectsOptions): Promise throw new Error(`Invalid --page option: '${page}'. Must be an integer >= 1`); } + const resolvedAuth = await resolveAuth({ org: options.org }); const client = new SonarQubeClient(resolvedAuth.serverUrl, resolvedAuth.token); const projectsClient = new ProjectsClient(client); diff --git a/src/lib/config-constants.ts b/src/lib/config-constants.ts index 862950b2..fb41ed77 100644 --- a/src/lib/config-constants.ts +++ b/src/lib/config-constants.ts @@ -38,8 +38,8 @@ export const APP_NAME = 'sonarqube-cli'; // CLI data directory // --------------------------------------------------------------------------- -/** Root directory for all CLI data: ~/.sonar/sonarqube-cli (override via SONAR_CLI_DIR env var in test environment) */ -export const CLI_DIR = process.env.SONAR_CLI_DIR ?? join(homedir(), '.sonar', APP_NAME); +/** Root directory for all CLI data: ~/.sonar/sonarqube-cli */ +export const CLI_DIR = join(homedir(), '.sonar', APP_NAME); // --------------------------------------------------------------------------- // State @@ -64,7 +64,9 @@ export const BIN_DIR = join(CLI_DIR, 'bin'); // Sonarsource binaries // --------------------------------------------------------------------------- -export const SONARSOURCE_BINARIES_URL = 'https://binaries.sonarsource.com'; +/** Base URL for downloading SonarSource binaries. Override via SONAR_CLI_BINARIES_URL for test environments. */ +export const SONARSOURCE_BINARIES_URL = + process.env.SONAR_CLI_BINARIES_URL ?? 'https://binaries.sonarsource.com'; export const SONAR_SECRETS_DIST_PREFIX = 'CommercialDistribution/sonar-secrets'; // --------------------------------------------------------------------------- diff --git a/src/lib/keychain.ts b/src/lib/keychain.ts index cc3cc094..76f7aeb0 100644 --- a/src/lib/keychain.ts +++ b/src/lib/keychain.ts @@ -20,6 +20,7 @@ // Keychain operations wrapper for keytar +import { readFileSync, writeFileSync } from 'node:fs'; import { APP_NAME as SERVICE_NAME } from './config-constants.js'; interface Credential { @@ -44,11 +45,60 @@ const noOpKeytar: KeytarModule = { findCredentials: () => Promise.resolve([]), }; +interface KeychainStore { + tokens: Record; +} + +function createFileKeytar(filePath: string): KeytarModule { + const readStore = (): KeychainStore => { + try { + return JSON.parse(readFileSync(filePath, 'utf-8')) as KeychainStore; + } catch { + return { tokens: {} }; + } + }; + + const writeStore = (store: KeychainStore): void => { + writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8'); + }; + + return { + getPassword: (_service, account) => Promise.resolve(readStore().tokens[account] ?? null), + setPassword: (_service, account, password) => { + const store = readStore(); + store.tokens[account] = password; + writeStore(store); + return Promise.resolve(); + }, + deletePassword: (_service, account) => { + const store = readStore(); + if (!(account in store.tokens)) { + return Promise.resolve(false); + } + const { [account]: _removed, ...remaining } = store.tokens; + store.tokens = remaining; + writeStore(store); + return Promise.resolve(true); + }, + findCredentials: (_service) => { + const store = readStore(); + return Promise.resolve( + Object.entries(store.tokens).map(([account, password]) => ({ account, password })), + ); + }, + }; +} + export function clearTokenCache(): void { tokenCache.clear(); } async function getKeytar() { + const keychainFile = process.env.SONAR_CLI_KEYCHAIN_FILE; + if (keychainFile) { + return createFileKeytar(keychainFile); + } + if (process.env.SONAR_CLI_DISABLE_KEYCHAIN === 'true') { return noOpKeytar; } diff --git a/src/lib/sonarsource-releases.ts b/src/lib/sonarsource-releases.ts index 0b39e129..ba39aa50 100644 --- a/src/lib/sonarsource-releases.ts +++ b/src/lib/sonarsource-releases.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// Sonarsource binaries client for downloading sonar-secrets +// SonarSource binaries client for downloading sonar-secrets import { readFileSync } from 'node:fs'; import type { PlatformInfo } from './install-types.js'; diff --git a/tests/integration/README.md b/tests/integration/README.md deleted file mode 100644 index c2376b2b..00000000 --- a/tests/integration/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Integration Tests - -Integration tests verify real behavior of CLI commands with real dependencies (binaries, APIs, filesystem). - -## Required Environment Variables - -To run integration tests, set the following environment variables: - -### For sonar-secrets checks - -```bash -export SONAR_SECRETS_AUTH_URL="https://sonarcloud.io" -export SONAR_SECRETS_TOKEN="" -``` - -**How to get a token:** -- Go to https://sonarcloud.io -- Sign in to your account -- Account → Security → Generate token -- Copy the token - -**Alternative:** If you don't have a token, integration tests for sonar-secrets will be skipped gracefully. - -### For onboard-agent tests - -```bash -export SONARCLOUD_TOKEN="" -``` - -(Can be the same value as `SONAR_SECRETS_TOKEN`) - -## Running Integration Tests - -```bash -# Set environment variables -export SONAR_SECRETS_TOKEN="sqp_xxxxx" -export SONARCLOUD_TOKEN="sqp_xxxxx" - -# Run integration tests -npm run test:integration - -# Or run all tests (unit + integration) -npm run test:all -``` - -## How Tests Work - -### Automatic Binary Download - -Before running sonar-secrets integration tests: -- Checks for binary in `~/.sonar/sonarqube-cli/bin/sonar-secrets` -- If missing - automatically downloads and installs from GitHub releases -- Uses real binary from official releases - -### Process - -``` -beforeAll() → - ✓ Check sonar-secrets binary exists - ✓ If not → download via `dist/sonar-cli secret install` - -each test → - ✓ Call `dist/sonar-cli secret check --stdin` or `--file` - ✓ Verify exit code and output - ✓ Clean up temporary files - -afterAll() → - ✓ Optional cleanup -``` - -## Test Structure - -``` -tests/integration/ -├── README.md # This file -├── onboard.test.ts # Tests for onboard-agent command -└── secret-check.test.ts # Tests for sonar secret check (stdin + file) -``` - -## What's NOT Tested Here - -- Thread safety -- Edge cases (tested in unit tests) -- Internal function logic (tested in unit tests) - -## Troubleshooting - -### "sonar-secrets command not found" - -```bash -# Install binary manually -dist/sonar-cli secret install -``` - -### "Scan timed out" - -Timeout on large files or slow internet is acceptable behavior. - -### "Token authentication failed" - -Check that: -1. Token hasn't expired (generate new one on sonarcloud.io) -2. Environment variable is set correctly: `echo $SONAR_SECRETS_TOKEN` -3. Test has access to the variable - -### Tests Skipped - -If token is not set - tests are skipped automatically: - -```typescript -if (!process.env.SONAR_SECRETS_TOKEN) { - test.skip('SONAR_SECRETS_TOKEN not set', () => {}) -} -``` - -This is normal for local development. - -## CI/CD - -In GitHub Actions, add secrets: - -```yaml -env: - SONAR_SECRETS_TOKEN: ${{ secrets.SONAR_SECRETS_TOKEN }} - SONARCLOUD_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }} - -jobs: - integration-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - run: npm run test:integration -``` diff --git a/tests/integration/harness/cli-runner.ts b/tests/integration/harness/cli-runner.ts new file mode 100644 index 00000000..642ef128 --- /dev/null +++ b/tests/integration/harness/cli-runner.ts @@ -0,0 +1,173 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// CLI runner — spawns the compiled sonarqube-cli binary and captures output + +import { existsSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import type { CliResult } from './types.js'; + +const PROJECT_ROOT = join(import.meta.dir, '../../..'); +const DEFAULT_BINARY = join(PROJECT_ROOT, 'dist', 'sonarqube-cli'); +const BINARY_PATH = process.env.SONAR_CLI_BINARY ?? DEFAULT_BINARY; +const DEFAULT_TIMEOUT_MS = 30000; + +export function assertBinaryExists(): void { + if (!existsSync(BINARY_PATH)) { + throw new Error( + `CLI binary not found at: ${BINARY_PATH}\n` + + `Run 'npm run build:binary' to build it first.\n` + + `Or set SONAR_CLI_BINARY env var to point to a different path.`, + ); + } +} + +export async function runCli( + command: string, + env: Record, + options: { stdin?: string; timeoutMs?: number; cwd: string; browserToken?: string }, +): Promise { + assertBinaryExists(); + + const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const startTime = Date.now(); + mkdirSync(options.cwd, { recursive: true }); + + const args = tokenize(command); + const proc = Bun.spawn([BINARY_PATH, ...args], { + env, + stdout: 'pipe', + stderr: 'pipe', + stdin: options.stdin ? 'pipe' : 'ignore', + cwd: options.cwd, + }); + + if (options.stdin !== undefined && proc.stdin) { + // proc.stdin is a Bun FileSink (not a Web WritableStream) + const sink = proc.stdin as { write(data: Uint8Array): void; end(): void }; + sink.write(new TextEncoder().encode(options.stdin)); + sink.end(); + } + + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + proc.kill(); + }, timeoutMs); + + let stdout: string; + + if (options.browserToken) { + stdout = await streamStdoutAndDeliverToken(proc.stdout, options.browserToken); + } else { + stdout = await new Response(proc.stdout).text(); + } + + const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]); + + clearTimeout(timer); + + if (timedOut) { + throw new Error(`CLI process timed out after ${timeoutMs}ms`); + } + + return { + exitCode, + stdout, + stderr, + durationMs: Date.now() - startTime, + }; +} + +/** + * Reads stdout incrementally. When the loopback auth port appears in the output + * (pattern: `port=NNNNN`), delivers the token via GET to the loopback server. + * Returns the full accumulated stdout once the stream ends. + */ +async function streamStdoutAndDeliverToken( + stream: ReadableStream, + token: string, +): Promise { + const decoder = new TextDecoder(); + const reader = stream.getReader(); + let accumulated = ''; + let tokenDelivered = false; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + accumulated += decoder.decode(value, { stream: true }); + + if (!tokenDelivered) { + const match = accumulated.match(/[?&]port=(\d+)/); + if (match) { + tokenDelivered = true; + const port = match[1]; + fetch(`http://127.0.0.1:${port}/?token=${encodeURIComponent(token)}`).catch(() => { + /* loopback server may close before response completes */ + }); + } + } + } + } finally { + reader.releaseLock(); + } + + return accumulated; +} + +/** + * Tokenize a command string into an args array. + * Handles single- and double-quoted strings to support paths with spaces. + */ +function tokenize(command: string): string[] { + const args: string[] = []; + let current = ''; + let inQuote = false; + let quoteChar = ''; + + for (const char of command) { + if (inQuote) { + if (char === quoteChar) { + inQuote = false; + } else { + current += char; + } + } else if (char === '"' || char === "'") { + inQuote = true; + quoteChar = char; + } else if (char === ' ') { + if (current) { + args.push(current); + current = ''; + } + } else { + current += char; + } + } + + if (current) { + args.push(current); + } + + return args; +} diff --git a/tests/integration/harness/dir.ts b/tests/integration/harness/dir.ts new file mode 100644 index 00000000..88eb2a0c --- /dev/null +++ b/tests/integration/harness/dir.ts @@ -0,0 +1,56 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Declarative builder for test file system fixtures + +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { File } from './file'; + +export class Dir { + private readonly baseDir: string; + + constructor(baseDir: string) { + this.baseDir = baseDir; + } + + get path() { + return this.baseDir; + } + + dir(...paths: string[]): Dir { + return new Dir(join(this.baseDir, ...paths)); + } + + file(...paths: string[]): File { + return new File(join(this.baseDir, ...paths)); + } + + exists(...paths: string[]): boolean { + return this.file(...paths).exists(); + } + + writeFile(relativePath: string, content: string) { + mkdirSync(this.baseDir, { recursive: true }); + const fullPath = join(this.baseDir, relativePath); + mkdirSync(dirname(fullPath), { recursive: true }); + writeFileSync(fullPath, content, 'utf-8'); + } +} diff --git a/tests/integration/harness/environment-builder.ts b/tests/integration/harness/environment-builder.ts new file mode 100644 index 00000000..2e9942cd --- /dev/null +++ b/tests/integration/harness/environment-builder.ts @@ -0,0 +1,155 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Declarative builder for the isolated test environment: state.json + binary setup + +import { mkdirSync, writeFileSync, copyFileSync, chmodSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import type { CliState } from '../../../src/lib/state.js'; +import { getDefaultState } from '../../../src/lib/state.js'; +import { detectPlatform } from '../../../src/lib/platform-detector.js'; +import { SONAR_SECRETS_VERSION } from '../../../src/lib/signatures.js'; +import { buildDownloadUrl } from '../../../src/lib/sonarsource-releases.js'; + +/** Mirrors the account-key logic in src/lib/keychain.ts */ +function toKeychainAccount(serverURL: string, org?: string): string { + try { + const hostname = new URL(serverURL).hostname; + return org ? `${hostname}:${org}` : hostname; + } catch { + return serverURL; + } +} + +function resolveSecretsBinarySource(): string { + const platform = detectPlatform(); + const downloadUrl = buildDownloadUrl(SONAR_SECRETS_VERSION, platform); + const filename = downloadUrl.split('/').at(-1)!; + return join(import.meta.dir, '..', 'resources', filename); +} + +export class EnvironmentBuilder { + private activeConnectionUrl?: string; + private activeConnectionType: 'cloud' | 'on-premise' = 'on-premise'; + private _installSecretsBinary = false; + private readonly keychainTokens: Array<{ serverURL: string; token: string; org?: string }> = []; + + withActiveConnection(url: string, type: 'cloud' | 'on-premise' = 'on-premise'): this { + this.activeConnectionUrl = url; + this.activeConnectionType = type; + return this; + } + + /** + * Ensures sonar-secrets is available inside the isolated test environment. + * Copies the mock binary from tests/integration/resources/sonar-secrets + * into /bin/sonar-secrets. + */ + withSecretsBinaryInstalled(): this { + this._installSecretsBinary = true; + return this; + } + + /** + * Stores a token in the file-based keychain used by the isolated test environment. + * Use this to test flows that read tokens from the keychain (e.g. list projects). + */ + withKeychainToken(serverURL: string, token: string, org?: string): this { + this.keychainTokens.push({ serverURL, token, org }); + return this; + } + + build(): CliState { + const state = getDefaultState('integration-test'); + + // disable telemetry for integration tests + state.telemetry.enabled = false; + + if (this.activeConnectionUrl) { + const connectionId = 'test-connection-id'; + state.auth.isAuthenticated = true; + state.auth.connections = [ + { + id: connectionId, + type: this.activeConnectionType, + serverUrl: this.activeConnectionUrl, + authenticatedAt: new Date().toISOString(), + keystoreKey: `sonarqube-cli:${this.activeConnectionUrl}`, + }, + ]; + state.auth.activeConnectionId = connectionId; + } + + if (this._installSecretsBinary) { + state.tools = { + installed: [ + { + name: 'sonar-secrets', + version: 'integration-test', + path: resolveSecretsBinarySource(), + installedAt: new Date().toISOString(), + installedByCliVersion: 'integration-test', + }, + ], + }; + } + + return state; + } + + /** + * Writes state.json to /state.json and, if withSecretsBinaryInstalled() was called, + * copies the mock binary to /bin/sonar-secrets. + */ + writeTo(cliHome: string, keychainJsonPath: string): Promise { + const state = this.build(); + mkdirSync(cliHome, { recursive: true }); + writeFileSync(join(cliHome, 'state.json'), JSON.stringify(state, null, 2), 'utf-8'); + + if (this.keychainTokens.length > 0) { + const tokens: Record = {}; + for (const { serverURL, token, org } of this.keychainTokens) { + tokens[toKeychainAccount(serverURL, org)] = token; + } + writeFileSync(keychainJsonPath, JSON.stringify({ tokens }, null, 2), 'utf-8'); + } + + if (!this._installSecretsBinary) { + return Promise.resolve(); + } + + const binDir = join(cliHome, 'bin'); + mkdirSync(binDir, { recursive: true }); + + const source = resolveSecretsBinarySource(); + const destPath = join(binDir, 'sonar-secrets'); + if (!existsSync(destPath)) { + if (!existsSync(source)) { + throw new Error( + `sonar-secrets binary not found at: ${source}\n` + + `Run 'bun run test:integration:prepare' to download it.`, + ); + } + copyFileSync(source, destPath); + chmodSync(destPath, 0o755); + } + return Promise.resolve(); + } +} diff --git a/tests/integration/harness/fake-binaries-server.ts b/tests/integration/harness/fake-binaries-server.ts new file mode 100644 index 00000000..4773d6b8 --- /dev/null +++ b/tests/integration/harness/fake-binaries-server.ts @@ -0,0 +1,121 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Lightweight in-process fake binaries server (Bun.serve). +// Simulates binaries.sonarsource.com so that `sonar install secrets` can be exercised +// without real network calls. Serves versioned artifacts from tests/integration/resources/ +// — downloaded by setup-integration-resources.ts — and returns 404 for unknown paths. + +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { RecordedRequest } from './types.js'; + +function resourcesDir(): string { + return join(import.meta.dir, '..', 'resources'); +} + +export class FakeBinariesServer { + private readonly server: ReturnType; + private readonly requests: RecordedRequest[]; + + constructor(server: ReturnType, requests: RecordedRequest[]) { + this.server = server; + this.requests = requests; + } + + /** Base URL to pass as SONAR_CLI_BINARIES_URL. */ + baseUrl(): string { + return `http://127.0.0.1:${this.server.port}`; + } + + getRecordedRequests(): RecordedRequest[] { + return [...this.requests]; + } + + async stop(): Promise { + await this.server.stop(true); + } +} + +export class FakeBinariesServerBuilder { + private _loadArtifacts = true; + + /** + * Makes the server return 404 for every request, simulating artifacts being + * unavailable (e.g. unknown version, server outage). + */ + noArtifacts(): this { + this._loadArtifacts = false; + return this; + } + + start(): Promise { + const requests: RecordedRequest[] = []; + + // Load versioned artifacts from resources (e.g. sonar-secrets-2.41.0.10709-linux-x86-64.exe) + const files = new Map(); + if (this._loadArtifacts) { + const dir = resourcesDir(); + for (const name of readdirSync(dir)) { + if (/^sonar-secrets-.*\.exe(\.asc)?$/.test(name)) { + files.set(name, readFileSync(join(dir, name))); + } + } + } + + const server = Bun.serve({ + port: 0, + hostname: '127.0.0.1', + fetch(req) { + const url = new URL(req.url); + const path = url.pathname; + const query: Record = {}; + url.searchParams.forEach((v, k) => { + query[k] = v; + }); + const headers: Record = {}; + req.headers.forEach((v, k) => { + headers[k] = v; + }); + + requests.push({ + method: req.method, + url: req.url, + path, + query, + headers, + timestamp: Date.now(), + }); + + // Match the requested filename against known artifacts + const filename = path.split('/').at(-1) ?? ''; + const fileBytes = files.get(filename); + if (!fileBytes) { + return new Response('Not Found', { status: 404 }); + } + + const contentType = filename.endsWith('.asc') ? 'text/plain' : 'application/octet-stream'; + return new Response(fileBytes, { headers: { 'Content-Type': contentType } }); + }, + }); + + return Promise.resolve(new FakeBinariesServer(server, requests)); + } +} diff --git a/tests/integration/harness/fake-sonarqube-server.ts b/tests/integration/harness/fake-sonarqube-server.ts new file mode 100644 index 00000000..ce687ef4 --- /dev/null +++ b/tests/integration/harness/fake-sonarqube-server.ts @@ -0,0 +1,262 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Lightweight in-process mock SonarQube HTTP server (Bun.serve) + +import type { RecordedRequest } from './types.js'; +import type { SonarQubeIssue } from '../../../src/lib/types.js'; + +export interface IssueConfig { + key?: string; + ruleKey: string; + message: string; + severity?: 'INFO' | 'MINOR' | 'MAJOR' | 'CRITICAL' | 'BLOCKER'; + component?: string; + status?: string; + type?: string; + line?: number; +} + +interface ProjectData { + key: string; + name: string; + issues: Required[]; +} + +export class ProjectBuilder { + private readonly projectKey: string; + private readonly issues: Required[] = []; + + constructor(projectKey: string) { + this.projectKey = projectKey; + } + + withIssue(issue: Partial): this { + this.issues.push({ + key: issue.key ?? `ISSUE-${this.issues.length + 1}`, + ruleKey: issue.ruleKey ?? 'java:S100', + message: issue.message ?? 'Issue', + severity: issue.severity ?? 'MAJOR', + component: issue.component ?? this.projectKey, + status: issue.status ?? 'OPEN', + type: issue.type ?? 'CODE_SMELL', + line: issue.line ?? 1, + }); + return this; + } + + getData(): ProjectData { + return { + key: this.projectKey, + name: this.projectKey, + issues: this.issues, + }; + } +} + +export class FakeSonarQubeServer { + private readonly server: ReturnType; + private readonly requests: RecordedRequest[]; + + constructor(server: ReturnType, requests: RecordedRequest[]) { + this.server = server; + this.requests = requests; + } + + baseUrl(): string { + return `http://127.0.0.1:${this.server.port}`; + } + + getRecordedRequests(): RecordedRequest[] { + return [...this.requests]; + } + + async stop(): Promise { + await this.server.stop(true); + } +} + +export class FakeSonarQubeServerBuilder { + private readonly projectBuilders: Map = new Map(); + private validToken?: string; + private systemStatus: 'UP' | 'DOWN' = 'UP'; + + withProject(key: string, fn?: (p: ProjectBuilder) => void): this { + const builder = new ProjectBuilder(key); + if (fn) fn(builder); + this.projectBuilders.set(key, builder); + return this; + } + + withAuthToken(token: string): this { + this.validToken = token; + return this; + } + + start(): Promise { + const projects = new Map([...this.projectBuilders.entries()].map(([k, v]) => [k, v.getData()])); + const validToken = this.validToken; + const systemStatus = this.systemStatus; + const requests: RecordedRequest[] = []; + + const server = Bun.serve({ + port: 0, + hostname: '127.0.0.1', + fetch(req) { + const url = new URL(req.url); + const path = url.pathname; + const query: Record = {}; + url.searchParams.forEach((v, k) => { + query[k] = v; + }); + const headers: Record = {}; + req.headers.forEach((v, k) => { + headers[k] = v; + }); + + requests.push({ + method: req.method, + url: req.url, + path, + query, + headers, + timestamp: Date.now(), + }); + + const authHeader = req.headers.get('Authorization'); + const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null; + const isAuthorized = !validToken || bearerToken === validToken; + + if (!isAuthorized) { + return new Response(JSON.stringify({ errors: [{ msg: 'Unauthorized' }] }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (path === '/api/authentication/validate') { + return new Response(JSON.stringify({ valid: true }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (path === '/api/editions/is_valid_license') { + return new Response(JSON.stringify({ isValidLicense: true }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (path === '/api/system/status') { + return new Response(JSON.stringify({ status: systemStatus, version: '9.9.0.00001' }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (path === '/api/issues/search') { + const projectKey = query.projects; + const projectData = projectKey ? projects.get(projectKey) : undefined; + + const issues: SonarQubeIssue[] = + projectData?.issues.map((issue) => ({ + key: issue.key, + rule: issue.ruleKey, + severity: issue.severity, + component: issue.component, + project: projectKey ?? '', + line: issue.line, + status: issue.status, + message: issue.message, + type: issue.type, + })) ?? []; + + const pageSize = Number.parseInt(query.ps ?? '500', 10); + const page = Number.parseInt(query.p ?? '1', 10); + + return new Response( + JSON.stringify({ + total: issues.length, + p: page, + ps: pageSize, + paging: { pageIndex: page, pageSize, total: issues.length }, + issues, + }), + { headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (path === '/api/components/show') { + const componentKey = query.component; + const projectData = componentKey ? projects.get(componentKey) : undefined; + + if (!projectData) { + return new Response( + JSON.stringify({ errors: [{ msg: `Component '${componentKey}' not found` }] }), + { status: 404, headers: { 'Content-Type': 'application/json' } }, + ); + } + + return new Response( + JSON.stringify({ component: { key: projectData.key, name: projectData.name } }), + { headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (path === '/api/qualityprofiles/search') { + return new Response( + JSON.stringify({ profiles: [{ key: 'default', name: 'Sonar way', language: 'js' }] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (path === '/api/components/search' || path === '/api/projects/search') { + const allProjects = [...projects.values()].map((p) => ({ + key: p.key, + name: p.name, + qualifier: 'TRK', + })); + + const pageSize = Number.parseInt(query.ps ?? '500', 10); + const page = Number.parseInt(query.p ?? '1', 10); + + return new Response( + JSON.stringify({ + paging: { pageIndex: page, pageSize, total: allProjects.length }, + components: allProjects, + }), + { headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (path === '/api/organizations') { + return new Response(JSON.stringify({ organizations: [] }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({ errors: [{ msg: `Unknown endpoint: ${path}` }] }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + }, + }); + + return Promise.resolve(new FakeSonarQubeServer(server, requests)); + } +} diff --git a/src/version.ts b/tests/integration/harness/file.ts similarity index 63% rename from src/version.ts rename to tests/integration/harness/file.ts index 6d034497..fb7a5b39 100644 --- a/src/version.ts +++ b/tests/integration/harness/file.ts @@ -18,5 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// Auto-generated from package.json — do not edit manually -export const VERSION = '0.4.2'; +// Declarative builder for test file system fixtures + +import { existsSync, readFileSync, statSync } from 'node:fs'; + +export class File { + public readonly path: string; + + constructor(path: string) { + this.path = path; + } + + asJson(): any { + return JSON.parse(readFileSync(this.path, 'utf-8')); + } + + exists(): boolean { + return existsSync(this.path); + } + + get isExecutable(): boolean { + const stats = statSync(this.path); + return !!(stats.mode & 0o100); + } +} diff --git a/tests/integration/harness/index.ts b/tests/integration/harness/index.ts new file mode 100644 index 00000000..a3744141 --- /dev/null +++ b/tests/integration/harness/index.ts @@ -0,0 +1,184 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// TestHarness — main entry point for integration tests + +import { mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { runCli } from './cli-runner.js'; +import { EnvironmentBuilder } from './environment-builder.js'; +import { Dir } from './dir'; +import { FakeSonarQubeServer, FakeSonarQubeServerBuilder } from './fake-sonarqube-server.js'; +import { FakeBinariesServer, FakeBinariesServerBuilder } from './fake-binaries-server.js'; +import type { CliResult, RunOptions } from './types.js'; +import { File } from './file'; + +export { EnvironmentBuilder } from './environment-builder.js'; +export { + FakeSonarQubeServerBuilder, + FakeSonarQubeServer, + ProjectBuilder, +} from './fake-sonarqube-server.js'; +export { FakeBinariesServer, FakeBinariesServerBuilder } from './fake-binaries-server.js'; +export type { CliResult, RunOptions, RecordedRequest } from './types.js'; + +export class TestHarness { + private readonly tempDir: Dir; + public readonly cwd: Dir; + public readonly userHome: Dir; + public readonly cliHome: Dir; + public readonly stateJsonFile: File; + public readonly keychainJsonFile: File; + private readonly servers: FakeSonarQubeServer[] = []; + private readonly binariesServers: FakeBinariesServer[] = []; + private _envBuilder?: EnvironmentBuilder; + private _extraEnv: Record = {}; + + private constructor(tempDir: string) { + this.tempDir = new Dir(tempDir); + this.cwd = this.tempDir.dir('cwd'); + this.userHome = this.tempDir.dir('home'); + this.cliHome = this.userHome.dir('.sonar', 'sonarqube-cli'); + this.stateJsonFile = this.cliHome.file('state.json'); + this.keychainJsonFile = this.tempDir.file('keychain.json'); + } + + static create(): Promise { + const tempDir = join( + tmpdir(), + `sonar-cli-harness-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); + mkdirSync(tempDir, { recursive: true }); + return Promise.resolve(new TestHarness(tempDir)); + } + + /** + * Returns the EnvironmentBuilder for this harness (lazily created, shared instance). + * Configure it before calling run(). + */ + state(): EnvironmentBuilder { + if (!this._envBuilder) { + this._envBuilder = new EnvironmentBuilder(); + } + return this._envBuilder; + } + + /** + * Creates a new FakeSonarQubeServerBuilder. Call .start() on the result to get a + * running server. The server is stopped automatically when dispose() is called. + */ + newFakeServer(): FakeSonarQubeServerBuilder & { start: () => Promise } { + const builder = new FakeSonarQubeServerBuilder(); + + // Wrap start() to register the server for cleanup + const originalStart = builder.start.bind(builder); + builder.start = async () => { + const server = await originalStart(); + this.servers.push(server); + return server; + }; + + return builder; + } + + /** + * Creates a new FakeBinariesServerBuilder. Call .start() on the result to get a + * running server. The server serves the mock sonar-secrets binary for any request + * and records all requests. It is stopped automatically when dispose() is called. + */ + newFakeBinariesServer(): FakeBinariesServerBuilder & { + start: () => Promise; + } { + const builder = new FakeBinariesServerBuilder(); + + const originalStart = builder.start.bind(builder); + builder.start = async () => { + const server = await originalStart(); + this.binariesServers.push(server); + return server; + }; + + return builder; + } + + /** + * Runs the CLI binary with the given command string. + * + * Before spawning, applies the configured environment (writes state.json + copies binary). + * Automatically injects SONAR_CLI_DISABLE_KEYCHAIN=true. + */ + async run(command: string, options?: RunOptions): Promise { + // Apply environment to tempDir before each run + if (this._envBuilder) { + await this._envBuilder.writeTo(this.cliHome.path, this.keychainJsonFile.path); + } + + // Clean environment — only include the minimum system vars needed to run a binary. + // This prevents developer-specific env vars (tokens, staging URLs, etc.) from + // leaking into the CLI process and affecting test behaviour. + const systemVars: Record = {}; + for (const key of ['PATH', 'HOME', 'TMPDIR', 'USER', 'LOGNAME', 'SHELL', 'TERM']) { + const val = process.env[key]; + if (val !== undefined) systemVars[key] = val; + } + + const homeEnv: Record = + process.platform === 'win32' + ? { USERPROFILE: this.userHome.path } + : { HOME: this.userHome.path }; + + const activeBinariesServer = this.binariesServers.at(-1); + const fakeBinariesEnv: Record = activeBinariesServer + ? { SONAR_CLI_BINARIES_URL: activeBinariesServer.baseUrl() } + : {}; + + const env: Record = { + ...systemVars, + ...fakeBinariesEnv, + SONAR_CLI_KEYCHAIN_FILE: this.keychainJsonFile.path, + CI: 'true', + ...this._extraEnv, + ...(options?.extraEnv ?? {}), + ...homeEnv, + }; + + return runCli(command, env, { + stdin: options?.stdin, + timeoutMs: options?.timeoutMs, + cwd: this.cwd.path, + browserToken: options?.browserToken, + }); + } + + /** + * Stops all fake servers and removes the temporary directory. + */ + async dispose(): Promise { + await Promise.all( + [...this.servers, ...this.binariesServers].map((s) => + s.stop().catch(() => { + /* ignore stop errors */ + }), + ), + ); + rmSync(this.tempDir.path, { recursive: true, force: true }); + } +} diff --git a/tests/integration/harness/types.ts b/tests/integration/harness/types.ts new file mode 100644 index 00000000..4362f112 --- /dev/null +++ b/tests/integration/harness/types.ts @@ -0,0 +1,49 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration test harness — shared types + +export interface CliResult { + exitCode: number; + stdout: string; + stderr: string; + durationMs: number; +} + +export interface RunOptions { + extraEnv?: Record; + timeoutMs?: number; + stdin?: string; + /** + * When set, the harness streams CLI stdout looking for the loopback OAuth + * port (pattern: `port=\d+`), then delivers this token via GET request to + * the loopback server. Use this to test interactive browser-auth flows. + */ + browserToken?: string; +} + +export interface RecordedRequest { + method: string; + url: string; + path: string; + query: Record; + headers: Record; + timestamp: number; +} diff --git a/tests/integration/onboard.test.ts b/tests/integration/onboard.test.ts deleted file mode 100644 index dbb20d6f..00000000 --- a/tests/integration/onboard.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * SonarQube CLI - * Copyright (C) 2026 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -// Integration test for onboarding flow - -import { expect, it } from 'bun:test'; - -import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; - -import { discoverProject } from '../../src/bootstrap/discovery.js'; -import { areHooksInstalled, installSecretScanningHooks } from '../../src/bootstrap/hooks.js'; -import { loadState, saveState } from '../../src/lib/state-manager'; -import { CliState, getDefaultState } from '../../src/lib/state'; - -const PROJECT_ROOT = join(import.meta.dir, '../..'); - -it('integration: full onboarding flow', async () => { - const testDir = join(tmpdir(), 'sonarqube-cli-test-integration-' + Date.now()); - mkdirSync(testDir, { recursive: true }); - - try { - const propsContent = ` -sonar.host.url=https://sonarcloud.io -sonar.projectKey=test_project -sonar.organization=test-org -`; - writeFileSync(join(testDir, 'sonar-project.properties'), propsContent); - - const projectInfo = await discoverProject(testDir); - - expect(projectInfo.hasSonarProps).toBe(true); - expect(projectInfo.sonarPropsData!.hostURL).toBe('https://sonarcloud.io'); - expect(projectInfo.sonarPropsData!.projectKey).toBe('test_project'); - expect(projectInfo.sonarPropsData!.organization).toBe('test-org'); - - const state: CliState = { - ...getDefaultState('0'), - auth: { - isAuthenticated: true, - connections: [ - { - id: 'id', - serverUrl: projectInfo.sonarPropsData!.hostURL, - type: 'cloud', - orgKey: projectInfo.sonarPropsData!.organization, - authenticatedAt: '0', - keystoreKey: 'key', - }, - ], - }, - }; - - saveState(state); - - await installSecretScanningHooks(projectInfo.root); - - // Verify hooks - const installed = await areHooksInstalled(projectInfo.root); - expect(installed).toBe(true); - - // Verify hook script exists - const hookScript = join( - projectInfo.root, - '.claude', - 'hooks', - 'sonar-secrets', - 'build-scripts', - 'pretool-secrets.sh', - ); - expect(existsSync(hookScript)).toBe(true); - - // Verify settings exists - const settingsPath = join(projectInfo.root, '.claude', 'settings.json'); - expect(existsSync(settingsPath)).toBe(true); - - console.log('✅ Full onboarding flow completed successfully'); - } finally { - rmSync(testDir, { recursive: true, force: true }); - } -}); - -it('integration: onboard with existing .sonarlint config', async () => { - const testDir = join(tmpdir(), 'sonarqube-cli-test-sonarlint-' + Date.now()); - const sonarlintDir = join(testDir, '.sonarlint'); - mkdirSync(sonarlintDir, { recursive: true }); - - try { - // Create .sonarlint/connectedMode.json - const configContent = { - sonarQubeUri: 'https://sonarqube.example.com', - projectKey: 'example_project', - organization: 'example-org', - }; - - writeFileSync(join(sonarlintDir, 'connectedMode.json'), JSON.stringify(configContent, null, 2)); - - // Discover - const projectInfo = await discoverProject(testDir); - - expect(projectInfo.hasSonarLintConfig).toBe(true); - expect(projectInfo.sonarLintData!.serverURL).toBe('https://sonarqube.example.com'); - - const state: CliState = { - ...getDefaultState('0'), - auth: { - isAuthenticated: true, - connections: [ - { - id: 'id', - serverUrl: projectInfo.sonarLintData!.serverURL, - type: 'on-premise', - authenticatedAt: '0', - keystoreKey: 'key', - }, - ], - }, - }; - - saveState(state); - - // Verify - const loaded = loadState(); - expect(loaded.auth.connections[0].serverUrl).toBe('https://sonarqube.example.com'); - - console.log('✅ Onboarding with existing .sonarlint config completed'); - } finally { - rmSync(testDir, { recursive: true, force: true }); - } -}); - -it( - 'integration: auth login process exits after token delivered to loopback server', - async () => { - // Regression test: process must exit quickly after completing the browser auth flow. - // Previously the process would hang due to open handles (loopback server, stdin stream). - // - // Strategy: - // 1. Spawn `sonar auth login` against a fake server (no cached token in keychain) - // 2. Read stdout until the loopback URL appears — extract the port - // 3. Write "\n" to stdin to pass pressAnyKeyPrompt - // 4. POST the token directly to the loopback server (simulates browser callback) - // 5. Assert the process exits within 5 seconds - // - // Exit code will be 1 (token validation against fake server fails with DNS error). - // That is expected — what matters is the process exits. - - const fakeServer = `https://sonar-exit-test-${Date.now()}.invalid`; - // Use isolated CLI dir to avoid corrupting the real ~/.sonarqube-cli/state.json - const isolatedCliDir = join(tmpdir(), `sonar-cli-test-auth-${Date.now()}`); - - const proc = Bun.spawn( - ['bun', 'run', 'src/index.ts', 'auth', 'login', '--server', fakeServer], - { - stdout: 'pipe', - stdin: 'ignore', - stderr: 'pipe', - cwd: PROJECT_ROOT, - env: { - ...process.env, - CI: 'true', - SONAR_CLI_DISABLE_KEYCHAIN: 'true', - SONAR_CLI_DIR: isolatedCliDir, - }, - }, - ); - - // Read stdout until the loopback URL appears - const reader = proc.stdout.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let port: number | undefined; - - while (!port) { - const chunk = await Promise.race([ - reader.read(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Loopback URL not found in stdout within 5s')), 5000), - ), - ]); - if (chunk.done) break; - buffer += decoder.decode(chunk.value, { stream: true }); - const match = buffer.match(/port=(\d+)/); - if (match) port = parseInt(match[1]); - } - - expect(port).toBeDefined(); - - // POST the token — simulates SonarCloud redirecting to the loopback server - // pressAnyKeyPrompt is skipped automatically when CI=true - await fetch(`http://127.0.0.1:${port}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: 'squ_integration_test_token' }), - }); - - // Process must exit within 5 seconds - const exitCode = await Promise.race([ - proc.exited, - new Promise((_, reject) => - setTimeout(() => reject(new Error('Process hung — did not exit within 5s')), 5000), - ), - ]); - - expect(typeof exitCode).toBe('number'); - - // Cleanup isolated CLI dir - rmSync(isolatedCliDir, { recursive: true, force: true }); - }, - { timeout: 15000 }, -); diff --git a/tests/integration/resources/.sonar-secrets-auth-cache b/tests/integration/resources/.sonar-secrets-auth-cache new file mode 100644 index 00000000..617f17d6 --- /dev/null +++ b/tests/integration/resources/.sonar-secrets-auth-cache @@ -0,0 +1 @@ +4bU\TZo.;D`$|{BY2ݸ=\oYZ5́@,hn \ No newline at end of file diff --git a/tests/integration/secret-check.test.ts b/tests/integration/secret-check.test.ts deleted file mode 100644 index 5bb2b0bc..00000000 --- a/tests/integration/secret-check.test.ts +++ /dev/null @@ -1,427 +0,0 @@ -/* - * SonarQube CLI - * Copyright (C) 2026 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -/** - * Integration tests for sonar secret check command - * Tests real sonar-secrets binary with stdin and file modes - * Note: This file contains hardcoded test secrets and OS commands for testing purposes only - * - * SONAR EXCLUSIONS (see pom.xml for file-level suppressions): - * - S4036: env variables spread is safe - only test environment data - * - S4721: execSync is safe - all commands and args are hardcoded for testing - * - * - * @SuppressWarnings("java:S4036") - * @SuppressWarnings("java:S4721") - */ - -import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; -import { execSync } from 'node:child_process'; -import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir, homedir, platform as osPlatform, arch as osArch } from 'node:os'; - -let BINARY_PATH: string; -const BINARY_DIR = join(homedir(), '.sonar', 'sonarqube-cli', 'bin'); - -// Test data with real secrets -const GITHUB_TOKEN = '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'; -const AWS_SECRET = 'kHeUAwnSUizTWpSbyGAz4f+As5LshPIjvtpswqGb'; -const CLEAN_TEXT = 'export API_URL="https://api.example.com"'; - -function getBinaryName(): string { - const plt = osPlatform(); - const arch = osArch(); - - if (plt === 'darwin') { - return arch === 'arm64' ? 'sonar-secrets-macos-arm64' : 'sonar-secrets-macos-x86-64'; - } else if (plt === 'linux') { - return arch === 'arm64' ? 'sonar-secrets-linux-arm64' : 'sonar-secrets-linux-x86-64'; - } else if (plt === 'win32') { - return arch === 'arm64' - ? 'sonar-secrets-windows-arm64.exe' - : 'sonar-secrets-windows-x86-64.exe'; - } - - throw new Error(`Unsupported platform: ${plt}`); -} - -function skipIfNoToken(): boolean { - const hasToken = process.env.SONAR_SECRETS_TOKEN && process.env.SONAR_SECRETS_AUTH_URL; - if (!hasToken) { - console.log('⚠️ Skipping sonar-secrets integration tests: SONAR_SECRETS_TOKEN not set'); - } - return !hasToken; -} - -/* sonar-cli: safe for integration tests with hardcoded test data */ -describe('sonar secret check - integration tests', () => { - beforeAll(() => { - const skip = skipIfNoToken(); - if (skip) { - return; - } - - // Detect binary path - const binaryName = getBinaryName(); - BINARY_PATH = join(BINARY_DIR, binaryName); - - // Check if binary exists, install if needed - if (!existsSync(BINARY_PATH)) { - console.log('📥 Installing sonar-secrets binary for integration tests...'); - try { - mkdirSync(BINARY_DIR, { recursive: true }); - // NOSONAR - S4721: Safe command in test environment - execSync('npm run build:binary', { stdio: 'inherit' }); - // NOSONAR - S4036,S4721: Safe test environment with only test vars - execSync('dist/sonar-cli secret install', { - stdio: 'inherit', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - } catch (error) { - console.error('Failed to install sonar-secrets:', error); - throw error; - } - } - - if (!existsSync(BINARY_PATH)) { - throw new Error(`sonar-secrets binary not found at ${BINARY_PATH}`); - } - - console.log(`✅ Using sonar-secrets at ${BINARY_PATH}`); - }); - - describe('stdin mode', () => { - it('should detect GitHub token in stdin', () => { - if (skipIfNoToken()) { - return; - } - - try { - const result = execSync(`echo "${GITHUB_TOKEN}" | dist/sonar-cli secret check --stdin`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - // If we get here, exit code was 0 (no error) - expect(result).toBeDefined(); - } catch (error) { - // Exit code 1 means secrets found (expected) - const err = error as { status?: number; stdout?: string }; - expect(err.status).toBe(1); - } - }); - - it('should pass clean content in stdin', () => { - if (skipIfNoToken()) { - return; - } - - try { - const result = execSync(`echo "${CLEAN_TEXT}" | dist/sonar-cli secret check --stdin`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - - // Clean content should succeed (exit 0) - expect(result).toBeDefined(); - } catch (error) { - // If error, exit code should not be 1 (secrets found) - const err = error as { status?: number }; - expect(err.status).not.toBe(1); - } - }); - - it('should handle empty stdin', () => { - if (skipIfNoToken()) { - return; - } - - try { - execSync('echo "" | dist/sonar-cli secret check --stdin', { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - // Empty input should succeed - } catch (error) { - const err = error as { status?: number }; - // Empty input is clean, so should not find secrets - expect(err.status).not.toBe(1); - } - }); - - it('should handle multiple secrets in stdin', () => { - if (skipIfNoToken()) { - return; - } - - const multiSecret = `${GITHUB_TOKEN}\n${AWS_SECRET}`; - - try { - execSync(`echo "${multiSecret}" | dist/sonar-cli secret check --stdin`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - } catch (error) { - // Multiple secrets should be detected (exit 1) - const err = error as { status?: number }; - expect(err.status).toBe(1); - } - }); - - it('should reject when both --stdin and --file are provided', () => { - if (skipIfNoToken()) { - return; - } - - try { - execSync(`echo "test" | dist/sonar-cli secret check --stdin --file /tmp/test.txt`, { - stdio: 'pipe', - }); - expect.unreachable(); - } catch (error) { - const err = error as { status?: number; stderr?: string }; - expect(err.status).not.toBe(0); - } - }); - - it('should reject when no --stdin and no --file', () => { - if (skipIfNoToken()) { - return; - } - - try { - execSync('dist/sonar-cli secret check', { - stdio: 'pipe', - }); - expect.unreachable(); - } catch (error) { - const err = error as { status?: number }; - expect(err.status).not.toBe(0); - } - }); - }); - - describe('file mode', () => { - let testFile: string; - - beforeAll(() => { - testFile = join(tmpdir(), `secret-test-${Date.now()}.env`); - }); - - afterAll(() => { - if (existsSync(testFile)) { - unlinkSync(testFile); - } - }); - - it('should detect AWS secret in file', () => { - if (skipIfNoToken()) { - return; - } - - writeFileSync(testFile, `props.set("aws-secret-access-key", "${AWS_SECRET}")`); - - try { - execSync(`dist/sonar-cli secret check --file "${testFile}"`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - } catch (error) { - // Should find secret (exit 1) - const err = error as { status?: number }; - expect(err.status).toBe(1); - } - }); - - it('should pass clean file content', () => { - if (skipIfNoToken()) { - return; - } - - writeFileSync(testFile, CLEAN_TEXT); - - try { - execSync(`dist/sonar-cli secret check --file "${testFile}"`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - // Clean file should succeed - } catch (error) { - const err = error as { status?: number }; - expect(err.status).not.toBe(1); - } - }); - - it('should detect GitHub token in file', () => { - if (skipIfNoToken()) { - return; - } - - writeFileSync(testFile, `export GH_TOKEN="${GITHUB_TOKEN}"`); - - try { - execSync(`dist/sonar-cli secret check --file "${testFile}"`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - } catch (error) { - // Should find secret - const err = error as { status?: number }; - expect(err.status).toBe(1); - } - }); - - it('should handle non-existent file gracefully', () => { - if (skipIfNoToken()) { - return; - } - - const nonExistent = join(tmpdir(), 'nonexistent-file-12345.txt'); - - try { - execSync(`dist/sonar-cli secret check --file "${nonExistent}"`, { - stdio: 'pipe', - }); - expect.unreachable(); - } catch (error) { - const err = error as { status?: number }; - expect(err.status).not.toBe(0); - } - }); - - it('should handle empty file', () => { - if (skipIfNoToken()) { - return; - } - - writeFileSync(testFile, ''); - - try { - execSync(`dist/sonar-cli secret check --file "${testFile}"`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - // Empty file is clean - } catch (error) { - const err = error as { status?: number }; - expect(err.status).not.toBe(1); - } - }); - }); - - describe('comparison: stdin vs file', () => { - let testFile: string; - - beforeAll(() => { - testFile = join(tmpdir(), `secret-compare-${Date.now()}.txt`); - }); - - afterAll(() => { - if (existsSync(testFile)) { - unlinkSync(testFile); - } - }); - - it('should produce same result for same content via stdin and file', () => { - if (skipIfNoToken()) { - return; - } - - const testContent = `API_KEY="${GITHUB_TOKEN}"`; - writeFileSync(testFile, testContent); - - let stdinExitCode = null; - let fileExitCode = null; - - // Test stdin - try { - execSync(`echo "${testContent}" | dist/sonar-cli secret check --stdin`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - stdinExitCode = 0; - } catch (error) { - const err = error as { status?: number }; - stdinExitCode = err.status; - } - - // Test file - try { - execSync(`dist/sonar-cli secret check --file "${testFile}"`, { - stdio: 'pipe', - env: { - ...process.env, - SONAR_SECRETS_TOKEN: process.env.SONAR_SECRETS_TOKEN, - SONAR_SECRETS_AUTH_URL: process.env.SONAR_SECRETS_AUTH_URL, - }, - }); - fileExitCode = 0; - } catch (error) { - const err = error as { status?: number }; - fileExitCode = err.status; - } - - // Both should detect the same secret - expect(stdinExitCode).toBe(fileExitCode); - }); - }); -}); diff --git a/tests/integration/secret-install-integration.test.ts b/tests/integration/secret-install-integration.test.ts deleted file mode 100644 index 3b9bac8e..00000000 --- a/tests/integration/secret-install-integration.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * SonarQube CLI - * Copyright (C) 2026 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -// Integration tests for secret install command - real function execution with mocked dependencies - -import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; -import { mkdirSync, existsSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { performSecretInstall } from '../../src/cli/commands/install.js'; - -const INTEGRATION_TEST_TIMEOUT_MS = 30000; - -describe('Secret Install Integration Tests', () => { - let testDir: string; - - beforeEach(() => { - testDir = join(tmpdir(), `test-secret-install-${Date.now()}`); - mkdirSync(testDir, { recursive: true }); - }); - - afterEach(() => { - if (existsSync(testDir)) { - rmSync(testDir, { recursive: true, force: true }); - } - }); - - it( - 'performSecretInstall: returns binary path string when successful', - async () => { - try { - const result = await performSecretInstall({ force: false }); - - // Should return a string path - expect(typeof result).toBe('string'); - expect(result.length).toBeGreaterThan(0); - - // Path should contain expected segments - expect(result.includes('.sonar/sonarqube-cli')).toBe(true); - expect(result.includes('bin')).toBe(true); - expect(result.includes('sonar-secrets')).toBe(true); - - // On non-Windows platforms, should not have .exe extension - const platform = process.platform; - if (platform !== 'win32') { - expect(result.endsWith('.exe')).toBe(false); - } - } catch (error) { - // Network errors acceptable (no GitHub access in test environment) - const errorMsg = (error as Error).message; - expect(errorMsg).toBeDefined(); - expect(errorMsg.length).toBeGreaterThan(0); - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); - - it( - 'performSecretInstall with force: true skips version check', - async () => { - try { - const result = await performSecretInstall({ force: true }); - - expect(typeof result).toBe('string'); - expect(result.includes('.sonar/sonarqube-cli')).toBe(true); - - // With force=true, should attempt fresh install regardless of existing version - // Force option should affect the flow (skips version check) - } catch (error) { - // Expected: no GitHub access - expect((error as Error).message).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); - - it( - 'performSecretInstall: returns same path on already-up-to-date error', - async () => { - try { - // First call (will fail due to network, but that's OK) - const firstResult = await performSecretInstall({ force: false }); - expect(typeof firstResult).toBe('string'); - expect(firstResult.includes('.sonar/sonarqube-cli')).toBe(true); - } catch (error) { - // Expected behavior: network error or already-up-to-date error both return path - if ((error as Error).message === 'Installation skipped - already up to date') { - // This is handled: returns binary path even on already-up-to-date error - const result = await performSecretInstall({ force: false }); - expect(typeof result).toBe('string'); - } else { - // Network error expected - const msg = (error as Error).message; - expect(msg.length).toBeGreaterThan(0); - } - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); - - it( - 'performSecretInstall: creates binary directory if missing', - async () => { - try { - const result = await performSecretInstall({ force: false }); - - // Even on network error, directory creation attempt was made - // Check that we got a path back - expect(result).toBeDefined(); - expect(typeof result).toBe('string'); - } catch (error) { - // Expected: GitHub API failure in test environment - expect((error as Error).message).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); - - it( - 'performSecretInstall: error handling propagates network errors', - async () => { - let errorThrown = false; - let errorMessage = ''; - - try { - await performSecretInstall({ force: false }); - } catch (error) { - errorThrown = true; - errorMessage = (error as Error).message; - } - - // Should either succeed (unlikely in test) or throw network error - if (errorThrown) { - expect(errorMessage.length).toBeGreaterThan(0); - // Network or Sonarsource-related error expected - expect( - errorMessage.includes('Failed') || - errorMessage.includes('failed') || - errorMessage.includes('fetch') || - errorMessage.includes('Connection') || - errorMessage.includes('version listing') || - errorMessage.includes('timeout') || - errorMessage.includes('aborted') || - errorMessage.includes('network') || - errorMessage.includes('unavailable'), - ).toBe(true); - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); - - it( - 'performSecretInstall: detects correct platform and architecture', - async () => { - try { - const result = await performSecretInstall({ force: false }); - - // Result should be a valid string path for the platform - expect(typeof result).toBe('string'); - expect(result.length).toBeGreaterThan(0); - } catch (error) { - // Network error expected in test environment - expect((error as Error).message).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT_MS, - ); -}); diff --git a/tests/integration/specs/analyze/analyze-secrets.test.ts b/tests/integration/specs/analyze/analyze-secrets.test.ts new file mode 100644 index 00000000..fad5da78 --- /dev/null +++ b/tests/integration/specs/analyze/analyze-secrets.test.ts @@ -0,0 +1,209 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `analyze secrets` — covers both unauthenticated and authenticated scans. +// +// Note: hardcoded token below is an intentional test fixture for the secret scanner. +// sonar-ignore-next-line S6769 + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +// Hardcoded test token — intentional fixture for secret detection, not a real credential +// sonar-ignore-next-line S6769 +const GITHUB_TEST_TOKEN = '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'; +const CLEAN_CONTENT = 'const greeting = "hello world";'; +const VALID_TOKEN = 'integration-test-token'; + +describe('analyze secrets', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 0 for clean file when binary is installed (--file)', + async () => { + harness.state().withSecretsBinaryInstalled(); + harness.cwd.writeFile('clean.js', CLEAN_CONTENT); + + const result = await harness.run(`analyze secrets --file clean.js`); + + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toContain('Scan completed successfully'); + }, + { timeout: 30000 }, + ); + + it( + 'exits with code 51 for file with secrets when binary is installed (--file)', + async () => { + harness.state().withSecretsBinaryInstalled(); + harness.cwd.writeFile('secrets.js', `const token = "${GITHUB_TEST_TOKEN}";`); + + const result = await harness.run(`analyze secrets --file secrets.js`); + + expect(result.exitCode).toBe(51); + // Binary always reports auth status when no credentials are configured + expect(result.stdout + result.stderr).toContain('Authentication was not successful'); + expect(result.stdout + result.stderr).toContain('GitHub Token'); + }, + { timeout: 30000 }, + ); + + it( + 'exits with code 0 for clean content via --stdin when binary is installed', + async () => { + harness.state().withSecretsBinaryInstalled(); + + const result = await harness.run('analyze secrets --stdin', { stdin: CLEAN_CONTENT }); + + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toContain('Scan completed successfully'); + }, + { timeout: 30000 }, + ); + + it( + 'exits with code 51 for content with secrets via --stdin when binary is installed', + async () => { + harness.state().withSecretsBinaryInstalled(); + + const result = await harness.run('analyze secrets --stdin', { + stdin: `const token = "${GITHUB_TEST_TOKEN}";`, + }); + + expect(result.exitCode).toBe(51); + // Binary always reports auth status when no credentials are configured + expect(result.stdout + result.stderr).toContain('Authentication was not successful'); + expect(result.stdout + result.stderr).toContain('GitHub Token'); + }, + { timeout: 30000 }, + ); + + it( + 'exits with code 1 and reports binary not installed when binary is absent', + async () => { + // No withSecretsBinaryInstalled() — binary absent + harness.cwd.writeFile('file.js', CLEAN_CONTENT); + + const result = await harness.run(`analyze secrets --file file.js`); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('sonar-secrets is not installed'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when neither --file nor --stdin is provided', + async () => { + harness.state().withSecretsBinaryInstalled(); + + const result = await harness.run('analyze secrets'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Either --file or --stdin is required'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 for non-existent file path', + async () => { + harness.state().withSecretsBinaryInstalled(); + + const result = await harness.run('analyze secrets --file /nonexistent/path/file.txt'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('File not found'); + }, + { timeout: 15000 }, + ); + + it( + 'forwards auth to binary when SONAR_CLI_TOKEN + SONAR_CLI_SERVER are set', + async () => { + harness.state().withSecretsBinaryInstalled(); + const server = await harness.newFakeServer().withAuthToken(VALID_TOKEN).start(); + + // Use a file with secrets so the binary outputs exit 51 and CLI forwards binary stderr. + // With valid auth the binary must NOT report "Authentication was not successful". + harness.cwd.writeFile('secrets.js', `const token = "${GITHUB_TEST_TOKEN}";`); + + const result = await harness.run(`analyze secrets --file secrets.js`, { + extraEnv: { + SONAR_CLI_TOKEN: VALID_TOKEN, + SONAR_CLI_SERVER: server.baseUrl(), + SONAR_SECRETS_ALLOW_UNSECURE_HTTP: 'true', + }, + }); + + expect(result.exitCode).toBe(51); + expect(result.stdout + result.stderr).not.toContain('Authentication was not successful'); + expect(result.stdout + result.stderr).toContain('GitHub Token'); + }, + { timeout: 30000 }, + ); + + it( + 'exits with code 1 when both --file and --stdin are provided', + async () => { + const result = await harness.run('analyze secrets --file somefile.js --stdin'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Cannot use both --file and --stdin'); + }, + { timeout: 15000 }, + ); + + it( + 'forwards auth from active connection and keychain to binary', + async () => { + const server = await harness.newFakeServer().withAuthToken(VALID_TOKEN).start(); + + harness + .state() + .withSecretsBinaryInstalled() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), VALID_TOKEN); + + // Use a file with secrets so the binary outputs exit 51 and CLI forwards binary stderr. + // With valid auth the binary must NOT report "Authentication was not successful". + harness.cwd.writeFile('secrets.js', `const token = "${GITHUB_TEST_TOKEN}";`); + + const result = await harness.run(`analyze secrets --file secrets.js`, { + extraEnv: { + SONAR_SECRETS_ALLOW_UNSECURE_HTTP: 'true', + }, + }); + + expect(result.exitCode).toBe(51); + expect(result.stdout + result.stderr).not.toContain('Authentication was not successful'); + expect(result.stdout + result.stderr).toContain('GitHub Token'); + }, + { timeout: 30000 }, + ); +}); diff --git a/tests/integration/specs/auth/auth.test.ts b/tests/integration/specs/auth/auth.test.ts new file mode 100644 index 00000000..11c9bdfa --- /dev/null +++ b/tests/integration/specs/auth/auth.test.ts @@ -0,0 +1,232 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `sonar auth login`, `auth logout`, `auth purge`, and `auth status` + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('auth login', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 when --server is not a valid URL', + async () => { + const result = await harness.run('auth login --server not-a-url --with-token mytoken'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Invalid server URL'); + }, + { timeout: 15000 }, + ); + + it( + 'saves token to keychain and state after --with-token and --server', + async () => { + const server = await harness.newFakeServer().withAuthToken('my-login-token').start(); + + const result = await harness.run( + `auth login --with-token my-login-token --server ${server.baseUrl()}`, + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Authentication successful'); + + // Verify keychain file was written with the token + expect(harness.keychainJsonFile.exists()).toBe(true); + const keychain = harness.keychainJsonFile.asJson() as { + tokens: Record; + }; + // Account key is hostname of the server (127.0.0.1) + expect(Object.values(keychain.tokens)).toContain('my-login-token'); + + // Verify state.json has a connection + const state = harness.stateJsonFile.asJson(); + expect(state.auth.connections.length).toBeGreaterThan(0); + expect(state.auth.connections[0].serverUrl).toBe(server.baseUrl()); + }, + { timeout: 15000 }, + ); +}); + +describe('auth logout', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'removes token from keychain and connection from state when logout succeeds', + async () => { + const server = await harness.newFakeServer().withAuthToken('logout-token').start(); + + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'logout-token'); + + const result = await harness.run(`auth logout --server ${server.baseUrl()}`); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Logged out'); + + // Verify token was removed from keychain + expect(harness.keychainJsonFile.exists()).toBe(true); + const keychain = harness.keychainJsonFile.asJson() as { + tokens: Record; + }; + expect(Object.values(keychain.tokens)).not.toContain('logout-token'); + }, + { timeout: 15000 }, + ); + + it( + 'exits gracefully when no token exists for the server', + async () => { + const server = await harness.newFakeServer().start(); + // No token in keychain — nothing to logout + + const result = await harness.run(`auth logout --server ${server.baseUrl()}`); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No token found'); + }, + { timeout: 15000 }, + ); +}); + +describe('auth purge', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 0 and reports no tokens when keychain is empty', + async () => { + const result = await harness.run('auth purge'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No tokens found'); + }, + { timeout: 15000 }, + ); + + it( + 'removes all tokens after confirmation', + async () => { + const server = await harness.newFakeServer().withAuthToken('purge-token-1').start(); + + const server2 = await harness.newFakeServer().withAuthToken('purge-token-2').start(); + + harness + .state() + .withKeychainToken(server.baseUrl(), 'purge-token-1') + .withKeychainToken(server2.baseUrl(), 'purge-token-2'); + + // ConfirmPrompt is not bypassed by CI=true — send 'y' via stdin + const result = await harness.run('auth purge', { stdin: 'y\n' }); + + expect(result.exitCode).toBe(0); + + // All tokens must have been removed from the keychain file + expect(harness.keychainJsonFile.exists()).toBe(true); + const keychain = harness.keychainJsonFile.asJson() as { + tokens: Record; + }; + expect(Object.keys(keychain.tokens ?? {}).length).toBe(0); + }, + { timeout: 15000 }, + ); +}); + +describe('auth status', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'reports not authenticated when no connection exists in state', + async () => { + const result = await harness.run('auth status'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No saved connection'); + }, + { timeout: 15000 }, + ); + + it( + 'reports token missing when connection exists but no keychain token', + async () => { + const server = await harness.newFakeServer().start(); + harness.state().withActiveConnection(server.baseUrl()); + // No withKeychainToken + + const result = await harness.run('auth status'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Token missing'); + }, + { timeout: 15000 }, + ); + + it( + 'reports connected when connection and token are both present', + async () => { + const server = await harness.newFakeServer().withAuthToken('status-token').start(); + + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'status-token'); + + const result = await harness.run('auth status'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Connected'); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/config/config-telemetry.test.ts b/tests/integration/specs/config/config-telemetry.test.ts new file mode 100644 index 00000000..0c2a2ac3 --- /dev/null +++ b/tests/integration/specs/config/config-telemetry.test.ts @@ -0,0 +1,80 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `config telemetry` + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('config telemetry', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 when both --enabled and --disabled are provided', + async () => { + const result = await harness.run('config telemetry --enabled --disabled'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Cannot use both --enabled and --disabled'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 0 and reports current status when no flags are provided', + async () => { + const result = await harness.run('config telemetry'); + + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toContain('Telemetry is currently'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 0 and enables telemetry when --enabled is provided', + async () => { + const result = await harness.run('config telemetry --enabled'); + + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toContain('Telemetry enabled'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 0 and disables telemetry when --disabled is provided', + async () => { + const result = await harness.run('config telemetry --disabled'); + + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toContain('Telemetry disabled'); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/install/install-secrets.test.ts b/tests/integration/specs/install/install-secrets.test.ts new file mode 100644 index 00000000..270fe1c8 --- /dev/null +++ b/tests/integration/specs/install/install-secrets.test.ts @@ -0,0 +1,108 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `sonar install secrets` — NO AUTH required + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('install secrets (download)', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with error when the binaries server returns 404', + async () => { + await harness.newFakeBinariesServer().noArtifacts().start(); + + const result = await harness.run('install secrets'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Download failed'); + expect(harness.cliHome.file('bin', 'sonar-secrets').exists()).toBe(false); + }, + { timeout: 15000 }, + ); + + it( + 'downloads and installs the binary from the mock binaries server', + async () => { + const fakeBinariesServer = await harness.newFakeBinariesServer().start(); + + const result = await harness.run('install secrets'); + + // The binary download request must have gone to the mock server, not the real one + const requests = fakeBinariesServer.getRecordedRequests(); + expect(requests).toHaveLength(1); + expect(requests[0].method).toBe('GET'); + expect(requests[0].path).toContain('/CommercialDistribution/sonar-secrets/'); + expect(requests[0].path).toContain('sonar-secrets-'); + + // The real binary is served, so signature verification passes and installation succeeds + expect(result.exitCode).toBe(0); + expect(harness.cliHome.file('bin', 'sonar-secrets').exists()).toBe(true); + }, + { timeout: 30000 }, + ); +}); + +describe('install secrets --status', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'reports not installed when sonar-secrets binary is absent', + async () => { + // No withSecretsBinaryInstalled() — binary is not present + const result = await harness.run('install secrets --status'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Not installed'); + }, + { timeout: 15000 }, + ); + + it( + 'reports installed when sonar-secrets binary is present', + async () => { + harness.state().withSecretsBinaryInstalled(); + + const result = await harness.run('install secrets --status'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Installed'); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/integrate/integrate.test.ts b/tests/integration/specs/integrate/integrate.test.ts new file mode 100644 index 00000000..5ba5dfe8 --- /dev/null +++ b/tests/integration/specs/integrate/integrate.test.ts @@ -0,0 +1,618 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `sonar integrate claude` + +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { isAbsolute } from 'node:path'; +import { TestHarness } from '../../harness'; + +describe('integrate claude', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + // --- Without --non-interactive (auth succeeds, no repair triggered) --- + + it( + 'performs full integration with valid --token and URL from sonar-project.properties', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --token test-token --non-interactive'); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'uses SONAR_CLI_TOKEN + SONAR_CLI_SERVER env vars for full integration', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('env-token') + .withProject('env-project') + .start(); + + // sonar-project.properties has only the project key — no sonar.host.url, + // so the server URL must come exclusively from SONAR_CLI_SERVER env var + harness.cwd.writeFile('sonar-project.properties', 'sonar.projectKey=env-project'); + + const result = await harness.run('integrate claude --non-interactive', { + extraEnv: { + SONAR_CLI_TOKEN: 'env-token', + SONAR_CLI_SERVER: server.baseUrl(), + }, + }); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'uses keychain token for full integration when no --token flag is provided', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('keychain-token') + .withProject('keychain-project') + .start(); + harness.state().withKeychainToken(server.baseUrl(), 'keychain-token'); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=keychain-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --non-interactive'); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'installs secrets-only hooks when sonar-project.properties has URL but no project key', + async () => { + const server = await harness.newFakeServer().start(); + harness.cwd.writeFile('sonar-project.properties', `sonar.host.url=${server.baseUrl()}`); + + const result = await harness.run('integrate claude --non-interactive'); + + expect(result.exitCode).toBe(0); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + // --- Without --non-interactive (interactive browser auth via browserToken) --- + + it( + 'performs full integration via browser auth when no token is initially available', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('browser-token') + .withProject('browser-project') + .start(); + + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=browser-project'].join('\n'), + ); + + const result = await harness.run('integrate claude', { + browserToken: 'browser-token', + }); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'replaces invalid token via browser auth and completes full integration', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('valid-browser-token') + .withProject('repair-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=repair-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --token invalid-token', { + browserToken: 'valid-browser-token', + }); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + }, + { timeout: 30000 }, + ); + + // --- With --non-interactive --- + + it( + 'installs hooks even when token is invalid (--non-interactive degraded mode)', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('valid-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --token wrong-token --non-interactive'); + + expect(result.exitCode).toBe(0); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'installs hooks when no token and --non-interactive', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('some-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --non-interactive'); + + expect(result.exitCode).toBe(0); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'does not open browser when env vars are set but token is invalid (env vars imply non-interactive)', + async () => { + // Regression test: when SONAR_CLI_TOKEN + SONAR_CLI_SERVER are set but the token is + // rejected by the server, the command must NOT open a browser — env vars imply CI/automated + // context. Without the fix this test hangs (browser auth is triggered, loopback server waits). + const server = await harness + .newFakeServer() + .withAuthToken('valid-token') // server only accepts 'valid-token' + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run( + 'integrate claude', // no --non-interactive flag + { + extraEnv: { + SONAR_CLI_TOKEN: 'invalid-token', // rejected by server → tokenValid = false + SONAR_CLI_SERVER: server.baseUrl(), + // no browserToken: if browser auth is triggered the test times out + }, + }, + ); + + expect(result.exitCode).toBe(0); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 15000 }, + ); + + it( + 'warns about missing SONAR_CLI_SERVER when only SONAR_CLI_TOKEN is set', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('some-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --non-interactive', { + extraEnv: { SONAR_CLI_TOKEN: 'some-token' }, + }); + + expect(result.exitCode).toBe(0); + // warn() outputs to stderr + expect(result.stderr).toContain('SONAR_CLI_SERVER'); + }, + { timeout: 30000 }, + ); + + it( + 'uses --server flag URL and overrides sonar-project.properties URL', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project') + .start(); + + const result = await harness.run( + `integrate claude --token test-token --server ${server.baseUrl()} --non-interactive`, + ); + + expect(result.exitCode).toBe(0); + const requests = server.getRecordedRequests(); + expect(requests.length).toBeGreaterThan(0); + }, + { timeout: 30000 }, + ); + + it( + 'performs full integration using --token, --server, and --project flags without sonar-project.properties', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('flag-token') + .withProject('flag-project') + .start(); + + const result = await harness.run( + `integrate claude --token flag-token --server ${server.baseUrl()} --project flag-project --non-interactive`, + ); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'installs settings.json with PreToolUse hook on full integration', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + const result = await harness.run('integrate claude --token test-token --non-interactive'); + + expect(result.exitCode).toBe(0); + const claudeSettingsFile = harness.cwd.file('.claude', 'settings.json'); + expect(claudeSettingsFile.exists()).toBe(true); + const settings = claudeSettingsFile.asJson(); + expect(settings.hooks?.PreToolUse).toBeDefined(); + }, + { timeout: 30000 }, + ); + + it( + 'pretool-secrets.sh exists and is executable after integration', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'), + ); + + await harness.run('integrate claude --token test-token --non-interactive'); + + const preToolScriptFile = harness.cwd.file( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ); + expect(preToolScriptFile.exists()).toBe(true); + expect(preToolScriptFile.isExecutable).toBe(true); + }, + { timeout: 30000 }, + ); +}); + +// ─── Local vs Global file placement ────────────────────────────────────────── + +describe('integrate claude — file placement (local vs global)', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + // ─── Project-level (no -g) ───────────────────────────────────────────────── + + describe('project-level hooks (no -g flag)', () => { + it( + 'writes hook scripts and settings.json inside projectDir/.claude/', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=proj'].join('\n'), + ); + + const result = await harness.run('integrate claude --token tok --non-interactive'); + + expect(result.exitCode).toBe(0); + expect(harness.cwd.exists('.claude', 'settings.json')).toBe(true); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + expect( + harness.cwd.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'prompt-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'does not touch the global dir when running without -g', + async () => { + await harness.run('integrate claude --non-interactive'); + + // Global dir must be completely untouched + expect(harness.userHome.exists('.claude')).toBe(false); + }, + { timeout: 30000 }, + ); + + it( + 'registers hook commands with relative paths in settings.json', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=proj'].join('\n'), + ); + + await harness.run('integrate claude --token tok --non-interactive'); + + const settings = harness.cwd.file('.claude', 'settings.json').asJson(); + const preToolCmd = settings.hooks.PreToolUse[0].hooks[0].command as string; + const promptCmd = settings.hooks.UserPromptSubmit[0].hooks[0].command as string; + + // Must be relative (not absolute) so they resolve from the project root + expect(isAbsolute(preToolCmd)).toBe(false); + expect(preToolCmd.startsWith('.claude')).toBe(true); + expect(isAbsolute(promptCmd)).toBe(false); + expect(promptCmd.startsWith('.claude')).toBe(true); + }, + { timeout: 30000 }, + ); + }); + + // ─── Global (-g flag) ────────────────────────────────────────────────────── + + describe('global hooks (-g flag)', () => { + it( + 'writes hook scripts and settings.json to $HOME/.claude/', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=proj'].join('\n'), + ); + + const result = await harness.run('integrate claude -g --token tok --non-interactive'); + + expect(result.exitCode).toBe(0); + expect(harness.userHome.exists('.claude', 'settings.json')).toBe(true); + expect( + harness.userHome.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'pretool-secrets.sh', + ), + ).toBe(true); + expect( + harness.userHome.exists( + '.claude', + 'hooks', + 'sonar-secrets', + 'build-scripts', + 'prompt-secrets.sh', + ), + ).toBe(true); + }, + { timeout: 30000 }, + ); + + it( + 'does not create .claude/ inside the project directory when -g is set', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj') + .start(); + + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=proj'].join('\n'), + ); + + await harness.run('integrate claude -g --token tok --non-interactive'); + + // Project-level .claude/ must NOT be created + expect(harness.cwd.exists('.claude')).toBe(false); + }, + { timeout: 30000 }, + ); + + it( + 'registers hook commands with absolute paths pointing to $HOME', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj') + .start(); + harness.cwd.writeFile( + 'sonar-project.properties', + [`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=proj'].join('\n'), + ); + + await harness.run('integrate claude -g --non-interactive'); + + const settings = harness.userHome.file('.claude', 'settings.json').asJson(); + const preToolCmd = settings.hooks.PreToolUse[0].hooks[0].command as string; + const promptCmd = settings.hooks.UserPromptSubmit[0].hooks[0].command as string; + + // Must be absolute paths rooted at harness.homeDir + expect(isAbsolute(preToolCmd)).toBe(true); + expect(preToolCmd.startsWith(harness.userHome.path)).toBe(true); + expect(isAbsolute(promptCmd)).toBe(true); + expect(promptCmd.startsWith(harness.userHome.path)).toBe(true); + }, + { timeout: 30000 }, + ); + }); +}); + +// ─── Argument validation ────────────────────────────────────────────────────── + +describe('integrate — argument validation', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 when an unsupported tool argument is provided', + async () => { + const result = await harness.run('integrate gemini'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain("error: unknown command 'gemini'"); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/list/list-issues-auth.test.ts b/tests/integration/specs/list/list-issues-auth.test.ts new file mode 100644 index 00000000..097c8a25 --- /dev/null +++ b/tests/integration/specs/list/list-issues-auth.test.ts @@ -0,0 +1,78 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `list issues` auth scenarios +// Complements list-issues.test.ts which covers happy-path and basic error cases + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('list issues — auth scenarios', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 and reports missing server when no auth is configured', + async () => { + const result = await harness.run('list issues --project my-project'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('No server URL found'); + }, + { timeout: 15000 }, + ); + + it( + 'uses keychain token from active state connection', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('keychain-token') + .withProject('state-project', (p) => + p.withIssue({ + ruleKey: 'java:S100', + message: 'Issue from keychain auth', + severity: 'MINOR', + }), + ) + .start(); + + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'keychain-token'); + + const result = await harness.run('list issues --project state-project'); + + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.issues).toHaveLength(1); + expect(parsed.issues[0].message).toBe('Issue from keychain auth'); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/list/list-issues.test.ts b/tests/integration/specs/list/list-issues.test.ts new file mode 100644 index 00000000..a08c6886 --- /dev/null +++ b/tests/integration/specs/list/list-issues.test.ts @@ -0,0 +1,276 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `list issues` via the compiled binary + fake SonarQube server + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('list issues', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'returns issues from fake server', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project', (p) => + p + .withIssue({ ruleKey: 'java:S1234', message: 'Fix this', severity: 'MAJOR' }) + .withIssue({ ruleKey: 'java:S5678', message: 'Another issue', severity: 'CRITICAL' }), + ) + .start(); + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'test-token'); + + const result = await harness.run(`list issues --project my-project`); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('java:S1234'); + expect(result.stdout).toContain('java:S5678'); + expect(result.stdout).toContain('Fix this'); + }, + { timeout: 15000 }, + ); + + it( + 'returns empty issues list when project has no issues', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('empty-project') + .start(); + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'test-token'); + + const result = await harness.run(`list issues --project empty-project`); + + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.issues).toHaveLength(0); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when token is invalid', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('valid-token') + .withProject('my-project') + .start(); + + const result = await harness.run( + `list issues --project my-project --server ${server.baseUrl()} --token wrong-token`, + ); + + expect(result.exitCode).toBe(1); + }, + { timeout: 15000 }, + ); + + it( + 'passes severity filter to API when --severity flag is provided', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('test-token') + .withProject('my-project', (p) => + p + .withIssue({ ruleKey: 'java:S1234', message: 'Major issue', severity: 'MAJOR' }) + .withIssue({ ruleKey: 'java:S9999', message: 'Blocker issue', severity: 'BLOCKER' }), + ) + .start(); + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'test-token'); + + const result = await harness.run(`list issues --project my-project --severity BLOCKER`); + + expect(result.exitCode).toBe(0); + const recorded = server.getRecordedRequests(); + const issuesReq = recorded.find((r) => r.path === '/api/issues/search'); + expect(issuesReq?.query.severities).toBe('BLOCKER'); + }, + { timeout: 15000 }, + ); + + it( + 'records the HTTP request sent to the fake server', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('my-token') + .withProject('test-project') + .start(); + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'my-token'); + + await harness.run(`list issues --project test-project`); + + const recorded = server.getRecordedRequests(); + const issuesRequest = recorded.find((r) => r.path === '/api/issues/search'); + + expect(issuesRequest).toBeDefined(); + expect(issuesRequest!.query.projects).toBe('test-project'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --project is missing', + async () => { + const result = await harness.run( + 'list issues --server http://localhost:9999 --token test-token', + ); + + expect(result.exitCode).toBe(1); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when server is unreachable', + async () => { + const result = await harness.run( + 'list issues --project my-project --server http://127.0.0.1:19999 --token test-token', + { timeoutMs: 10000 }, + ); + + expect(result.exitCode).toBe(1); + }, + { timeout: 15000 }, + ); + + it( + 'outputs valid JSON with issues array', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('tok') + .withProject('proj', (p) => + p.withIssue({ ruleKey: 'ts:S1000', message: 'TypeScript issue', severity: 'MINOR' }), + ) + .start(); + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'tok'); + + const result = await harness.run(`list issues --project proj`); + + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed.issues)).toBe(true); + expect(parsed.issues[0].rule).toBe('ts:S1000'); + expect(parsed.issues[0].message).toBe('TypeScript issue'); + }, + { timeout: 15000 }, + ); +}); + +describe('list issues — argument validation', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 when --page-size is not a number', + async () => { + const result = await harness.run('list issues --project my-project --page-size abc'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain( + "error: option '--page-size ' argument 'abc' is invalid. Not a number.", + ); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --page-size is less than 1', + async () => { + const result = await harness.run('list issues --project my-project --page-size 0'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain( + "Invalid --page-size option: '0'. Must be an integer between 1 and 500", + ); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --page-size is greater than 500', + async () => { + const result = await harness.run('list issues --project my-project --page-size 501'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Invalid --page-size'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --format is not a recognised value', + async () => { + const result = await harness.run('list issues --project my-project --format xml'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Invalid format'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --severity is not a recognised value', + async () => { + const result = await harness.run('list issues --project my-project --severity UNKNOWN'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('Invalid severity'); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/integration/specs/list/list-projects.test.ts b/tests/integration/specs/list/list-projects.test.ts new file mode 100644 index 00000000..70e3d1cb --- /dev/null +++ b/tests/integration/specs/list/list-projects.test.ts @@ -0,0 +1,155 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Integration tests for `list projects` — requires state connection + keychain token +// +// Note: `list projects` reads auth directly from state+keychain (not `resolveAuth`), +// so env vars SONAR_CLI_TOKEN/SONAR_CLI_SERVER are NOT used here. + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { TestHarness } from '../../harness'; + +describe('list projects', () => { + let harness: TestHarness; + + beforeEach(async () => { + harness = await TestHarness.create(); + }); + + afterEach(async () => { + await harness.dispose(); + }); + + it( + 'exits with code 1 and reports missing connection when no state exists', + async () => { + const result = await harness.run('list projects'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('No server URL found'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 and reports missing token when connection exists but no keychain token', + async () => { + const server = await harness.newFakeServer().withAuthToken('some-token').start(); + + harness.state().withActiveConnection(server.baseUrl()); + // No withKeychainToken — token absent from keychain + + const result = await harness.run('list projects'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('No authentication token found'); + }, + { timeout: 15000 }, + ); + + it( + 'returns JSON with projects array when connection and token are valid', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('valid-token') + .withProject('proj-a') + .withProject('proj-b') + .start(); + + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'valid-token'); + + const result = await harness.run('list projects'); + + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed.projects)).toBe(true); + expect(parsed.projects.length).toBeGreaterThanOrEqual(2); + const keys = parsed.projects.map((p: { key: string }) => p.key); + expect(keys).toContain('proj-a'); + expect(keys).toContain('proj-b'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when keychain token is invalid (401)', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken('correct-token') + .withProject('some-project') + .start(); + + harness + .state() + .withActiveConnection(server.baseUrl()) + .withKeychainToken(server.baseUrl(), 'wrong-token'); + + const result = await harness.run('list projects'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('401'); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --page-size is not a number', + async () => { + const result = await harness.run('list projects --page-size abc'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain( + "error: option \'--page-size \' argument \'abc\' is invalid. Not a number.", + ); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --page-size is 0', + async () => { + const result = await harness.run('list projects --page-size 0'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain( + "Invalid --page-size option: '0'. Must be an integer between 1 and 500", + ); + }, + { timeout: 15000 }, + ); + + it( + 'exits with code 1 when --page-size exceeds 500', + async () => { + const result = await harness.run('list projects --page-size 501'); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain( + "Invalid --page-size option: '501'. Must be an integer between 1 and 500", + ); + }, + { timeout: 15000 }, + ); +}); diff --git a/tests/unit/auth-commands.test.ts b/tests/unit/auth-commands.test.ts index e66b0fea..0bd3194e 100644 --- a/tests/unit/auth-commands.test.ts +++ b/tests/unit/auth-commands.test.ts @@ -27,7 +27,7 @@ import { authLogin, authLogout, authPurge, authStatus } from '../../src/cli/comm import { SonarQubeClient } from '../../src/sonarqube/client.js'; import * as discovery from '../../src/bootstrap/discovery.js'; import { setMockUi, getMockUiCalls, clearMockUiCalls } from '../../src/ui'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; import * as stateManager from '../../src/lib/state-manager.js'; import { getDefaultState } from '../../src/lib/state.js'; diff --git a/tests/unit/auth-resolver.test.ts b/tests/unit/auth-resolver.test.ts index bf19fabb..bca055c6 100644 --- a/tests/unit/auth-resolver.test.ts +++ b/tests/unit/auth-resolver.test.ts @@ -25,7 +25,7 @@ import { resolveAuth, ENV_TOKEN, ENV_SERVER } from '../../src/lib/auth-resolver. import * as stateManager from '../../src/lib/state-manager.js'; import { getDefaultState } from '../../src/lib/state.js'; import { setMockUi } from '../../src/ui/index.js'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; const SONARCLOUD_URL = 'https://sonarcloud.io'; const SONARQUBE_URL = 'https://sonarqube.example.com'; diff --git a/tests/unit/auth-scenarios.test.ts b/tests/unit/auth-scenarios.test.ts index 1ecfd592..9a33668a 100644 --- a/tests/unit/auth-scenarios.test.ts +++ b/tests/unit/auth-scenarios.test.ts @@ -41,7 +41,7 @@ import { } from '../../src/bootstrap/auth.js'; import { SonarQubeClient } from '../../src/sonarqube/client.js'; import { clearTokenCache } from '../../src/lib/keychain.js'; -import { setKeytarImpl } from '../helpers/mock-keytar.js'; +import { setKeytarImpl } from './helpers/mock-keytar.js'; import { setMockUi } from '../../src/ui/index.js'; const HTTP_SCHEME = 'http'; diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts index 8d44f46f..64e22d0c 100644 --- a/tests/unit/auth.test.ts +++ b/tests/unit/auth.test.ts @@ -28,7 +28,7 @@ import { getAllCredentials, purgeAllTokens, } from '../../src/lib/keychain.js'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; const TOKEN_COUNT = 3; diff --git a/tests/helpers/mock-keytar.ts b/tests/unit/helpers/mock-keytar.ts similarity index 98% rename from tests/helpers/mock-keytar.ts rename to tests/unit/helpers/mock-keytar.ts index b2ad4d64..e4938e64 100644 --- a/tests/helpers/mock-keytar.ts +++ b/tests/unit/helpers/mock-keytar.ts @@ -21,7 +21,7 @@ // Shared mock keytar for unit tests import { mock } from 'bun:test'; -import { clearTokenCache } from '../../src/lib/keychain.js'; +import { clearTokenCache } from '../../../src/lib/keychain'; export interface MockKeytarImpl { getPassword(service: string, account: string): Promise; diff --git a/tests/unit/integrate.test.ts b/tests/unit/integrate.test.ts index f013d00e..553bce88 100644 --- a/tests/unit/integrate.test.ts +++ b/tests/unit/integrate.test.ts @@ -113,7 +113,6 @@ describe('integrateCommand: env var auth', () => { project: 'my-project', token: 'squ_cli_token', org: 'my-org', - skipHooks: true, }); const warns = getMockUiCalls() .filter((c) => c.method === 'warn') @@ -128,7 +127,6 @@ describe('integrateCommand: env var auth', () => { project: 'my-project', token: 'squ_cli_token', org: 'my-org', - skipHooks: true, }); const warns = getMockUiCalls() .filter((c) => c.method === 'warn') @@ -166,7 +164,6 @@ describe('integrateCommand: full flow', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: true, }); } finally { discoverSpy.mockRestore(); @@ -174,6 +171,33 @@ describe('integrateCommand: full flow', () => { } }); + it('saves active connection to state after successful integration', async () => { + const discoverSpy = spyOn(discovery, 'discoverProject').mockResolvedValue(FAKE_PROJECT_INFO); + const healthSpy = spyOn(health, 'runHealthChecks').mockResolvedValue(CLEAN_HEALTH); + const capturedState = getDefaultState('test'); + loadStateSpy.mockReturnValue(capturedState); + saveStateSpy.mockImplementation(() => {}); + + try { + await integrate('claude', { + server: 'https://sonarcloud.io', + project: 'my-project', + token: 'test-token', + org: 'test-org', + }); + + // Verify connection was added to state + expect(capturedState.auth.activeConnectionId).toBeDefined(); + expect(capturedState.auth.connections).toHaveLength(1); + expect(capturedState.auth.connections[0].serverUrl).toBe('https://sonarcloud.io'); + expect(capturedState.auth.connections[0].orgKey).toBe('test-org'); + expect(capturedState.auth.isAuthenticated).toBe(true); + } finally { + discoverSpy.mockRestore(); + healthSpy.mockRestore(); + } + }); + it('shows verification results after successful health check', async () => { const discoverSpy = spyOn(discovery, 'discoverProject').mockResolvedValue(FAKE_PROJECT_INFO); const healthSpy = spyOn(health, 'runHealthChecks').mockResolvedValue(CLEAN_HEALTH); @@ -184,7 +208,6 @@ describe('integrateCommand: full flow', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: true, }); const texts = getMockUiCalls() .filter((c) => c.method === 'text') @@ -216,7 +239,6 @@ describe('integrateCommand: full flow', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: true, }); const texts = getMockUiCalls() .filter((c) => c.method === 'text') @@ -246,7 +268,6 @@ describe('integrateCommand: full flow', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: true, }); expect(repairSpy).toHaveBeenCalled(); } finally { @@ -267,7 +288,6 @@ describe('integrateCommand: full flow', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: false, }); expect(addInstalledHookSpy).toHaveBeenCalledWith( expect.anything(), @@ -350,7 +370,6 @@ describe('integrateCommand: configuration validation', () => { project: 'my-project', org: 'my-org', token: 'test-token', - skipHooks: true, }); const infos = getMockUiCalls() .filter((c) => c.method === 'info') @@ -395,7 +414,7 @@ describe('integrateCommand: discovered project configuration', () => { const discoverSpy = spyOn(discovery, 'discoverProject').mockResolvedValue(projectInfoWithProps); const healthSpy = spyOn(health, 'runHealthChecks').mockResolvedValue(CLEAN_HEALTH); try { - await integrate('claude', { token: 'test-token', skipHooks: true }); + await integrate('claude', { token: 'test-token' }); const texts = getMockUiCalls() .filter((c) => c.method === 'text') .map((c) => String(c.args[0])); @@ -421,7 +440,7 @@ describe('integrateCommand: discovered project configuration', () => { ); const healthSpy = spyOn(health, 'runHealthChecks').mockResolvedValue(CLEAN_HEALTH); try { - await integrate('claude', { token: 'test-token', skipHooks: true }); + await integrate('claude', { token: 'test-token' }); const texts = getMockUiCalls() .filter((c) => c.method === 'text') .map((c) => String(c.args[0])); @@ -462,7 +481,6 @@ describe('integrateCommand: --global flag', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: true, global: true, }); } finally { @@ -481,7 +499,6 @@ describe('integrateCommand: --global flag', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: false, global: true, }); } finally { @@ -501,7 +518,6 @@ describe('integrateCommand: --global flag', () => { project: 'my-project', token: 'test-token', org: 'test-org', - skipHooks: false, global: true, }); expect(hooksSpy).toHaveBeenCalledWith(FAKE_PROJECT_INFO.root, homedir()); @@ -574,7 +590,6 @@ describe('integrateCommand: no token available', () => { integrate('claude', { server: 'https://sonarcloud.io', project: 'my-project', - skipHooks: true, }), ).rejects.toThrow(new Error('repair failed')); const warns = getMockUiCalls() diff --git a/tests/unit/keychain-caching.test.ts b/tests/unit/keychain-caching.test.ts index fb8267e9..55407769 100644 --- a/tests/unit/keychain-caching.test.ts +++ b/tests/unit/keychain-caching.test.ts @@ -25,7 +25,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { clearTokenCache, getToken } from '../../src/lib/keychain.js'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; // Test constants const MULTIPLE_TOKENS_COUNT = 3; diff --git a/tests/unit/keychain-file.test.ts b/tests/unit/keychain-file.test.ts new file mode 100644 index 00000000..01f1eb5f --- /dev/null +++ b/tests/unit/keychain-file.test.ts @@ -0,0 +1,296 @@ +/* + * SonarQube CLI + * Copyright (C) 2026 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Unit tests for file-based keychain (SONAR_CLI_KEYCHAIN_FILE). + * Exercises createFileKeytar, generateKeychainAccount, and all public API + * functions through the real implementation — no keytar mocking needed. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { + clearTokenCache, + deleteToken, + getAllCredentials, + getToken, + purgeAllTokens, + saveToken, +} from '../../src/lib/keychain.js'; + +describe('File-based keychain', () => { + let testDir: string; + let keychainFile: string; + let savedKeychainFile: string | undefined; + let savedDisableKeychain: string | undefined; + + beforeEach(() => { + testDir = join(tmpdir(), `keychain-file-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + keychainFile = join(testDir, 'keychain.json'); + + savedKeychainFile = process.env.SONAR_CLI_KEYCHAIN_FILE; + savedDisableKeychain = process.env.SONAR_CLI_DISABLE_KEYCHAIN; + + process.env.SONAR_CLI_KEYCHAIN_FILE = keychainFile; + delete process.env.SONAR_CLI_DISABLE_KEYCHAIN; + + clearTokenCache(); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + + if (savedKeychainFile === undefined) { + delete process.env.SONAR_CLI_KEYCHAIN_FILE; + } else { + process.env.SONAR_CLI_KEYCHAIN_FILE = savedKeychainFile; + } + + if (savedDisableKeychain === undefined) { + delete process.env.SONAR_CLI_DISABLE_KEYCHAIN; + } else { + process.env.SONAR_CLI_DISABLE_KEYCHAIN = savedDisableKeychain; + } + + clearTokenCache(); + }); + + describe('saveToken and getToken', () => { + it('should persist token to file and retrieve it after cache clear', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'squ_test_token', 'my-org'); + + // Act — clear cache to force file read + clearTokenCache(); + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert + expect(result).toBe('squ_test_token'); + }); + + it('should return cached value on second call without re-reading file', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'original-token', 'my-org'); + + // Corrupt the file — cache should be used and return original value + writeFileSync(keychainFile, '{ invalid json }'); + + // Act + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert — original value from cache, not corrupt file + expect(result).toBe('original-token'); + }); + + it('should return null when token is not in file or cache', async () => { + // Act — no file written yet, missing file handled by readStore catch + const result = await getToken('https://sonarcloud.io', 'nonexistent-org'); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null when keychain file is corrupt (readStore error branch)', async () => { + // Arrange + writeFileSync(keychainFile, '{ not valid json }'); + + // Act + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert + expect(result).toBeNull(); + }); + + it('should overwrite token when saved again for same account', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token-v1', 'my-org'); + await saveToken('https://sonarcloud.io', 'token-v2', 'my-org'); + + // Act — clear cache to read from file + clearTokenCache(); + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert + expect(result).toBe('token-v2'); + }); + }); + + describe('deleteToken', () => { + it('should remove token from file and return null after cache clear', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token123', 'my-org'); + + // Act + await deleteToken('https://sonarcloud.io', 'my-org'); + clearTokenCache(); + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert + expect(result).toBeNull(); + }); + + it('should not affect other tokens when deleting one', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token1', 'org1'); + await saveToken('https://sonarcloud.io', 'token2', 'org2'); + + // Act + await deleteToken('https://sonarcloud.io', 'org1'); + clearTokenCache(); + + // Assert + expect(await getToken('https://sonarcloud.io', 'org1')).toBeNull(); + expect(await getToken('https://sonarcloud.io', 'org2')).toBe('token2'); + }); + + it('should silently succeed when deleting non-existent token', async () => { + // Act & Assert — deletePassword false branch, must not throw + const result = await deleteToken('https://sonarcloud.io', 'ghost-org'); + expect(result).toBeUndefined(); + }); + }); + + describe('getAllCredentials', () => { + it('should return empty array when keychain file is missing', async () => { + // Act + const credentials = await getAllCredentials(); + + // Assert + expect(credentials).toEqual([]); + }); + + it('should return all saved credentials', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token1', 'org1'); + await saveToken('https://sonarcloud.io', 'token2', 'org2'); + await saveToken('https://sonarqube.example.com', 'token3'); + + // Act + const credentials = await getAllCredentials(); + + // Assert + expect(credentials).toHaveLength(3); + const accounts = credentials.map((c) => c.account); + expect(accounts).toContain('sonarcloud.io:org1'); + expect(accounts).toContain('sonarcloud.io:org2'); + expect(accounts).toContain('sonarqube.example.com'); + }); + }); + + describe('purgeAllTokens', () => { + it('should delete all tokens and leave keychain empty', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token1', 'org1'); + await saveToken('https://sonarcloud.io', 'token2', 'org2'); + + // Act + await purgeAllTokens(); + + // Assert — verify both cache and file are cleared + expect(await getToken('https://sonarcloud.io', 'org1')).toBeNull(); + expect(await getToken('https://sonarcloud.io', 'org2')).toBeNull(); + expect(await getAllCredentials()).toHaveLength(0); + }); + + it('should succeed when there are no tokens', async () => { + // Act & Assert + const result = await purgeAllTokens(); + expect(result).toBeUndefined(); + expect(await getAllCredentials()).toHaveLength(0); + }); + }); + + describe('clearTokenCache', () => { + it('should force re-read from file on next getToken call', async () => { + // Arrange — save initial token + await saveToken('https://sonarcloud.io', 'old-token', 'my-org'); + + // Manually update the file to simulate an external change + const store = { tokens: { 'sonarcloud.io:my-org': 'new-token' } }; + writeFileSync(keychainFile, JSON.stringify(store)); + + // Act — clear cache to pick up file change + clearTokenCache(); + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert + expect(result).toBe('new-token'); + }); + }); + + describe('account key generation (generateKeychainAccount)', () => { + it('should use hostname:org format for SonarCloud with org', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token1', 'my-org'); + clearTokenCache(); + + // Act + const result = await getToken('https://sonarcloud.io', 'my-org'); + + // Assert — account stored as "sonarcloud.io:my-org" + expect(result).toBe('token1'); + const credentials = await getAllCredentials(); + expect(credentials[0].account).toBe('sonarcloud.io:my-org'); + }); + + it('should use hostname only for SonarQube without org', async () => { + // Arrange + await saveToken('https://sonarqube.example.com', 'token1'); + clearTokenCache(); + + // Act + const result = await getToken('https://sonarqube.example.com'); + + // Assert — account stored as hostname only + expect(result).toBe('token1'); + const credentials = await getAllCredentials(); + expect(credentials[0].account).toBe('sonarqube.example.com'); + }); + + it('should use raw string as key when URL parsing fails', async () => { + // Arrange — invalid URL triggers catch branch in generateKeychainAccount + const invalidUrl = 'not-a-valid-url'; + await saveToken(invalidUrl, 'token1'); + clearTokenCache(); + + // Act + const result = await getToken(invalidUrl); + + // Assert — raw URL used as account key + expect(result).toBe('token1'); + const credentials = await getAllCredentials(); + expect(credentials[0].account).toBe(invalidUrl); + }); + + it('should isolate tokens by org on the same server', async () => { + // Arrange + await saveToken('https://sonarcloud.io', 'token-org1', 'org1'); + await saveToken('https://sonarcloud.io', 'token-org2', 'org2'); + clearTokenCache(); + + // Act & Assert — different orgs → different accounts → different tokens + expect(await getToken('https://sonarcloud.io', 'org1')).toBe('token-org1'); + expect(await getToken('https://sonarcloud.io', 'org2')).toBe('token-org2'); + }); + }); +}); diff --git a/tests/unit/secret-scan.test.ts b/tests/unit/secret-scan.test.ts index 801119ad..82cbb281 100644 --- a/tests/unit/secret-scan.test.ts +++ b/tests/unit/secret-scan.test.ts @@ -30,7 +30,7 @@ import * as processLib from '../../src/lib/process.js'; import * as stateManager from '../../src/lib/state-manager.js'; import { getDefaultState } from '../../src/lib/state.js'; import { saveToken } from '../../src/lib/keychain.js'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; import { analyzeSecrets } from '../../src/cli/commands/analyze.js'; import { CommandFailedError } from '../../src/cli/commands/common/error.js'; diff --git a/tests/unit/secret.test.ts b/tests/unit/secret.test.ts index da68d30f..61ad3e32 100644 --- a/tests/unit/secret.test.ts +++ b/tests/unit/secret.test.ts @@ -36,7 +36,7 @@ import * as processLib from '../../src/lib/process.js'; import * as stateManager from '../../src/lib/state-manager.js'; import { getDefaultState } from '../../src/lib/state.js'; import { saveToken } from '../../src/lib/keychain.js'; -import { createMockKeytar } from '../helpers/mock-keytar.js'; +import { createMockKeytar } from './helpers/mock-keytar.js'; import type { PlatformInfo } from '../../src/lib/install-types.js'; import { analyzeSecrets } from '../../src/cli/commands/analyze';