From 47175a0aa73c06e883434463c861119acc635847 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 20:54:35 -0400 Subject: [PATCH 01/90] unity-cli@v1.0.0 - Hello World! --- .github/CODEOWNERS | 1 + README.md | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c58980f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @StephenHodgson \ No newline at end of file diff --git a/README.md b/README.md index b3f2f59..9cedfd3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # unity-cli -A command line utility for the Unity Game Engine + +A command line utility for the Unity Game Engine. This tool is primarily designed to help automate common tasks in Unity projects, such as building, testing, and managing assets in CI/CD devOps pipelines. + +## Installation + +```bash +npm install -g unity-cli +``` + +## Usage + +```bash +unity-cli [command] [options] +``` From 603e5dc7f7ec47a0afcc58da443eedcc35831a60 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 21:39:04 -0400 Subject: [PATCH 02/90] setup basic cli scaffolding and unit tests --- .npmignore | 9 + README.md | 6 - jest.config.mjs | 10 + package-lock.json | 4797 +++++++++++++++++++++++++++++++++++++++ package.json | 41 + src/index.ts | 20 + tests/basic.test.js | 7 + tests/basic.test.js.map | 1 + tests/basic.test.ts | 7 + tsconfig.jest.json | 6 + tsconfig.json | 26 + 11 files changed, 4924 insertions(+), 6 deletions(-) create mode 100644 .npmignore create mode 100644 jest.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tests/basic.test.js create mode 100644 tests/basic.test.js.map create mode 100644 tests/basic.test.ts create mode 100644 tsconfig.jest.json create mode 100644 tsconfig.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0805074 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +dist/ +node_modules/ +*.log +*.tsbuildinfo +coverage/ +.git/ +.github/ +tsconfig.json +package-lock.json diff --git a/README.md b/README.md index 9cedfd3..ca4623a 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,3 @@ A command line utility for the Unity Game Engine. This tool is primarily designe ```bash npm install -g unity-cli ``` - -## Usage - -```bash -unity-cli [command] [options] -``` diff --git a/jest.config.mjs b/jest.config.mjs new file mode 100644 index 0000000..a09e7b9 --- /dev/null +++ b/jest.config.mjs @@ -0,0 +1,10 @@ +export default { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + roots: ['/tests'], + transform: { + '^.+\\.ts$': ['ts-jest', { useESM: true, tsconfig: '/tsconfig.jest.json' }], + }, + testMatch: ['**/?(*.)+(test).ts'], + // Removed deprecated globals config +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9e434c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4797 @@ +{ + "name": "unity-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unity-cli", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "commander": "^14.0.1" + }, + "bin": { + "unity-cli": "dist/index.js" + }, + "devDependencies": { + "@types/commander": "^2.12.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.4.0", + "jest": "^30.1.1", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", + "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", + "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.2", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.1.3", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.3", + "jest-resolve-dependencies": "30.1.3", + "jest-runner": "30.1.3", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.3", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", + "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.1.2", + "jest-snapshot": "30.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", + "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", + "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", + "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", + "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", + "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.2", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", + "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.1.3", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/commander": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz", + "integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz", + "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.11.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.1.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", + "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", + "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.2", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.218", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", + "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", + "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.1.3", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.1.3" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", + "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "p-limit": "^3.1.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", + "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", + "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.1.3", + "@jest/types": "30.0.5", + "babel-jest": "30.1.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.1.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.3", + "jest-runner": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", + "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", + "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", + "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", + "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.2", + "@jest/environment": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.3", + "jest-runtime": "30.1.3", + "jest-util": "30.0.5", + "jest-watcher": "30.1.3", + "jest-worker": "30.1.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", + "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/globals": "30.1.2", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", + "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.2", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.1.2", + "graceful-fs": "^4.2.11", + "jest-diff": "30.1.2", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", + "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.5", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/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/undici-types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", + "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd74c5d --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "unity-cli", + "version": "1.0.0", + "description": "A command line utility for the Unity Game Engine.", + "author": "RageAgainstThePixel", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/RageAgainstThePixel/unity-cli.git" + }, + "bugs": { + "url": "https://github.com/RageAgainstThePixel/unity-cli/issues" + }, + "homepage": "https://github.com/RageAgainstThePixel/unity-cli#readme", + "type": "module", + "main": "dist/index.js", + "bin": { + "unity-cli": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "link": "npm link", + "tests": "jest --roots tests" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@types/commander": "^2.12.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.4.0", + "jest": "^30.1.1", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + }, + "dependencies": { + "commander": "^14.0.1" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f5b7df1 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const pkgPath = join(__dirname, '..', 'package.json'); +const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); +const program = new Command(); + +program + .name('unity-cli') + .description('A command line utility for the Unity Game Engine.') + .version(pkg.version); + +// TODO additional commands + +program.parse(process.argv); diff --git a/tests/basic.test.js b/tests/basic.test.js new file mode 100644 index 0000000..bf0a0d9 --- /dev/null +++ b/tests/basic.test.js @@ -0,0 +1,7 @@ +import { describe, it, expect } from '@jest/globals'; +describe('unity-cli basic test', () => { + it('should pass a simple test', () => { + expect(true).toBe(true); + }); +}); +//# sourceMappingURL=basic.test.js.map \ No newline at end of file diff --git a/tests/basic.test.js.map b/tests/basic.test.js.map new file mode 100644 index 0000000..09f9f84 --- /dev/null +++ b/tests/basic.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"basic.test.js","sourceRoot":"","sources":["basic.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAErD,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/tests/basic.test.ts b/tests/basic.test.ts new file mode 100644 index 0000000..ac3ecc9 --- /dev/null +++ b/tests/basic.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from '@jest/globals'; + +describe('unity-cli basic test', () => { + it('should pass a simple test', () => { + expect(true).toBe(true); + }); +}); diff --git a/tsconfig.jest.json b/tsconfig.jest.json new file mode 100644 index 0000000..65ffdd4 --- /dev/null +++ b/tsconfig.jest.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..506bcfe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "esnext", + "types": [ + "node" + ], + "sourceMap": true, + "declaration": false, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "esModuleInterop": true, + }, + "include": [ + "src" + ] +} \ No newline at end of file From a5bbd904f089b1d385dee16f5cd98d764f34932c Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 21:39:33 -0400 Subject: [PATCH 03/90] removed comment --- jest.config.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/jest.config.mjs b/jest.config.mjs index a09e7b9..e99c99a 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -6,5 +6,4 @@ export default { '^.+\\.ts$': ['ts-jest', { useESM: true, tsconfig: '/tsconfig.jest.json' }], }, testMatch: ['**/?(*.)+(test).ts'], - // Removed deprecated globals config }; From 6f1a1f4592f366789ec9dd8be29bdb74321d3951 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 21:40:58 -0400 Subject: [PATCH 04/90] update .npmignore --- .npmignore | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.npmignore b/.npmignore index 0805074..f711c57 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,14 @@ -dist/ node_modules/ *.log *.tsbuildinfo coverage/ .git/ .github/ -tsconfig.json +tsconfig*.json package-lock.json +tests/ +jest.config.mjs +.vscode/ +.idea/ +.env +.env.local \ No newline at end of file From 820809dfb65733b783402c334aab3b1481375413 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 21:41:39 -0400 Subject: [PATCH 05/90] update .npmignore --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index f711c57..5501ee9 100644 --- a/.npmignore +++ b/.npmignore @@ -6,6 +6,7 @@ coverage/ .github/ tsconfig*.json package-lock.json +src/ tests/ jest.config.mjs .vscode/ From 016865fcef0ed70355f295fc586cf4de2cdc9923 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 14 Sep 2025 21:42:41 -0400 Subject: [PATCH 06/90] update .npmignore --- .npmignore | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.npmignore b/.npmignore index 5501ee9..8c98168 100644 --- a/.npmignore +++ b/.npmignore @@ -1,15 +1,14 @@ -node_modules/ -*.log -*.tsbuildinfo -coverage/ .git/ .github/ -tsconfig*.json -package-lock.json +.idea/ +.vscode/ src/ tests/ -jest.config.mjs -.vscode/ -.idea/ +node_modules/ .env -.env.local \ No newline at end of file +.env.local +*.log +*.tsbuildinfo +tsconfig*.json +package-lock.json +jest.config.mjs \ No newline at end of file From c9b4d9fec773c9146e20085264ea8b72099f9547 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 17 Sep 2025 11:30:58 -0400 Subject: [PATCH 07/90] added license-client added license-version, activate-license and return-license commands --- package-lock.json | 455 +++++++++++++++++++++++++++++++++++------- package.json | 4 +- src/index.ts | 62 +++++- src/license-client.ts | 410 +++++++++++++++++++++++++++++++++++++ src/unity-hub.ts | 33 +++ src/utilities.ts | 43 ++++ tsconfig.json | 9 +- 7 files changed, 934 insertions(+), 82 deletions(-) create mode 100644 src/license-client.ts create mode 100644 src/unity-hub.ts create mode 100644 src/utilities.ts diff --git a/package-lock.json b/package-lock.json index 9e434c9..7bf2ee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "commander": "^14.0.1" + "@actions/glob": "^0.5.0", + "commander": "^14.0.1", + "glob": "^11.0.3" }, "bin": { "unity-cli": "dist/index.js" @@ -24,6 +26,73 @@ "typescript": "^5.9.2" } }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/glob": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.5.0.tgz", + "integrity": "sha512-tST2rjPvJLRZLuT9NMUtyBjvj9Yo0MiJS3ow004slMvm8GFM+Zv9HvMJ7HWzfUyJnGrJvDsYkWBaaG3YKXRtCw==", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.9.1", + "minimatch": "^3.0.4" + } + }, + "node_modules/@actions/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@actions/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -578,11 +647,40 @@ "tslib": "^2.4.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -843,6 +941,83 @@ } } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/schemas": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", @@ -1567,7 +1742,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1580,7 +1754,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1728,7 +1901,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -2019,7 +2191,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2032,7 +2203,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -2048,7 +2218,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -2069,7 +2238,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2147,7 +2315,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -2174,7 +2341,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/error-ex": { @@ -2328,7 +2494,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -2407,22 +2572,24 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -2543,7 +2710,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2586,7 +2752,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -2674,19 +2839,18 @@ } }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest": { @@ -2848,6 +3012,83 @@ } } }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-diff": { "version": "30.1.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", @@ -3134,6 +3375,83 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-snapshot": { "version": "30.1.2", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", @@ -3474,16 +3792,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3503,7 +3820,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -3668,7 +3984,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parse-json": { @@ -3714,35 +4029,35 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -3886,7 +4201,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3899,7 +4213,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3909,7 +4222,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4010,7 +4322,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -4029,7 +4340,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4044,7 +4354,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4054,14 +4363,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4074,7 +4381,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -4091,7 +4397,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4104,7 +4409,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4384,6 +4688,15 @@ "license": "0BSD", "optional": true }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4435,6 +4748,18 @@ "node": ">=0.8.0" } }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", @@ -4544,7 +4869,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4567,7 +4891,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -4586,7 +4909,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -4604,7 +4926,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4614,14 +4935,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4636,7 +4955,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4649,7 +4967,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index cd74c5d..4f34c17 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "url": "https://github.com/RageAgainstThePixel/unity-cli/issues" }, "homepage": "https://github.com/RageAgainstThePixel/unity-cli#readme", - "type": "module", "main": "dist/index.js", "bin": { "unity-cli": "./dist/index.js" @@ -36,6 +35,7 @@ "typescript": "^5.9.2" }, "dependencies": { - "commander": "^14.0.1" + "commander": "^14.0.1", + "glob": "^11.0.3" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f5b7df1..78fc604 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,68 @@ #!/usr/bin/env node + import { Command } from 'commander'; import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import { join } from 'path'; +import { LicenseType, LicensingClient } from './license-client'; +import { promptForSecretInput } from './utilities'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); const program = new Command(); -program - .name('unity-cli') +program.name('unity-cli') .description('A command line utility for the Unity Game Engine.') .version(pkg.version); -// TODO additional commands +program.command('license-version') + .description('Print the version of the Unity License Client.') + .action(async () => { + const client = new LicensingClient(); + await client.Version(); + }); + +program.command('activate-license') + .description('Activate a Unity license.') + .option('--email ', 'Email associated with the Unity account. Required when activating a personal or professional license.') + .option('--password ', 'Password for the Unity account. Required when activating a personal or professional license.') + .option('--serial ', 'License serial number. Required when activating a professional license.') + .option('--type ', 'License type (personal, professional, floating)') + .option('--config ', 'Path to the configuration file. Required when activating a floating license.') + .action(async (options) => { + const client = new LicensingClient(); + const licenseType: LicenseType = options.type; + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { + throw new Error(`Invalid license type: ${licenseType}`); + } + + if (licenseType !== LicenseType.floating) { + if (!options.email) { + options.email = await promptForSecretInput('Email: '); + } + + if (!options.password) { + options.password = await promptForSecretInput('Password: '); + } + + if (licenseType === LicenseType.professional && !options.serial) { + options.serial = await promptForSecretInput('Serial: '); + } + } + + await client.Activate(licenseType, options.config, options.serial, options.email, options.password); + }); + +program.command('return-license') + .description('Return a Unity license.') + .option('--type ', 'License type (personal, professional, floating)') + .action(async (options) => { + const client = new LicensingClient(); + const licenseType: LicenseType = options.type; + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { + throw new Error(`Invalid license type: ${licenseType}`); + } + + await client.Deactivate(licenseType); + }); program.parse(process.argv); diff --git a/src/license-client.ts b/src/license-client.ts new file mode 100644 index 0000000..9a07a94 --- /dev/null +++ b/src/license-client.ts @@ -0,0 +1,410 @@ +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { UnityHub } from './unity-hub'; +import { ResolveGlobPath } from './utilities'; + +export enum LicenseType { + personal = 'personal', + professional = 'professional', + floating = 'floating' +} + +export class LicensingClient { + private unityHub: UnityHub = new UnityHub(); + private licenseClientPath: string | undefined; + private licenseVersion: string | undefined; + + constructor(licenseVersion: string | undefined = undefined) { + this.licenseVersion = licenseVersion; + } + + async init() { + await fs.promises.access(this.unityHub.executable, fs.constants.R_OK); + await fs.promises.access(this.unityHub.rootDirectory, fs.constants.R_OK); + const licensingClientExecutable = process.platform === 'win32' ? 'Unity.Licensing.Client.exe' : 'Unity.Licensing.Client'; + const licenseClientPath = await ResolveGlobPath([this.unityHub.rootDirectory, '**', licensingClientExecutable]); + this.licenseClientPath = licenseClientPath; + await fs.promises.access(this.licenseClientPath, fs.constants.X_OK); + return this.licenseClientPath; + } + + private getUnityCommonDir() { + const result = process.env['UNITY_COMMON_DIR']; + + if (result) { + return result; + } + + const platform = os.platform(); + + switch (platform) { + case 'win32': { + const programData = process.env['PROGRAMDATA'] || 'C:\\ProgramData'; + return path.join(programData, 'Unity'); + } + case 'darwin': { + return '/Library/Application Support/Unity'; + } + case 'linux': { + const dataHome = process.env['XDG_DATA_HOME'] || path.join(os.homedir(), '.local', 'share'); + return path.join(dataHome, 'unity3d', 'Unity'); + } + default: + throw new Error(`Failed to determine Unity common directory for platform: ${platform}`); + } + } + + private getExitCodeMessage(exitCode: number): string { + switch (exitCode) { + case 0: + return 'OK'; + case 1: + return 'Invalid arguments'; + case 2: + return 'Invalid credentials'; + case 3: + return 'Organization ID is missing'; + case 4: + return 'Package Access Control List file download failed'; + case 5: + return 'Context initialization failed'; + case 6: + return 'Replication service initialization failed'; + case 7: + return 'Orchestrator initialization failed'; + case 8: + return 'Floating service initialization failed'; + case 9: + return 'Package service initialization failed'; + case 10: + return 'Access token initialization failed'; + case 11: + return 'Multi client pipe server start failed'; + case 12: + return 'License activation generation failed'; + case 13: + return 'Syncing entitlements failed'; + case 14: + return 'No valid entitlement found'; + case 15: + return 'License update failed'; + case 16: + return 'Unable to get list of user seats'; + case 17: + return 'Seat activation or deactivation failed'; + case 18: + return 'Getting entitlements failed'; + case 19: + return 'Acquiring license failed'; + case 20: + return 'Renewing floating lease failed'; + case 21: + return 'Returning floating lease failed'; + default: + return `Unknown Error`; + } + } + + private async patchBinary(src: string, dest: string, searchValue: Buffer, replaceValue: Buffer): Promise { + const data = await fs.promises.readFile(src); + let modified = false; + + for (let i = 0; i <= data.length - searchValue.length; i++) { + if (data.subarray(i, i + searchValue.length).equals(searchValue)) { + replaceValue.copy(data, i); + modified = true; + i += searchValue.length - 1; + } + } + + if (!modified) { + throw new Error('Could not find the search value'); + } + + await fs.promises.writeFile(dest, data); + } + + private async patchLicenseVersion(): Promise { + if (!this.licenseVersion) { + // check if the UNITY_EDITOR_PATH is set. If it is, use it to determine the license version + const unityEditorPath = process.env['UNITY_EDITOR_PATH']; + + if (unityEditorPath) { + const versionMatch = unityEditorPath.match(/(\d+)\.(\d+)\.(\d+)/); + + if (!versionMatch) { + this.licenseVersion = '6.x'; // default to 6.x if version cannot be determined + } else { + switch (versionMatch[1]) { + case '4': + this.licenseVersion = '4.x'; + break; + case '5': + this.licenseVersion = '5.x'; + break; + default: + this.licenseVersion = '6.x'; // default to 6.x for any other + break; + } + } + } + + if (!this.licenseVersion) { + this.licenseVersion = '6.x'; // default to 6.x if not set + } + } + + if (this.licenseVersion === '6.x') { + return; + } + + if (this.licenseVersion !== '5.x' && this.licenseVersion !== '4.x') { + console.warn(`Warning: Specified license version '${this.licenseVersion}' is unsupported, skipping`); + return; + } + + if (!this.licenseClientPath) { + this.licenseClientPath = await this.init(); + } + + const clientDirectory = path.dirname(this.licenseClientPath); + const patchedDirectory = path.join(os.tmpdir(), `UnityLicensingClient-${this.licenseVersion.replace('.', '_')}`); + + if (await fs.promises.mkdir(patchedDirectory, { recursive: true }) === undefined) { + console.log('Unity Licensing Client was already patched, reusing') + } else { + let found = false; + for (const fileName of await fs.promises.readdir(clientDirectory)) { + if (fileName === 'Unity.Licensing.EntitlementResolver.dll') { + await this.patchBinary( + path.join(clientDirectory, fileName), path.join(patchedDirectory, fileName), + Buffer.from('6.x', 'utf16le'), + Buffer.from(this.licenseVersion, 'utf16le'), + ); + found = true; + } else { + await fs.promises.symlink(path.join(clientDirectory, fileName), path.join(patchedDirectory, fileName)); + } + } + + if (!found) { + throw new Error('Could not find Unity.Licensing.EntitlementResolver.dll in the unityhub installation'); + } + } + + this.licenseClientPath = path.join(patchedDirectory, path.basename(this.licenseClientPath)); + const unityCommonDir = this.getUnityCommonDir(); + const legacyLicenseFile = path.join(unityCommonDir, `Unity_v${this.licenseVersion}.ulf`); + await fs.promises.mkdir(unityCommonDir, { recursive: true }); + + try { + await fs.promises.symlink(path.join(patchedDirectory, 'Unity_lic.ulf'), legacyLicenseFile); + } catch (error) { + if (error && (error as NodeJS.ErrnoException).code === 'EEXIST') { + await fs.promises.unlink(legacyLicenseFile); + await fs.promises.symlink(path.join(patchedDirectory, 'Unity_lic.ulf'), legacyLicenseFile); + } else { + throw error; + } + } + + process.env['UNITY_COMMON_DIR'] = patchedDirectory; + } + + private async exec(args: string[]): Promise { + await this.patchLicenseVersion(); + + if (!this.licenseClientPath) { + this.licenseClientPath = await this.init(); + } + + await fs.promises.access(this.licenseClientPath, fs.constants.X_OK); + + let output: string = ''; + let exitCode: number = 0; + + try { + exitCode = await new Promise((resolve, reject) => { + const child = spawn(this.licenseClientPath as string, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + + child.stdout.on('data', (data) => { + const chunk = data.toString(); + output += chunk; + console.log(chunk); + }); + + child.stderr.on('data', (data) => { + const chunk = data.toString(); + output += chunk; + console.error(chunk); + }); + + child.on('error', (error) => { + reject(error); + }); + + child.on('close', (code) => { + resolve(code === null ? 0 : code); + }); + }); + } finally { + if (exitCode !== 0) { + const message = this.getExitCodeMessage(exitCode); + throw new Error(`License command failed with exit code ${exitCode}: ${message}`); + } + } + + return output; + } + + public async Version(): Promise { + await this.exec(['--version']); + } + + public async Activate(licenseType: LicenseType, servicesConfig: string | undefined = undefined, serial: string | undefined = undefined, username: string | undefined = undefined, password: string | undefined = undefined): Promise { + let activeLicenses = await this.showEntitlements(); + + if (activeLicenses.includes(licenseType)) { + console.log(`License of type '${licenseType}' is already active, skipping activation`); + return; + } + + switch (licenseType) { + case LicenseType.floating: { + if (!servicesConfig) { + throw new Error('Services config path is required for floating license activation'); + } + + let servicesPath: string; + switch (process.platform) { + case 'win32': + servicesPath = path.join(process.env.PROGRAMDATA || '', 'Unity', 'config'); + break; + case 'darwin': + servicesPath = path.join('/Library', 'Application Support', 'Unity', 'config'); + break; + case 'linux': + servicesPath = path.join('/usr', 'share', 'unity3d', 'config'); + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + const servicesConfigPath = path.join(servicesPath, 'services-config.json'); + await fs.promises.writeFile(servicesConfigPath, Buffer.from(servicesConfig, 'base64')); + return; + } + default: { // personal and professional license activation + if (!username) { + const encodedUsername = process.env.UNITY_USERNAME_BASE64; + + if (!encodedUsername) { + throw Error('Username is required for Unity License Activation!'); + } + + username = Buffer.from(encodedUsername, 'base64').toString('utf-8'); + } + + const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (username.length === 0 || !emailRegex.test(username)) { + throw Error('Username must be your Unity ID email address!'); + } + + if (!password) { + const encodedPassword = process.env.UNITY_PASSWORD_BASE64; + + if (!encodedPassword) { + throw Error('Password is required for Unity License Activation!'); + } + + password = Buffer.from(encodedPassword, 'base64').toString('utf-8'); + } + + if (password.length === 0) { + throw Error('Password is required for Unity License Activation!'); + } + + await this.activateLicense(licenseType, username, password, serial); + } + } + } + + public async Deactivate(licenseType: LicenseType): Promise { + if (licenseType === LicenseType.floating) { + return; + } + + const activeLicenses = await this.showEntitlements(); + + if (activeLicenses.includes(licenseType)) { + await this.returnLicense(licenseType); + } + } + + private async showEntitlements(): Promise { + const output = await this.exec([`--showEntitlements`]); + const matches = output.matchAll(/Product Name: (?.+)/g); + const licenses: LicenseType[] = []; + for (const match of matches) { + if (match.groups?.license) { + switch (match.groups.license) { + case 'Unity Pro': + if (!licenses.includes(LicenseType.professional)) { + licenses.push(LicenseType.professional); + } + break; + case 'Unity Personal': + if (!licenses.includes(LicenseType.personal)) { + licenses.push(LicenseType.personal); + } + break; + default: + throw Error(`Unsupported license type: ${match.groups.license}`); + } + } + } + return licenses; + } + + private async activateLicense(licenseType: LicenseType, username: string, password: string, serial: string | undefined = undefined): Promise { + const args = [ + `--activate-ulf`, + `--username`, username, + `--password`, password + ]; + + if (serial !== undefined && serial.length > 0) { + serial = serial.trim(); + args.push(`--serial`, serial); + } + + if (licenseType === LicenseType.personal) { + args.push(`--include-personal`); + } + + await this.exec(args); + + const activeLicenses = await this.showEntitlements(); + + if (!activeLicenses.includes(licenseType)) { + throw new Error(`Failed to activate license of type '${licenseType}'`); + } + + console.log(`Successfully activated license of type '${licenseType}'`); + } + + private async returnLicense(licenseType: LicenseType): Promise { + await this.exec([`--return-ulf`]); + + const activeLicenses = await this.showEntitlements(); + + if (activeLicenses.includes(licenseType)) { + throw new Error(`Failed to return license of type '${licenseType}'`); + } + + console.log(`Successfully returned license of type '${licenseType}'`); + } +} \ No newline at end of file diff --git a/src/unity-hub.ts b/src/unity-hub.ts new file mode 100644 index 0000000..3cb00d3 --- /dev/null +++ b/src/unity-hub.ts @@ -0,0 +1,33 @@ +import * as path from 'path'; + +export class UnityHub { + public executable: string; + public rootDirectory: string; + public editorInstallationDirectory: string; + public editorFileExtension: string; + + constructor() { + switch (process.platform) { + case 'win32': + this.executable = process.env.UNITY_HUB_PATH || 'C:/Program Files/Unity Hub/Unity Hub.exe'; + this.rootDirectory = path.join(this.executable, '../'); + this.editorInstallationDirectory = 'C:/Program Files/Unity/Hub/Editor/'; + this.editorFileExtension = '/Editor/Unity.exe'; + break; + case 'darwin': + this.executable = process.env.UNITY_HUB_PATH || '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'; + this.rootDirectory = path.join(this.executable, '../../../'); + this.editorInstallationDirectory = '/Applications/Unity/Hub/Editor/'; + this.editorFileExtension = '/Unity.app/Contents/MacOS/Unity'; + break; + case 'linux': + this.executable = process.env.UNITY_HUB_PATH || '/opt/unityhub/unityhub'; + this.rootDirectory = path.join(this.executable, '../'); + this.editorInstallationDirectory = `${process.env.HOME}/Unity/Hub/Editor/`; + this.editorFileExtension = '/Editor/Unity'; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } +} \ No newline at end of file diff --git a/src/utilities.ts b/src/utilities.ts new file mode 100644 index 0000000..9129324 --- /dev/null +++ b/src/utilities.ts @@ -0,0 +1,43 @@ + +import * as path from 'path'; +import * as glob from 'glob'; +import * as fs from 'fs'; +import * as readline from 'readline'; + +export async function ResolveGlobPath(globs: string[]): Promise { + const globPath: string = path.join(...globs).split(path.sep).join('/'); + const files: string[] = await glob.glob(globPath, { nodir: true }); + + for (const file of files) { + await fs.promises.access(file, fs.constants.R_OK); + return file; + } + + throw new Error(`No accessible file found for glob pattern: ${globPath}`); +} + +export async function promptForSecretInput(prompt: string): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + // @ts-ignore + rl.stdoutMuted = true; + + rl.question(prompt, (input) => { + rl.close(); + console.log(); // Move to next line after input + resolve(input); + }); + + // @ts-ignore + rl._writeToOutput = function _writeToOutput(stringToWrite: string) { + // @ts-ignore + if (rl.stdoutMuted) rl.output.write("*"); + // @ts-ignore + else rl.output.write(stringToWrite); + }; + }); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 506bcfe..6f9ef87 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,23 +2,24 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "commonjs", + "moduleResolution": "node", "target": "esnext", "types": [ "node" ], "sourceMap": true, - "declaration": false, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "strict": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", "skipLibCheck": true, "esModuleInterop": true, + "emitDeclarationOnly": false, + "declaration": true }, "include": [ "src" From d1b3e5041b70eb166390bb8f6ab5d075d476acd1 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 17 Sep 2025 11:37:35 -0400 Subject: [PATCH 08/90] formatting --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 78fc604..b6d8d0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ program.command('activate-license') .action(async (options) => { const client = new LicensingClient(); const licenseType: LicenseType = options.type; + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); } @@ -58,6 +59,7 @@ program.command('return-license') .action(async (options) => { const client = new LicensingClient(); const licenseType: LicenseType = options.type; + if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); } From 19b72602e81e42054b5e8c502f50fede9fbdb86b Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 17 Sep 2025 13:15:37 -0400 Subject: [PATCH 09/90] fix terminal prompts --- src/utilities.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/utilities.ts b/src/utilities.ts index 9129324..4d88609 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -20,24 +20,18 @@ export async function promptForSecretInput(prompt: string): Promise { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, + terminal: true }); - // @ts-ignore - rl.stdoutMuted = true; - rl.question(prompt, (input) => { + // mask the previous line with asterisks in place of each character + readline.moveCursor(process.stdout, 0, -1); + readline.clearLine(process.stdout, 0); + process.stdout.write(prompt + '*'.repeat(input.length) + '\n'); rl.close(); console.log(); // Move to next line after input resolve(input); }); - - // @ts-ignore - rl._writeToOutput = function _writeToOutput(stringToWrite: string) { - // @ts-ignore - if (rl.stdoutMuted) rl.output.write("*"); - // @ts-ignore - else rl.output.write(stringToWrite); - }; }); } \ No newline at end of file From b574bbc7fe9b0c4021c50d76d114b6991ae50d43 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 17 Sep 2025 13:23:41 -0400 Subject: [PATCH 10/90] add short args --- src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index b6d8d0b..c4250cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,14 +23,14 @@ program.command('license-version') program.command('activate-license') .description('Activate a Unity license.') - .option('--email ', 'Email associated with the Unity account. Required when activating a personal or professional license.') - .option('--password ', 'Password for the Unity account. Required when activating a personal or professional license.') - .option('--serial ', 'License serial number. Required when activating a professional license.') - .option('--type ', 'License type (personal, professional, floating)') - .option('--config ', 'Path to the configuration file. Required when activating a floating license.') + .option('-e, --email ', 'Email associated with the Unity account. Required when activating a personal or professional license.') + .option('-p, --password ', 'Password for the Unity account. Required when activating a personal or professional license.') + .option('-s, --serial ', 'License serial number. Required when activating a professional license.') + .option('-l, --license ', 'License type (personal, professional, floating).') + .option('-c, --config ', 'Path to the configuration file. Required when activating a floating license.') .action(async (options) => { const client = new LicensingClient(); - const licenseType: LicenseType = options.type; + const licenseType: LicenseType = options.license; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); From 161e73e0f459831a22bd7d4371d9ddc203b2f4e1 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 17 Sep 2025 18:28:17 -0400 Subject: [PATCH 11/90] implement unity hub installation --- package-lock.json | 257 +++++++++++++++++------------------------- package.json | 19 ++-- src/index.ts | 8 +- src/license-client.ts | 11 +- src/unity-hub.ts | 180 +++++++++++++++++++++++++++++ src/utilities.ts | 58 +++++++++- 6 files changed, 366 insertions(+), 167 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bf2ee5..1bf8896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@actions/glob": "^0.5.0", + "@electron/asar": "^4.0.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.1", "commander": "^14.0.1", - "glob": "^11.0.3" + "glob": "^11.0.3", + "semver": "^7.7.2", + "yaml": "^2.8.1" }, "bin": { "unity-cli": "dist/index.js" @@ -20,79 +23,13 @@ "@types/commander": "^2.12.0", "@types/jest": "^30.0.0", "@types/node": "^24.4.0", + "@types/semver": "^7.7.0", "jest": "^30.1.1", "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.2" } }, - "node_modules/@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", - "license": "MIT", - "dependencies": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" - } - }, - "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", - "license": "MIT", - "dependencies": { - "@actions/io": "^1.0.1" - } - }, - "node_modules/@actions/glob": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.5.0.tgz", - "integrity": "sha512-tST2rjPvJLRZLuT9NMUtyBjvj9Yo0MiJS3ow004slMvm8GFM+Zv9HvMJ7HWzfUyJnGrJvDsYkWBaaG3YKXRtCw==", - "license": "MIT", - "dependencies": { - "@actions/core": "^1.9.1", - "minimatch": "^3.0.4" - } - }, - "node_modules/@actions/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@actions/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@actions/http-client": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", - "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", - "license": "MIT", - "dependencies": { - "tunnel": "^0.0.6", - "undici": "^5.25.4" - } - }, - "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -149,6 +86,16 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", @@ -183,6 +130,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -613,6 +570,32 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@electron/asar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.0.1.tgz", + "integrity": "sha512-F4Ykm1jiBGY1WV/o8Q8oFW8Nq0u+S2/vPujzNJtdSJ6C4LHC4CiGLn7c17s7SolZ23gcvCebMncmZtNc+MkxPQ==", + "license": "MIT", + "dependencies": { + "commander": "^13.1.0", + "glob": "^11.0.1", + "minimatch": "^10.0.1" + }, + "bin": { + "asar": "bin/asar.mjs" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -647,13 +630,14 @@ "tslib": "^2.4.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "node_modules/@hey-api/client-fetch": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.4.tgz", + "integrity": "sha512-ebh1JjUdMAqes/Rg8OvbjDqGWGNhgHgmPtHlkIOUtj3y2mUXqX2g9sVoI/rSKW/FdADPng/90k5AL7bwT8W2lA==", + "deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.", "license": "MIT", - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/hey-api" } }, "node_modules/@isaacs/balanced-match": { @@ -1227,6 +1211,16 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@rage-against-the-pixel/unity-releases-api": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/unity-releases-api/-/unity-releases-api-1.0.1.tgz", + "integrity": "sha512-ubhZf78D7HTyGiQeQo9e6UIDzbRLgSP2HkPcb+fH8K9tbV0MkX0xhiYGnGPq94swZlVm52c5mZBE9hi4K2F3lg==", + "license": "MIT", + "dependencies": { + "@hey-api/client-fetch": "^0.4.4", + "jose": "^5.10.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -1396,6 +1390,13 @@ "undici-types": "~7.11.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1901,6 +1902,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -2218,6 +2220,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -2781,19 +2784,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -3485,19 +3475,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-util": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", @@ -3613,6 +3590,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3730,19 +3716,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4188,13 +4161,15 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -4610,19 +4585,6 @@ } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-jest/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -4688,15 +4650,6 @@ "license": "0BSD", "optional": true }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4748,18 +4701,6 @@ "node": ">=0.8.0" } }, - "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/undici-types": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", @@ -5013,6 +4954,18 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 4f34c17..beb89ed 100644 --- a/package.json +++ b/package.json @@ -16,26 +16,31 @@ "bin": { "unity-cli": "./dist/index.js" }, + "files": [ + "dist" + ], "scripts": { "build": "tsc", "dev": "tsc --watch", "link": "npm link", "tests": "jest --roots tests" }, - "files": [ - "dist" - ], + "dependencies": { + "@electron/asar": "^4.0.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.1", + "commander": "^14.0.1", + "glob": "^11.0.3", + "semver": "^7.7.2", + "yaml": "^2.8.1" + }, "devDependencies": { "@types/commander": "^2.12.0", "@types/jest": "^30.0.0", "@types/node": "^24.4.0", + "@types/semver": "^7.7.0", "jest": "^30.1.1", "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.2" - }, - "dependencies": { - "commander": "^14.0.1", - "glob": "^11.0.3" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c4250cc..4046506 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { Command } from 'commander'; import { readFileSync } from 'fs'; import { join } from 'path'; import { LicenseType, LicensingClient } from './license-client'; -import { promptForSecretInput } from './utilities'; +import { PromptForSecretInput } from './utilities'; const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); @@ -38,15 +38,15 @@ program.command('activate-license') if (licenseType !== LicenseType.floating) { if (!options.email) { - options.email = await promptForSecretInput('Email: '); + options.email = await PromptForSecretInput('Email: '); } if (!options.password) { - options.password = await promptForSecretInput('Password: '); + options.password = await PromptForSecretInput('Password: '); } if (licenseType === LicenseType.professional && !options.serial) { - options.serial = await promptForSecretInput('Serial: '); + options.serial = await PromptForSecretInput('Serial: '); } } diff --git a/src/license-client.ts b/src/license-client.ts index 9a07a94..7eebc46 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -21,8 +21,13 @@ export class LicensingClient { } async init() { - await fs.promises.access(this.unityHub.executable, fs.constants.R_OK); - await fs.promises.access(this.unityHub.rootDirectory, fs.constants.R_OK); + try { + await fs.promises.access(this.unityHub.executable, fs.constants.R_OK); + await fs.promises.access(this.unityHub.rootDirectory, fs.constants.R_OK); + } catch (error) { + await this.unityHub.Install(); + } + const licensingClientExecutable = process.platform === 'win32' ? 'Unity.Licensing.Client.exe' : 'Unity.Licensing.Client'; const licenseClientPath = await ResolveGlobPath([this.unityHub.rootDirectory, '**', licensingClientExecutable]); this.licenseClientPath = licenseClientPath; @@ -227,7 +232,7 @@ export class LicensingClient { try { exitCode = await new Promise((resolve, reject) => { - const child = spawn(this.licenseClientPath as string, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + const child = spawn(this.licenseClientPath!, args, { stdio: ['ignore', 'pipe', 'pipe'] }); child.stdout.on('data', (data) => { const chunk = data.toString(); diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 3cb00d3..75d1cf7 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,4 +1,7 @@ import * as path from 'path'; +import * as fs from 'fs'; +import { spawn } from 'child_process'; +import { DownloadFile, Exec } from './utilities'; export class UnityHub { public executable: string; @@ -30,4 +33,181 @@ export class UnityHub { throw new Error(`Unsupported platform: ${process.platform}`); } } + + private async exec(args: string[]): Promise { + await fs.promises.access(this.executable, fs.constants.X_OK); + + let output: string = ''; + let exitCode: number = 0; + + // These lines are commonly found in stderr but can be ignored + const ignoredLines = [ + `This error originated either by throwing inside of an async function without a catch block`, + `Unexpected error attempting to determine if executable file exists`, + `dri3 extension not supported`, + `Failed to connect to the bus:`, + `Checking for beta autoupdate feature for deb/rpm distributions`, + `Found package-type: deb`, + `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` + ]; + + function processOutput(line: string) { + if (line && line.trim().length > 0) { + if (ignoredLines.some(ignored => line.includes(ignored))) { + return; + } + + console.log(line); + output += `${line}\n`; + } + } + + try { + exitCode = await new Promise((resolve, reject) => { + const executable = process.platform === 'linux' ? 'unity-hub' : this.executable; + const execArgs = process.platform === 'linux' ? ['--headless', ...args] : ['--', '--headless', ...args]; + const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); + + child.stdout.on('data', (data) => { + processOutput(data.toString()); + }); + + child.stderr.on('data', (data) => { + processOutput(data.toString()); + }); + + child.on('error', (error) => { + reject(error); + }); + + child.on('close', (code) => { + resolve(code === null ? 0 : code); + }); + }); + } finally { + if (exitCode !== 0) { + throw new Error(`License command failed with exit code ${exitCode}`); + } + } + + const match = output.match(/Assertion (?.+) failed/g); + + if (match || + output.includes('async hook stack has become corrupted')) { + console.warn(`Install failed, retrying...`); + return await this.exec(args); + } + + if (output.includes('Error:')) { + const error = output.match(/Error: (.+)/); + const errorMessage = error && error[1] ? error[1] : 'Unknown Error'; + + switch (errorMessage) { + case 'No modules found to install.': + return output; + default: + throw new Error(`Failed to execute Unity Hub: ${errorMessage}`); + } + } + + return output; + } + + public async Install(): Promise { + try { + await fs.promises.access(this.executable, fs.constants.X_OK); + } catch { + await this.installInternal(); + } + } + + private async installInternal(): Promise { + switch (process.platform) { + case 'win32': { + const url = 'https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe'; + const downloadPath = path.join(process.env.TEMP || '.', 'UnityHubSetup.exe'); + await DownloadFile(url, downloadPath); + + console.log(`Running Unity Hub installer...`); + + try { + await Exec(downloadPath, ['/S']); + } finally { + fs.promises.unlink(downloadPath); + } + + break; + } + case 'darwin': { + const baseUrl = 'https://public-cdn.cloud.unity3d.com/hub/prod'; + const url = `${baseUrl}/UnityHubSetup-${process.arch}.dmg`; + const downloadPath = path.join(process.env.TEMP || '.', `UnityHubSetup-${process.arch}.dmg`); + console.log(`Downloading Unity Hub from ${url} to ${downloadPath}`); + + await DownloadFile(url, downloadPath); + + let mountPoint = ''; + console.log(`Mounting DMG...`); + + try { + const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse']); + // can be "/Volumes/Unity Hub 3.13.1-arm64" or "/Volumes/Unity Hub 3.13.1" + const mountPointMatch = output.match(/\/Volumes\/Unity Hub.*$/m); + + if (!mountPointMatch || mountPointMatch.length === 0) { + throw new Error(`Failed to find mount point in hdiutil output: ${output}`); + } + + mountPoint = mountPointMatch[0]; + console.log(`Mounted Unity Hub at ${mountPoint}`); + + const appPath = path.join(mountPoint, 'Unity Hub.app'); + console.log(`Copying ${appPath} to /Applications...`); + + await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); + await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); + await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); + await fs.promises.mkdir('/Library/Application Support/Unity', { recursive: true }); + await fs.promises.chmod('/Library/Application Support/Unity', 0o777); + } finally { + try { + if (mountPoint && mountPoint.length > 0) { + await Exec('hdiutil', ['detach', mountPoint, '-quiet']); + } + } finally { + await fs.promises.unlink(downloadPath); + } + } + break; + } + case 'linux': { + await Exec('sudo', ['sh', '-c', `#!/bin/bash +set -e +dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id +wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null +echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list +echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" | tee /etc/apt/sources.list.d/jammy.list +apt-get update +apt-get install -y --no-install-recommends unityhub ffmpeg libgtk2.0-0 libglu1-mesa libgconf-2-4 libncurses5 +apt-get clean +sed -i 's/^\\(.*DISPLAY=:.*XAUTHORITY=.*\\)\\( "\\$@" \\)2>&1$/\\1\\2/' /usr/bin/xvfb-run +printf '#!/bin/bash\nxvfb-run --auto-servernum /opt/unityhub/unityhub "$@" 2>/dev/null' | tee /usr/bin/unity-hub >/dev/null +chmod 777 /usr/bin/unity-hub +hubPath=$(which unityhub) + +if [ -z "$hubPath" ]; then + echo "Failed to install Unity Hub" + exit 1 +fi + +chmod -R 777 "$hubPath"`]); + break; + } + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + await fs.promises.access(this.executable, fs.constants.X_OK); + console.log(`Unity Hub installed successfully.`); + } } \ No newline at end of file diff --git a/src/utilities.ts b/src/utilities.ts index 4d88609..862b5ea 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -2,7 +2,9 @@ import * as path from 'path'; import * as glob from 'glob'; import * as fs from 'fs'; +import * as https from 'https'; import * as readline from 'readline'; +import { spawn } from 'child_process'; export async function ResolveGlobPath(globs: string[]): Promise { const globPath: string = path.join(...globs).split(path.sep).join('/'); @@ -16,7 +18,7 @@ export async function ResolveGlobPath(globs: string[]): Promise { throw new Error(`No accessible file found for glob pattern: ${globPath}`); } -export async function promptForSecretInput(prompt: string): Promise { +export async function PromptForSecretInput(prompt: string): Promise { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, @@ -34,4 +36,58 @@ export async function promptForSecretInput(prompt: string): Promise { resolve(input); }); }); +} + +export async function Exec(command: string, args: string[]): Promise { + let output: string = ''; + let exitCode: number = 0; + + try { + exitCode = await new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + + child.stdout.on('data', (data) => { + const chunk = data.toString(); + output += chunk; + console.log(chunk); + }); + + child.stderr.on('data', (data) => { + const chunk = data.toString(); + output += chunk; + console.error(chunk); + }); + + child.on('error', (error) => { + reject(error); + }); + + child.on('close', (code) => { + resolve(code === null ? 0 : code); + }); + }); + } finally { + if (exitCode !== 0) { + throw new Error(`${command} failed with exit code ${exitCode}`); + } + } + + return output; +} + +export async function DownloadFile(url: string, downloadPath: string): Promise { + console.log(`Downloading from ${url} to ${downloadPath}`); + await new Promise((resolve, reject) => { + const file = fs.createWriteStream(downloadPath); + https.get(url, (response) => { + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', (error) => { + fs.unlink(downloadPath, () => reject(`Download failed: ${error}`)); + }); + }); + await fs.promises.access(downloadPath, fs.constants.R_OK | fs.constants.X_OK); } \ No newline at end of file From 1d1bf72f76238b8740f18fef5ce592369032ee87 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 18 Sep 2025 12:36:43 -0400 Subject: [PATCH 12/90] added unity hub installer and editor installer options --- src/android-sdk.ts | 150 ++++++++ src/index.ts | 84 ++++- src/license-client.ts | 21 +- src/logging.ts | 68 ++++ src/unity-editor.ts | 26 ++ src/unity-hub.ts | 771 ++++++++++++++++++++++++++++++++++++++-- src/unity-version.ts | 164 +++++++++ src/utilities.ts | 55 ++- tests/basic.test.js.map | 1 - 9 files changed, 1293 insertions(+), 47 deletions(-) create mode 100644 src/android-sdk.ts create mode 100644 src/logging.ts create mode 100644 src/unity-editor.ts create mode 100644 src/unity-version.ts delete mode 100644 tests/basic.test.js.map diff --git a/src/android-sdk.ts b/src/android-sdk.ts new file mode 100644 index 0000000..ad53893 --- /dev/null +++ b/src/android-sdk.ts @@ -0,0 +1,150 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { UnityEditor } from './unity-editor'; +import { ReadFileContents, ResolveGlobToPath } from './utilities'; +import { Logger, LogLevel } from './logging'; +import { spawn } from 'child_process'; + +const logger = Logger.instance; + +export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: string): Promise { + let sdkPath = undefined; + await createRepositoryCfg(); + const rootEditorPath = await UnityEditor.GetEditorRootPath(editorPath); + const projectSettingsPath = path.join(projectPath, 'ProjectSettings/ProjectSettings.asset'); + const projectSettingsContent = await ReadFileContents(projectSettingsPath); + const matchResult = projectSettingsContent.match(/(?<=AndroidTargetSdkVersion: )\d+/); + const androidTargetSdk = matchResult ? parseInt(matchResult[0]) : 0; + logger.debug(`AndroidTargetSdkVersion:\n > ${androidTargetSdk}`); + + if (androidTargetSdk === undefined || androidTargetSdk === 0) { return; } + + logger.debug('Validating Android Target SDK Installed...'); + sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); + + if (sdkPath) { + logger.debug(`Target Android SDK android-${androidTargetSdk} Installed in:\n > "${sdkPath}"`); + return; + } + + logger.debug(`Installing Android Target SDK:\n > android-${androidTargetSdk}`); + const sdkManagerPath = await getSdkManager(rootEditorPath); + const javaSdk = await getJDKPath(rootEditorPath); + await execSdkManager(sdkManagerPath, javaSdk, ['--licenses']); + await execSdkManager(sdkManagerPath, javaSdk, ['--update']); + await execSdkManager(sdkManagerPath, javaSdk, ['platform-tools', `platforms;android-${androidTargetSdk}`]); + sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); + + if (!sdkPath) { + throw new Error(`Failed to install android-${androidTargetSdk} in ${rootEditorPath}`); + } + + logger.debug(`Target Android SDK Installed in:\n > "${sdkPath}"`); + +} + +async function createRepositoryCfg(): Promise { + const androidPath = path.join(os.homedir(), '.android'); + await fs.promises.mkdir(androidPath, { recursive: true }); + const fileHandle = await fs.promises.open(path.join(androidPath, 'repositories.cfg'), 'w'); + await fileHandle.close(); +} + +async function getJDKPath(rootEditorPath: string): Promise { + const jdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'AndroidPlayer', 'OpenJDK']); + + if (!jdkPath) { + throw new Error(`Failed to resolve OpenJDK in ${rootEditorPath}`); + } + + await fs.promises.access(jdkPath, fs.constants.R_OK); + logger.debug(`jdkPath:\n > "${jdkPath}"`); + return jdkPath; +} + +async function getSdkManager(rootEditorPath: string): Promise { + let globPath: string[] = []; + switch (process.platform) { + case 'darwin': + case 'linux': + globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager']; + break; + case 'win32': + globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat']; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + const sdkmanagerPath = await ResolveGlobToPath(globPath); + + if (!sdkmanagerPath) { + throw new Error(`Failed to resolve sdkmanager in ${globPath}`); + } + + await fs.promises.access(sdkmanagerPath, fs.constants.R_OK); + logger.debug(`sdkmanagerPath:\n > "${sdkmanagerPath}"`); + return sdkmanagerPath; +} + +async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise { + logger.debug(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); + const sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'AndroidPlayer', '**', `android-${androidTargetSdk}`]); + try { + await fs.promises.access(sdkPath, fs.constants.R_OK); + } catch (error) { + logger.debug(`android-${androidTargetSdk} not installed`); + return undefined; + } + logger.debug(`sdkPath:\n > "${sdkPath}"`); + return sdkPath; +} + +async function execSdkManager(sdkManagerPath: string, javaPath: string, args: string[]): Promise { + const acceptBuffer = Buffer.from(Array(10).fill('y').join(os.EOL), 'utf8'); + let output = ''; + let exitCode = 0; + + try { + exitCode = await new Promise((resolve, reject) => { + if (logger.logLevel === LogLevel.DEBUG) { + logger.info(`\x1b[34m${sdkManagerPath} ${args.join(' ')}\x1b[0m`); + } + + const child = spawn(sdkManagerPath, args, { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, JAVA_HOME: javaPath } + }); + + child.stdout.on('data', (data: Buffer) => { + const chunk = data.toString(); + output += chunk; + + if (output.includes('Accept? (y/N):')) { + child.stdin?.write(acceptBuffer); + output = ''; + } + + logger.debug(chunk); + }); + + child.stderr.on('data', (data: Buffer) => { + const chunk = data.toString(); + output += chunk; + logger.error(chunk); + }); + + child.on('error', (error: Error) => { + reject(error); + }); + + child.on('close', (code: number | null) => { + resolve(code === null ? 0 : code); + }); + }); + } finally { + if (exitCode !== 0) { + throw new Error(`${sdkManagerPath} ${args.join(' ')} failed with exit code ${exitCode}`); + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4046506..c05a56d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,9 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { LicenseType, LicensingClient } from './license-client'; import { PromptForSecretInput } from './utilities'; +import { UnityHub } from './unity-hub'; +import { Logger, LogLevel } from './logging'; +import { UnityVersion } from './unity-version'; const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); @@ -28,9 +31,14 @@ program.command('activate-license') .option('-s, --serial ', 'License serial number. Required when activating a professional license.') .option('-l, --license ', 'License type (personal, professional, floating).') .option('-c, --config ', 'Path to the configuration file. Required when activating a floating license.') + .option('--verbose', 'Enable verbose logging.') .action(async (options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + const client = new LicensingClient(); - const licenseType: LicenseType = options.license; + const licenseType: LicenseType = options.license.toString().toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); @@ -55,10 +63,15 @@ program.command('activate-license') program.command('return-license') .description('Return a Unity license.') - .option('--type ', 'License type (personal, professional, floating)') + .option('-l, --license ', 'License type (personal, professional, floating)') + .option('--verbose', 'Enable verbose logging.') .action(async (options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + const client = new LicensingClient(); - const licenseType: LicenseType = options.type; + const licenseType: LicenseType = options.license.toString().toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); @@ -67,4 +80,69 @@ program.command('return-license') await client.Deactivate(licenseType); }); +program.command('hub-version') + .description('Print the version of the Unity Hub.') + .action(async () => { + const hub = new UnityHub(); + await hub.Version(); + }); + +program.command('hub-install') + .description('Install the Unity Hub.') + .option('--verbose', 'Enable verbose logging.') + .action(async (options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + + const hub = new UnityHub(); + await hub.Install(); + }); + +program.command('hub-path') + .description('Print the path to the Unity Hub executable.') + .action(async () => { + const hub = new UnityHub(); + process.stdout.write(hub.executable); + }); + +program.command('hub') + .description('Run commands directly to the Unity Hub. (You need not to pass --headless or -- to this command).') + .allowUnknownOption(true) + .option('--verbose', 'Enable verbose logging.') + .argument('', 'Arguments to pass to the Unity Hub executable.') + .action(async (args: string[], options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + + const hub = new UnityHub(); + await hub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); + }); + +program.command('hub-get-editor') + .description('Attempts to find or install the specified Unity Editor version.') + .option('-u, --unity-version ', 'The Unity version to get (e.g. 2020.3.1f1, 2021.x, 2022.1.*, 6000).') + .option('-c, --changeset ', 'The Unity changeset to get (e.g. 1234567890ab).') + .option('-m, --modules ', 'The Unity module to get (e.g. ios, android).') + .option('--verbose', 'Enable verbose logging.') + .action(async (options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + + Logger.instance.debug(JSON.stringify(options)); + + if (!options.unityVersion) { + throw new Error('You must specify a Unity version with -u or --unity-version.'); + } + + const unityVersion = new UnityVersion(options.unityVersion, options.changeset); + const modules: string[] = options.modules ? options.modules.split(',').split(' ') : []; + const hub = new UnityHub(); + + const editorPath = await hub.GetEditor(unityVersion, modules); + process.stdout.write(editorPath); + }); + program.parse(process.argv); diff --git a/src/license-client.ts b/src/license-client.ts index 7eebc46..6c0eee0 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -2,8 +2,9 @@ import * as os from 'os'; import * as fs from 'fs'; import * as path from 'path'; import { spawn } from 'child_process'; +import { Logger } from './logging'; import { UnityHub } from './unity-hub'; -import { ResolveGlobPath } from './utilities'; +import { ResolveGlobToPath } from './utilities'; export enum LicenseType { personal = 'personal', @@ -15,6 +16,7 @@ export class LicensingClient { private unityHub: UnityHub = new UnityHub(); private licenseClientPath: string | undefined; private licenseVersion: string | undefined; + private logger: Logger = Logger.instance; constructor(licenseVersion: string | undefined = undefined) { this.licenseVersion = licenseVersion; @@ -29,7 +31,7 @@ export class LicensingClient { } const licensingClientExecutable = process.platform === 'win32' ? 'Unity.Licensing.Client.exe' : 'Unity.Licensing.Client'; - const licenseClientPath = await ResolveGlobPath([this.unityHub.rootDirectory, '**', licensingClientExecutable]); + const licenseClientPath = await ResolveGlobToPath([this.unityHub.rootDirectory, '**', licensingClientExecutable]); this.licenseClientPath = licenseClientPath; await fs.promises.access(this.licenseClientPath, fs.constants.X_OK); return this.licenseClientPath; @@ -166,7 +168,7 @@ export class LicensingClient { } if (this.licenseVersion !== '5.x' && this.licenseVersion !== '4.x') { - console.warn(`Warning: Specified license version '${this.licenseVersion}' is unsupported, skipping`); + this.logger.warn(`Warning: Specified license version '${this.licenseVersion}' is unsupported, skipping`); return; } @@ -178,7 +180,7 @@ export class LicensingClient { const patchedDirectory = path.join(os.tmpdir(), `UnityLicensingClient-${this.licenseVersion.replace('.', '_')}`); if (await fs.promises.mkdir(patchedDirectory, { recursive: true }) === undefined) { - console.log('Unity Licensing Client was already patched, reusing') + this.logger.info('Unity Licensing Client was already patched, reusing'); } else { let found = false; for (const fileName of await fs.promises.readdir(clientDirectory)) { @@ -232,18 +234,19 @@ export class LicensingClient { try { exitCode = await new Promise((resolve, reject) => { + this.logger.info(`\x1b[34m${this.licenseClientPath} ${args.join(' ')}\x1b[0m`); const child = spawn(this.licenseClientPath!, args, { stdio: ['ignore', 'pipe', 'pipe'] }); child.stdout.on('data', (data) => { const chunk = data.toString(); output += chunk; - console.log(chunk); + this.logger.info(chunk); }); child.stderr.on('data', (data) => { const chunk = data.toString(); output += chunk; - console.error(chunk); + this.logger.error(chunk); }); child.on('error', (error) => { @@ -272,7 +275,7 @@ export class LicensingClient { let activeLicenses = await this.showEntitlements(); if (activeLicenses.includes(licenseType)) { - console.log(`License of type '${licenseType}' is already active, skipping activation`); + this.logger.info(`License of type '${licenseType}' is already active, skipping activation`); return; } @@ -398,7 +401,7 @@ export class LicensingClient { throw new Error(`Failed to activate license of type '${licenseType}'`); } - console.log(`Successfully activated license of type '${licenseType}'`); + this.logger.info(`Successfully activated license of type '${licenseType}'`); } private async returnLicense(licenseType: LicenseType): Promise { @@ -410,6 +413,6 @@ export class LicensingClient { throw new Error(`Failed to return license of type '${licenseType}'`); } - console.log(`Successfully returned license of type '${licenseType}'`); + this.logger.info(`Successfully returned license of type '${licenseType}'`); } } \ No newline at end of file diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..47a00f4 --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,68 @@ +export enum LogLevel { + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', +} + +export class Logger { + public logLevel: LogLevel = LogLevel.INFO; + public ci: string | undefined; + static instance: Logger = new Logger(); + + private constructor() { + if (process.env.GITHUB_ACTIONS) { + this.ci = 'GITHUB_ACTIONS'; + } + + Logger.instance = this; + } + + /** + * Logs a message to the console. + * @param level The log level for this message. + * @param message The message to log. + * @param optionalParams Additional parameters to log. + */ + public log(level: LogLevel, message: any, optionalParams: any[] = []): void { + if (this.shouldLog(level)) { + switch (this.ci) { + case 'GITHUB_ACTIONS': { + console.log(`::${level}::${message}`, ...optionalParams); + break; + } + default: { + const clear = '\x1b[0m'; + const stringColor: string = { + [LogLevel.DEBUG]: '\x1b[35m', // Purple + [LogLevel.INFO]: clear, // No color / White + [LogLevel.WARN]: '\x1b[33m', // Yellow + [LogLevel.ERROR]: '\x1b[31m', // Red + }[level] || clear; // Default + console.log(`${stringColor}${message}${clear}`, ...optionalParams); + } + } + } + } + + public debug(message: any, ...optionalParams: any[]): void { + this.log(LogLevel.DEBUG, message, optionalParams); + } + + public info(message: any, ...optionalParams: any[]): void { + this.log(LogLevel.INFO, message, optionalParams); + } + + public warn(message: any, ...optionalParams: any[]): void { + this.log(LogLevel.WARN, message, optionalParams); + } + + public error(message: any, ...optionalParams: any[]): void { + this.log(LogLevel.ERROR, message, optionalParams); + } + + private shouldLog(level: LogLevel): boolean { + const levelOrder = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; + return levelOrder.indexOf(level) >= levelOrder.indexOf(this.logLevel); + } +} \ No newline at end of file diff --git a/src/unity-editor.ts b/src/unity-editor.ts new file mode 100644 index 0000000..b4b47e2 --- /dev/null +++ b/src/unity-editor.ts @@ -0,0 +1,26 @@ +import { Logger } from './logging.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class UnityEditor { + private static logger: Logger = Logger.instance; + + static async GetEditorRootPath(editorPath: string): Promise { + this.logger.debug(`searching for editor root path: ${editorPath}`); + let editorRootPath = editorPath; + switch (process.platform) { + case 'darwin': + editorRootPath = path.join(editorPath, '../../../../'); + break; + case 'linux': + editorRootPath = path.join(editorPath, '../../'); + break; + case 'win32': + editorRootPath = path.join(editorPath, '../../'); + break + } + await fs.promises.access(editorRootPath, fs.constants.R_OK); + this.logger.debug(`found editor root path: ${editorRootPath}`); + return editorRootPath; + } +} \ No newline at end of file diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 75d1cf7..43ccbee 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,7 +1,25 @@ -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'yaml'; +import * as asar from '@electron/asar'; import { spawn } from 'child_process'; -import { DownloadFile, Exec } from './utilities'; +import { Logger, LogLevel } from './logging'; +import { + SemVer, + coerce, + compare, + valid +} from 'semver'; +import { + DeleteDirectory, + DownloadFile, + Exec, + ReadFileContents +} from './utilities'; +import { UnityVersion } from './unity-version'; +import { GetUnityReleasesData, UnityRelease } from '@rage-against-the-pixel/unity-releases-api/dist/unity-releases-api/types.gen'; +import { UnityReleasesClient } from '@rage-against-the-pixel/unity-releases-api/dist/client'; +import { UnityEditor } from './unity-editor'; export class UnityHub { public executable: string; @@ -9,6 +27,8 @@ export class UnityHub { public editorInstallationDirectory: string; public editorFileExtension: string; + private logger: Logger = Logger.instance; + constructor() { switch (process.platform) { case 'win32': @@ -34,7 +54,13 @@ export class UnityHub { } } - private async exec(args: string[]): Promise { + /** + * Executes the Unity Hub command with the specified arguments. + * @param args Arguments to pass to the Unity Hub executable. + * @param silent If true, suppresses output logging. + * @returns The output from the command. + */ + public async Exec(args: string[], options: { silent: boolean, showCommand: boolean } = { silent: false, showCommand: true }): Promise { await fs.promises.access(this.executable, fs.constants.X_OK); let output: string = ''; @@ -51,30 +77,36 @@ export class UnityHub { `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` ]; - function processOutput(line: string) { + const processOutput = (data: Buffer) => { + const line = data.toString(); + if (line && line.trim().length > 0) { if (ignoredLines.some(ignored => line.includes(ignored))) { return; } - console.log(line); + if (!options.silent) { + this.logger.info(line); + } + output += `${line}\n`; } - } + }; try { exitCode = await new Promise((resolve, reject) => { + const filteredArgs = args.filter(arg => arg !== '--headless' && arg !== '--'); const executable = process.platform === 'linux' ? 'unity-hub' : this.executable; - const execArgs = process.platform === 'linux' ? ['--headless', ...args] : ['--', '--headless', ...args]; - const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); + const execArgs = process.platform === 'linux' ? ['--headless', ...filteredArgs] : ['--', '--headless', ...filteredArgs]; - child.stdout.on('data', (data) => { - processOutput(data.toString()); - }); + if (options.showCommand) { + this.logger.info(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); + } - child.stderr.on('data', (data) => { - processOutput(data.toString()); - }); + const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); + + child.stdout.on('data', processOutput); + child.stderr.on('data', processOutput); child.on('error', (error) => { reject(error); @@ -94,8 +126,8 @@ export class UnityHub { if (match || output.includes('async hook stack has become corrupted')) { - console.warn(`Install failed, retrying...`); - return await this.exec(args); + this.logger.warn(`Install failed, retrying...`); + return await this.Exec(args); } if (output.includes('Error:')) { @@ -113,22 +145,71 @@ export class UnityHub { return output; } + /** + * Prints the installed Unity Hub version. + */ + public async Version(): Promise { + const version = await this.getInstalledHubVersion(); + this.logger.info(`Unity Hub Version: ${version.version}`); + } + + /** + * Installs or updates the Unity Hub. + * If the Unity Hub is already installed, it will be updated to the latest version. + */ public async Install(): Promise { + let isInstalled = false; try { await fs.promises.access(this.executable, fs.constants.X_OK); + isInstalled = true; } catch { - await this.installInternal(); + await this.installHub(); } + + if (isInstalled) { + const installedVersion: SemVer = await this.getInstalledHubVersion(); + this.logger.debug(`Installed Unity Hub version: ${installedVersion.version}`); + let latestVersion: SemVer | undefined = undefined; + + try { + latestVersion = await this.getLatestHubVersion(); + this.logger.debug(`Latest Unity Hub version: ${latestVersion.version}`); + } catch (error) { + this.logger.warn(`Failed to get latest Unity Hub version: ${error}`); + } + + if (latestVersion && compare(installedVersion, latestVersion) < 0) { + this.logger.info(`Updating Unity Hub from ${installedVersion.version} to ${latestVersion.version}...`); + + if (process.platform !== 'linux') { + await DeleteDirectory(this.rootDirectory); + await this.installHub(); + } else { + await Exec('sudo', ['sh', '-c', `#!/bin/bash +set -e +wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null +sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list' +sudo apt-get update --allow-releaseinfo-change +sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); + } + } else { + this.logger.info(`Unity Hub is up to date.`); + } + } + + this.logger.info(`Unity Hub Path:\n > ${this.executable}`); + + await this.Exec(['help']); } - private async installInternal(): Promise { + private async installHub(): Promise { switch (process.platform) { case 'win32': { const url = 'https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe'; const downloadPath = path.join(process.env.TEMP || '.', 'UnityHubSetup.exe'); await DownloadFile(url, downloadPath); - console.log(`Running Unity Hub installer...`); + this.logger.info(`Running Unity Hub installer...`); try { await Exec(downloadPath, ['/S']); @@ -142,12 +223,12 @@ export class UnityHub { const baseUrl = 'https://public-cdn.cloud.unity3d.com/hub/prod'; const url = `${baseUrl}/UnityHubSetup-${process.arch}.dmg`; const downloadPath = path.join(process.env.TEMP || '.', `UnityHubSetup-${process.arch}.dmg`); - console.log(`Downloading Unity Hub from ${url} to ${downloadPath}`); + this.logger.info(`Downloading Unity Hub from ${url} to ${downloadPath}`); await DownloadFile(url, downloadPath); let mountPoint = ''; - console.log(`Mounting DMG...`); + this.logger.debug(`Mounting DMG...`); try { const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse']); @@ -159,10 +240,10 @@ export class UnityHub { } mountPoint = mountPointMatch[0]; - console.log(`Mounted Unity Hub at ${mountPoint}`); + this.logger.debug(`Mounted Unity Hub at ${mountPoint}`); const appPath = path.join(mountPoint, 'Unity Hub.app'); - console.log(`Copying ${appPath} to /Applications...`); + this.logger.debug(`Copying ${appPath} to /Applications...`); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); @@ -208,6 +289,648 @@ chmod -R 777 "$hubPath"`]); } await fs.promises.access(this.executable, fs.constants.X_OK); - console.log(`Unity Hub installed successfully.`); + this.logger.info(`Unity Hub installed successfully.`); + } + + private async getInstalledHubVersion(): Promise { + let asarPath: string | undefined = undefined; + + switch (process.platform) { + case 'darwin': { + asarPath = path.join(this.rootDirectory, 'Contents', 'Resources', 'app.asar'); + break; + } + default: { + asarPath = path.join(this.rootDirectory, 'resources', 'app.asar'); + break; + } + } + + await fs.promises.access(asarPath, fs.constants.R_OK); + const fileBuffer = asar.extractFile(asarPath, 'package.json'); + const packageJson = JSON.parse(fileBuffer.toString()); + const version = coerce(packageJson.version); + + if (!version || !valid(version)) { + throw new Error(`Failed to parse Unity Hub version: ${packageJson.version}`); + } + + return version; + } + + private async getLatestHubVersion(): Promise { + let url: string | undefined = undefined; + + switch (process.platform) { + case 'win32': + url = 'https://public-cdn.cloud.unity3d.com/hub/prod/latest.yml'; + break; + case 'darwin': + url = 'https://public-cdn.cloud.unity3d.com/hub/prod/latest-mac.yml'; + break; + case 'linux': + url = 'https://public-cdn.cloud.unity3d.com/hub/prod/latest-linux.yml'; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + const response = await fetch(url); + const data = await response.text(); + const parsed = yaml.parse(data); + const version = coerce(parsed.version); + + if (!version || !valid(version)) { + throw new Error(`Failed to parse latest Unity Hub version: ${parsed.version}`); + } + + return version; + } + + /** + * Returns the path where the Unity editors will be installed. + * @returns {Promise} The install path. + */ + public async GetInstallPath(): Promise { + const result = (await this.Exec(['install-path', '--get'])).trim(); + if (!result || result.length === 0) { + throw new Error('Failed to get Unity Hub install path.'); + } + + return result; + } + + /** + * Sets the path where Unity editors will be installed. + * @param installPath The path to set. + */ + public async SetInstallPath(installPath: string): Promise { + await fs.promises.mkdir(installPath, { recursive: true }); + await this.Exec(['install-path', '--set', installPath]); + } + + /** + * Locate and associate an installed editor from a stipulated path. + * @param editorPath + */ + public async AddEditor(editorPath: string): Promise { + await fs.promises.access(editorPath, fs.constants.R_OK | fs.constants.X_OK); + await this.Exec(['editors', '--add', editorPath]); + } + + /** + * Attempts to find or install the specified Unity Editor version with the requested modules. + * @param unityVersion The Unity version to find or install. + * @param modules The modules to install alongside the editor. + * @returns The path to the Unity Editor executable. + */ + public async GetEditor(unityVersion: UnityVersion, modules: string[]): Promise { + const retryErrorMessages = [ + 'Editor already installed in this location', + 'failed to download. Error given: Request timeout' + ]; + + this.logger.debug(`Getting release info for Unity ${unityVersion.toString()}...`); + let editorPath = await this.checkInstalledEditors(unityVersion, false); + + // attempt to resolve the full version with the changeset if we don't have one already + if (!unityVersion.isLegacy() && !editorPath && !unityVersion.changeset) { + try { + const releases = await this.getLatestHubReleases(); + unityVersion = unityVersion.findMatch(releases); + const unityReleaseInfo: UnityRelease = await this.getEditorReleaseInfo(unityVersion); + unityVersion = new UnityVersion(unityReleaseInfo.version, unityReleaseInfo.shortRevision, unityVersion.architecture); + } catch (error) { + this.logger.warn(`Failed to get Unity release info for ${unityVersion.toString()}! falling back to legacy search...\n${error}`); + try { + unityVersion = await this.fallbackVersionLookup(unityVersion); + } catch (fallbackError) { + this.logger.warn(`Failed to lookup changeset for Unity ${unityVersion.toString()}!\n${fallbackError}`); + } + } + } + + let installPath: string | undefined = undefined; + + if (!editorPath) { + try { + installPath = await this.installUnity(unityVersion, modules); + } catch (error: Error | any) { + if (retryErrorMessages.some(msg => error.message.includes(msg))) { + if (editorPath) { + await DeleteDirectory(editorPath); + } + + if (installPath) { + await DeleteDirectory(installPath); + } + installPath = await this.installUnity(unityVersion, modules); + } else { + throw error; + } + } + + editorPath = await this.checkInstalledEditors(unityVersion, true, installPath); + } + + if (!editorPath) { + throw new Error(`Failed to find or install Unity Editor: ${unityVersion.toString()}`); + } + + await fs.promises.access(editorPath, fs.constants.X_OK); + await this.patchBeeBackend(editorPath); + + if (unityVersion.isLegacy() || modules.length === 0) { + return editorPath; + } + + try { + this.logger.info(`Checking installed modules for Unity ${unityVersion.toString()}...`); + const [installedModules, additionalModules] = await this.checkEditorModules(editorPath, unityVersion, modules); + + if (installedModules && installedModules.length > 0) { + this.logger.info(`Installed Modules:`); + + for (const module of installedModules) { + this.logger.info(` > ${module}`); + } + } + if (additionalModules && additionalModules.length > 0) { + this.logger.info(`Additional Modules:`); + + for (const module of additionalModules) { + this.logger.info(` > ${module}`); + } + } + } catch (error: Error | any) { + if (error.message.includes(`No modules found`)) { + await DeleteDirectory(editorPath); + await this.GetEditor(unityVersion, modules); + } + } + + return editorPath; + } + + /** + * Lists the installed Unity Editors. + * @returns A list of installed Unity Editor versions and their paths. + */ + public async ListInstalledEditors(): Promise { + return (await this.Exec(['editors', '-i'], { silent: this.logger.logLevel !== LogLevel.DEBUG, showCommand: this.logger.logLevel === LogLevel.DEBUG })).split('\n').filter(line => line.trim().length > 0).map(line => line.trim()); + } + + private async checkInstalledEditors(unityVersion: UnityVersion, failOnEmpty: boolean, installPath: string | undefined = undefined): Promise { + let editorPath = undefined; + if (!installPath) { + const paths: string[] = await this.ListInstalledEditors(); + this.logger.debug(`Paths: ${JSON.stringify(paths, null, 2)}`); + if (paths && paths.length > 0) { + const pattern = /(?\d+\.\d+\.\d+[abcfpx]?\d*)\s*(?:\((?Apple silicon|Intel)\))?\s*,? installed at (?.*)/; + const matches = paths.map(path => path.match(pattern)).filter(match => match && match.groups); + this.logger.debug(`Matches: ${JSON.stringify(matches, null, 2)}`); + + if (paths.length !== matches.length) { + throw new Error(`Failed to parse all installed Unity Editors!`); + } + + // Prefer exact version match first + const exactMatch = matches.find(match => match?.groups?.version === unityVersion.version); + + if (exactMatch) { + editorPath = exactMatch.groups!.editorPath; + } else { + // Fallback: semver satisfies + const versionMatches = matches.filter(match => match?.groups?.version && unityVersion.satisfies(match.groups.version)); + this.logger.debug(`Version Matches: ${JSON.stringify(versionMatches, null, 2)}`); + + if (versionMatches.length === 0) { + return undefined; + } + + const archMap = { + 'ARM64': 'Apple silicon', + 'X86_64': 'Intel', + }; + + for (const match of versionMatches) { + if (!match || !match.groups || !match.groups.version || !match.groups.editorPath) { + continue; + } + // If no architecture is set, or no arch in match, accept the version match + if (!unityVersion.architecture || !match.groups.arch) { + editorPath = match.groups.editorPath; + } + // If architecture is set and present in match, check for match + else if (archMap[unityVersion.architecture] === match.groups.arch) { + editorPath = match.groups.editorPath; + } + // Fallback: check if editorPath includes architecture string (case-insensitive) + else if (unityVersion.architecture && match.groups.editorPath.toLowerCase().includes(`-${unityVersion.architecture.toLowerCase()}`)) { + editorPath = match.groups.editorPath; + } + } + } + } + } else { + if (process.platform == 'win32') { + editorPath = path.join(installPath, 'Unity.exe'); + } else { + editorPath = installPath; + } + } + + if (!editorPath) { + if (failOnEmpty) { + throw new Error(`Failed to find installed Unity Editor: ${unityVersion.toString()}`); + } + else { + return undefined; + } + } + + if (process.platform === 'darwin') { + editorPath = path.join(editorPath, '/Contents/MacOS/Unity'); + } + + try { + await fs.promises.access(editorPath, fs.constants.R_OK); + } catch (error) { + throw new Error(`Failed to find installed Unity Editor: ${unityVersion.toString()}\n > ${error}`); + } + + this.logger.debug(`Found installed Unity Editor: ${editorPath}`); + return editorPath; + } + + private async getLatestHubReleases(): Promise { + // Normalize output to bare version strings (e.g., 2022.3.62f1) + // Unity Hub can return lines like: + // - "6000.0.56f1 (Apple silicon)" + // - "2022.3.62f1 installed at C:\\..." + // - "2022.3.62f1, installed at ..." (older format) + // We extract the first version token and discard the rest. + const versionRegex = /(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)/; + return (await this.Exec([`editors`, `--releases`])) + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(line => { + const match = line.match(versionRegex); + return match ? match[0] : ''; + }) + .filter(v => v.length > 0); + } + + /** + * Patches the Bee Backend for Unity Linux Editor. + * https://discussions.unity.com/t/linux-editor-stuck-on-loading-because-of-bee-backend-w-workaround/854480 + * @param editorPath The path to the Unity Editor executable. + */ + private async patchBeeBackend(editorPath: string): Promise { + if (process.platform === 'linux') { + const dataPath = path.join(path.dirname(editorPath), 'Data'); + const beeBackend = path.join(dataPath, 'bee_backend'); + const dotBeeBackend = path.join(dataPath, '.bee_backend'); + if (fs.existsSync(beeBackend) && !fs.existsSync(dotBeeBackend)) { + this.logger.debug(`Patching Unity Linux Editor for Bee Backend...`); + await fs.promises.rename(beeBackend, dotBeeBackend); + const wrapperSource: string = `#!/bin/bash +# https://discussions.unity.com/t/linux-editor-stuck-on-loading-because-of-bee-backend-w-workaround/854480 +set -e +args=("$@") +for ((i = 0; i < "\${#args[@]}"; ++i)); do + case \${args[i]} in + --stdin-canary) + unset "args[i]" + break + ;; + esac +done +"$(dirname "$0")/.$(basename "$0")" "\${args[@]}"` + await fs.promises.writeFile(beeBackend, wrapperSource, { encoding: 'utf-8', mode: 0o755 }); + } + } + } + + private async getEditorReleaseInfo(unityVersion: UnityVersion): Promise { + // Prefer querying the releases API with the exact fully-qualified Unity version (e.g., 2022.3.10f1). + // If we don't have a fully-qualified version, use the most specific prefix available: + // - "YYYY.M" when provided (e.g., 6000.1) + // - otherwise "YYYY" + const fullUnityVersionPattern = /^\d{1,4}\.\d+\.\d+[abcfpx]\d+$/; + let version: string; + if (fullUnityVersionPattern.test(unityVersion.version)) { + version = unityVersion.version; + } else { + const mm = unityVersion.version.match(/^(\d{1,4})(?:\.(\d+))?/); + if (mm) { + version = mm[2] ? `${mm[1]}.${mm[2]}` : mm[1]!; + } else { + version = unityVersion.version.split('.')[0]!; + } + } + + const releasesClient = new UnityReleasesClient(); + + function getPlatform(): Array<('MAC_OS' | 'LINUX' | 'WINDOWS')> { + switch (process.platform) { + case 'darwin': + return ['MAC_OS']; + case 'linux': + return ['LINUX']; + case 'win32': + return ['WINDOWS']; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } + + const request: GetUnityReleasesData = { + query: { + version: version, + architecture: [unityVersion.architecture], + platform: getPlatform(), + limit: 1, + } + }; + + this.logger.debug(`Get Unity Release: ${JSON.stringify(request, null, 2)}`); + const { data, error } = await releasesClient.api.ReleaseService.getUnityReleases(request); + + if (error) { + throw new Error(`Failed to get Unity releases: ${JSON.stringify(error, null, 2)}`); + } + + if (!data || !data.results || data.results.length === 0) { + throw new Error(`No Unity releases found for version: ${version}`); + } + + this.logger.debug(`Found Unity Release: ${JSON.stringify(data, null, 2)}`); + // Filter to stable 'f' releases only unless the user explicitly asked for a pre-release + const isExplicitPrerelease = /[abcpx]$/.test(unityVersion.version) || /[abcpx]/.test(unityVersion.version); + const results = (data.results || []) + .filter(r => isExplicitPrerelease ? true : /f\d+$/.test(r.version)) + // Sort descending by minor, patch, f-number where possible; fallback to semver coercion + .sort((a, b) => { + const parse = (v: string) => { + const m = v.match(/(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)/); + return m ? [parseInt(m[2]!), parseInt(m[3]!), m[4], parseInt(m[5]!)] as [number, number, string, number] : [0, 0, 'f', 0] as [number, number, string, number]; + }; + const [aMinor, aPatch, aTag, aNum] = parse(a.version); + const [bMinor, bPatch, bTag, bNum] = parse(b.version); + // Prefer higher minor + if (aMinor !== bMinor) return bMinor - aMinor; + // Then higher patch + if (aPatch !== bPatch) return bPatch - aPatch; + // Tag order: f > p > c > b > a > x + const order = { f: 5, p: 4, c: 3, b: 2, a: 1, x: 0 } as Record; + if (order[aTag] !== order[bTag]) return (order[bTag] || 0) - (order[aTag] || 0); + return bNum - aNum; + }); + + if (results.length === 0) { + throw new Error(`No suitable Unity releases (stable) found for version: ${version}`); + } + + this.logger.debug(`Found Unity Release: ${JSON.stringify({ query: version, picked: results[0] }, null, 2)}`); + return results[0]!; + } + + private async fallbackVersionLookup(unityVersion: UnityVersion): Promise { + const url = `https://unity.com/releases/editor/whats-new/${unityVersion.version}`; + this.logger.debug(`Fetching release page: "${url}"`); + let response: Response; + + try { + response = await fetch(url); + } catch (error) { + this.logger.warn(`Failed to fetch changeset for Unity ${unityVersion.toString()}: ${error}`); + return unityVersion; + } + + const responseText = await response.text(); + + if (!response.ok) { + this.logger.info(responseText); + } + + if (!response.ok) { + throw new Error(`Failed to fetch changeset for Unity ${unityVersion.toString()} [${response.status}] "${url}"`); + } + + this.logger.debug(`Release page content: \n${responseText}`); + const match = responseText.match(/unityhub:\/\/(?\d+\.\d+\.\d+[abcfpx]?\d*)\/(?[a-zA-Z0-9]+)/); + + if (match && match.groups && match.groups.version && match.groups.changeset) { + return new UnityVersion(match.groups.version, match.groups.changeset, unityVersion.architecture); + } + + this.logger.error(`Failed to find changeset for Unity ${unityVersion.toString()}`); + return unityVersion; + } + + private async checkEditorModules(editorPath: string, unityVersion: UnityVersion, modules: string[]): Promise<[string[], string[]]> { + let args = ['install-modules', '--version', unityVersion.version]; + + if (unityVersion.architecture) { + args.push('-a', unityVersion.architecture.toLowerCase()); + } + + for (const module of modules) { + args.push('-m', module); + } + + const output = await this.Exec([...args, '--cm']); + const editorRootPath = await UnityEditor.GetEditorRootPath(editorPath); + const modulesPath = path.join(editorRootPath, 'modules.json'); + this.logger.debug(`Editor Modules Manifest:\n > "${modulesPath}"`); + const moduleMatches = output.matchAll(/Omitting module (?.+) because it's already installed/g); + + if (moduleMatches) { + const omittedModules = [...moduleMatches].map(match => match.groups?.module); + for (const module of omittedModules) { + if (module && !modules.includes(module)) { + modules.push(module); + } + } + } + + const installedModules = [...modules]; + const additionalModules = []; + const additionalModulesJson = await this.getModulesContent(modulesPath); + + if (additionalModulesJson.length > 0) { + for (const module of additionalModulesJson) { + if (module.category === "Platforms" && module.visible === true) { + if (!installedModules.includes(module.id)) { + additionalModules.push(module.id); + } + } + } + } + + return [installedModules, additionalModules]; + } + + private async getModulesContent(modulesPath: string): Promise { + const modulesContent = await ReadFileContents(modulesPath); + return JSON.parse(modulesContent); + } + + private async installUnity(unityVersion: UnityVersion, modules: string[]): Promise { + if (unityVersion.isLegacy()) { + return await this.installUnity4x(unityVersion); + } + + if (process.platform === 'linux') { + const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch; + + if (['2019.1', '2019.2'].some(v => unityVersion.version.startsWith(v))) { + const url = `https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`; + const downloadPath = path.join(process.env.TEMP || '.', `libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`); + await DownloadFile(url, downloadPath); + + try { + await Exec('sudo', ['dpkg', '-i', downloadPath]); + } finally { + fs.promises.unlink(downloadPath); + } + } else if (['2019.3', '2019.4'].some(v => unityVersion.version.startsWith(v)) || unityVersion.version.startsWith('2020.')) { + const url = `https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`; + const downloadPath = path.join(process.env.TEMP || '.', `libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`); + await DownloadFile(url, downloadPath); + + try { + await Exec('sudo', ['dpkg', '-i', downloadPath]); + } finally { + fs.promises.unlink(downloadPath); + } + } + } + + const args = ['install', '--version', unityVersion.version]; + + if (unityVersion.changeset) { + args.push('--changeset', unityVersion.changeset); + } + + if (unityVersion.architecture) { + args.push('-a', unityVersion.architecture.toLowerCase()); + } + + if (modules.length > 0) { + for (const module of modules) { + this.logger.info(` > with module: ${module}`); + args.push('-m', module); + } + + args.push('--cm'); + } + + this.logger.info(`Installing Unity ${unityVersion.toString()}...`); + const output = await this.Exec(args); + + if (output.includes(`Error while installing an editor or a module from changeset`)) { + throw new Error(`Failed to install Unity ${unityVersion.toString()}`); + } + } + + private async installUnity4x(unityVersion: UnityVersion): Promise { + const installDir = await this.GetInstallPath(); + + switch (process.platform) { + case 'win32': { + const installPath = path.join(installDir, `Unity ${unityVersion.version}`); + + if (!fs.existsSync(installPath)) { + const url = `https://beta.unity3d.com/download/UnitySetup-${unityVersion.version}.exe`; + const installerPath = path.join(process.env.TEMP || '.', `UnitySetup-${unityVersion.version}.exe`); + await DownloadFile(url, installerPath); + + this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); + + try { + await Exec(installerPath, ['/S', `/D=${installPath}`, '-Wait', '-NoNewWindow']); + } catch (error) { + this.logger.error(`Failed to install Unity ${unityVersion.toString()}: ${error}`); + } finally { + fs.promises.unlink(installerPath); + } + } + + await fs.promises.access(installPath, fs.constants.R_OK); + return installPath; + } + case 'darwin': { + const installPath = path.join(installDir, `Unity ${unityVersion.version}`, 'Unity.app'); + + if (!fs.existsSync(installPath)) { + const url = `https://beta.unity3d.com/download/unity-${unityVersion.version}.dmg`; + const installerPath = path.join(process.env.TEMP || '.', `UnitySetup-${unityVersion.version}.dmg`); + await DownloadFile(url, installerPath); + + this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); + + let mountPoint = ''; + + try { + const output = await Exec('hdiutil', ['attach', installerPath, '-nobrowse']); + const mountPointMatch = output.match(/\/Volumes\/Unity Installer.*$/m); + + if (!mountPointMatch || mountPointMatch.length === 0) { + throw new Error(`Failed to find mount point in hdiutil output: ${output}`); + } + + mountPoint = mountPointMatch[0]; + this.logger.debug(`Mounted Unity Installer at ${mountPoint}`); + + const pkgPath = path.join(mountPoint, 'Unity.pkg'); + await fs.promises.access(pkgPath, fs.constants.R_OK); + + this.logger.debug(`Found .pkg installer: ${pkgPath}`); + await Exec('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR']); + const unityAppPath = path.join('/Applications', 'Unity'); + const targetPath = path.join(installDir, `Unity ${unityVersion.version}`); + + if (fs.existsSync(unityAppPath)) { + this.logger.debug(`Moving ${unityAppPath} to ${targetPath}...`); + await fs.promises.mkdir(path.dirname(targetPath), { recursive: true }); + + const items = await fs.promises.readdir(unityAppPath); + + for (const item of items) { + if (item !== 'Hub') { + const src = path.join(unityAppPath, item); + const dest = path.join(targetPath, item); + this.logger.debug(` > Moving ${src} to ${dest}`); + await fs.promises.cp(src, dest, { recursive: true }); + await fs.promises.rm(src, { recursive: true, force: true }); + } + } + + await fs.promises.chmod(targetPath, 0o777); + } else { + throw new Error(`Failed to find Unity.app after installation.`); + } + } catch (error) { + this.logger.error(`Failed to mount Unity ${unityVersion.toString()} installer: ${error}`); + } finally { + try { + if (mountPoint && mountPoint.length > 0) { + await Exec('hdiutil', ['detach', mountPoint, '-quiet']); + } + } finally { + await fs.promises.unlink(installerPath); + } + } + } + + await fs.promises.access(installPath, fs.constants.R_OK); + return installPath; + } + default: + throw new Error(`Unity ${unityVersion.toString()} is not supported on ${process.platform}`); + } } } \ No newline at end of file diff --git a/src/unity-version.ts b/src/unity-version.ts new file mode 100644 index 0000000..26db39f --- /dev/null +++ b/src/unity-version.ts @@ -0,0 +1,164 @@ +import { + SemVer, + coerce, + compare, + satisfies +} from 'semver'; +import { Logger } from './logging'; +import { arch } from 'os'; + +export class UnityVersion { + public version: string; + public changeset: string | null | undefined; + public architecture: 'X86_64' | 'ARM64'; + + private semVer: SemVer; + private logger = Logger.instance; + + constructor( + version: string, + changeset: string | null | undefined = undefined, + architecture: 'X86_64' | 'ARM64' | undefined = undefined + ) { + this.version = version; + this.changeset = changeset; + + const coercedVersion = coerce(version); + + if (!coercedVersion) { + throw new Error(`Invalid Unity version: ${JSON.stringify(version)}`); + } + + this.semVer = coercedVersion; + + // Default to current architecture if not specified + architecture = architecture || (arch() === 'arm64' ? 'ARM64' : 'X86_64'); + + if (architecture === 'ARM64' && !this.isArmCompatible()) { + this.architecture = 'X86_64'; + } else { + this.architecture = architecture; + } + } + + static compare(a: UnityVersion, b: UnityVersion): number { + // Compare using coerced SemVer objects to handle partial inputs (e.g., "2022") safely + return compare(a.semVer, b.semVer, true); + } + + toString(): string { + return this.changeset ? `${this.version} (${this.changeset})` : this.version; + } + + isLegacy(): boolean { + return this.semVer.major <= 4; + } + + isArmCompatible(): boolean { + if (this.semVer.major < 2021) { return false; } + return compare(this.semVer, '2021.0.0', true) >= 0; + } + + findMatch(versions: string[]): UnityVersion { + const fullPattern = /^\d{1,4}\.\d+\.\d+[abcfpx]\d+$/; + const exactMatch = versions.find(release => { + // Only match fully formed Unity versions (e.g., 2021.3.5f1, 2022.1.0b12) + const match = release.match(/(?\d{1,4}\.\d+\.\d+[abcfpx]\d+)/); + return match && match.groups && match.groups.version === this.version; + }); + + if (exactMatch) { + this.logger.debug(`Exact match found for ${this.version}`); + return new UnityVersion(this.version, null, this.architecture); + } + + // Trigger fallback for any non fully-qualified version or wildcard patterns + // e.g., "6000", "6000.1", "6000.0.0", "2022.x", "6000.*" + const hasWildcard = /\.x($|[^\w])/.test(this.version) || /\.\*($|[^\w])/.test(this.version); + const triggerFallback = hasWildcard || !fullPattern.test(this.version); + + if (triggerFallback) { + // Determine major/minor for fallback, supporting wildcards and partials + let major: string; + let minor: string; + const xMatch = this.version.match(/^(\d{1,4})(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/); + + if (xMatch) { + major = xMatch[1]!; + minor = xMatch[2]!; + } + + let releases = versions + .map(release => { + const match = release.match(/(?\d{1,4}\.\d+\.\d+[abcfpx]\d+)/); + return match && match.groups ? match.groups.version : null; + }) + .filter(Boolean) + .filter(version => { + if (!version) { return false; } + const parts = version.match(/(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)/); + if (!parts || parts[4] !== 'f') { return false; } + // major must match + if (major && parts[1] !== major) { return false; } + // minor: if 'x' or '*', allow any, else must match + if (minor && minor !== 'x' && minor !== '*' && parts[2] !== minor) { return false; } + return true; + }); + + // If no matches and requested minor was explicitly '0', broaden to any minor for that major + if (releases.length === 0 && minor! === '0') { + releases = versions + .map(release => { + const match = release.match(/(?\d{1,4}\.\d+\.\d+[abcfpx]\d+)/); + return match && match.groups ? match.groups.version : null; + }) + .filter(Boolean) + .filter(version => { + if (!version) { return false; } + const parts = version.match(/(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)/); + if (!parts || parts[4] !== 'f') { return false; } + if (major && parts[1] !== major) { return false; } + return true; // ignore minor + }); + } + + // Sort by minor, patch, and f number descending + releases.sort((a, b) => { + const parse = (v: string) => { + const match = v.match(/(\d{1,4})\.(\d+)\.(\d+)([abcfpx])(\d+)/); + return match ? [parseInt(match[2]!), parseInt(match[3]!), parseInt(match[5]!)] : [0, 0, 0]; + }; + + const [aMinor, aPatch, af] = parse(a!); + const [bMinor, bPatch, bf] = parse(b!); + + if (aMinor !== bMinor) { return bMinor! - aMinor!; } + if (aPatch !== bPatch) { return bPatch! - aPatch!; } + return bf! - af!; + }); + + this.logger.debug(`Searching for fallback match for ${this.version}:`); + releases.forEach(version => { + this.logger.debug(` > ${version}`); + }); + + if (releases.length > 0) { + this.logger.debug(`Found fallback Unity ${releases[0]}`); + return new UnityVersion(releases[0]!, null, this.architecture); + } + } + + this.logger.debug(`No matching Unity version found for ${this.version}`); + return this; + } + + satisfies(version: string): boolean { + const coercedVersion = coerce(version); + + if (!coercedVersion) { + throw new Error(`Invalid version to check against: ${version}`); + } + + return satisfies(coercedVersion, `^${this.semVer}`); + } +} \ No newline at end of file diff --git a/src/utilities.ts b/src/utilities.ts index 862b5ea..9802b76 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -5,14 +5,25 @@ import * as fs from 'fs'; import * as https from 'https'; import * as readline from 'readline'; import { spawn } from 'child_process'; +import { Logger } from './logging'; -export async function ResolveGlobPath(globs: string[]): Promise { +const logger = Logger.instance; + +/** + * Resolves a glob pattern to this first file path that matches it. + * @param globs An array of path segments that may include glob patterns. + * @returns The first matching file path. + */ +export async function ResolveGlobToPath(globs: string[]): Promise { const globPath: string = path.join(...globs).split(path.sep).join('/'); - const files: string[] = await glob.glob(globPath, { nodir: true }); + logger.debug(`glob: ${globPath}`); + const paths: string[] = await glob.glob(globPath, { nodir: true }); + + logger.debug(`Resolved "${globPath}" to ${paths.length} paths:\n > ${paths.join('\n > ')}`); - for (const file of files) { - await fs.promises.access(file, fs.constants.R_OK); - return file; + for (const path of paths) { + await fs.promises.access(path, fs.constants.R_OK); + return path; } throw new Error(`No accessible file found for glob pattern: ${globPath}`); @@ -32,30 +43,37 @@ export async function PromptForSecretInput(prompt: string): Promise { readline.clearLine(process.stdout, 0); process.stdout.write(prompt + '*'.repeat(input.length) + '\n'); rl.close(); - console.log(); // Move to next line after input + console.log(); // Don't use logger. Move to next line after input. resolve(input); }); }); } -export async function Exec(command: string, args: string[]): Promise { +export async function Exec(command: string, args: string[], options: { silent: boolean, showCommand: boolean } = { silent: false, showCommand: true }): Promise { let output: string = ''; let exitCode: number = 0; try { exitCode = await new Promise((resolve, reject) => { + if (options.showCommand) { + logger.info(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); + } + const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); child.stdout.on('data', (data) => { const chunk = data.toString(); output += chunk; - console.log(chunk); + + if (!options.silent) { + logger.info(chunk); + } }); child.stderr.on('data', (data) => { const chunk = data.toString(); output += chunk; - console.error(chunk); + logger.error(chunk); }); child.on('error', (error) => { @@ -76,7 +94,7 @@ export async function Exec(command: string, args: string[]): Promise { } export async function DownloadFile(url: string, downloadPath: string): Promise { - console.log(`Downloading from ${url} to ${downloadPath}`); + logger.debug(`Downloading from ${url} to ${downloadPath}...`); await new Promise((resolve, reject) => { const file = fs.createWriteStream(downloadPath); https.get(url, (response) => { @@ -90,4 +108,21 @@ export async function DownloadFile(url: string, downloadPath: string): Promise { + logger.debug(`Attempting to delete directory: ${targetPath}...`); + if (targetPath && targetPath.length > 0 && fs.existsSync(targetPath)) { + await fs.promises.rm(targetPath, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 }); + } +} + +export async function ReadFileContents(filePath: string): Promise { + const fileHandle = await fs.promises.open(filePath, 'r'); + try { + const projectSettingsContent = await fileHandle.readFile('utf8'); + return projectSettingsContent; + } finally { + await fileHandle.close(); + } } \ No newline at end of file diff --git a/tests/basic.test.js.map b/tests/basic.test.js.map deleted file mode 100644 index 09f9f84..0000000 --- a/tests/basic.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"basic.test.js","sourceRoot":"","sources":["basic.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAErD,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file From addcea9514375f1ad3bd493fa8fa880710ef9957 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 18 Sep 2025 18:11:12 -0400 Subject: [PATCH 13/90] tweaks --- src/index.ts | 28 ++++++++++++++++++++-------- src/unity-hub.ts | 8 +++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index c05a56d..264237a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,20 +83,27 @@ program.command('return-license') program.command('hub-version') .description('Print the version of the Unity Hub.') .action(async () => { - const hub = new UnityHub(); - await hub.Version(); + const unityHub = new UnityHub(); + await unityHub.Version(); }); program.command('hub-install') .description('Install the Unity Hub.') .option('--verbose', 'Enable verbose logging.') + .option('--json', 'Prints the last line of output as JSON string.') .action(async (options) => { if (options.verbose) { Logger.instance.logLevel = LogLevel.DEBUG; } - const hub = new UnityHub(); - await hub.Install(); + const unityHub = new UnityHub(); + const hubPath = await unityHub.Install(); + + if (options.json) { + process.stdout.write(JSON.stringify({ UNITY_HUB: hubPath })); + } else { + process.stdout.write(hubPath); + } }); program.command('hub-path') @@ -109,8 +116,8 @@ program.command('hub-path') program.command('hub') .description('Run commands directly to the Unity Hub. (You need not to pass --headless or -- to this command).') .allowUnknownOption(true) - .option('--verbose', 'Enable verbose logging.') .argument('', 'Arguments to pass to the Unity Hub executable.') + .option('--verbose', 'Enable verbose logging.') .action(async (args: string[], options) => { if (options.verbose) { Logger.instance.logLevel = LogLevel.DEBUG; @@ -120,12 +127,17 @@ program.command('hub') await hub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); }); -program.command('hub-get-editor') - .description('Attempts to find or install the specified Unity Editor version.') - .option('-u, --unity-version ', 'The Unity version to get (e.g. 2020.3.1f1, 2021.x, 2022.1.*, 6000).') +program.command('setup-unity') + .description('Sets up the environment for the specified project and finds or installs the Unity Editor version for it.') + .option('-p, --unity-project ', 'The path to a Unity project or "none" to skip project detection.') + .option('-u, --unity-version ', 'The Unity version to get (e.g. 2020.3.1f1, 2021.x, 2022.1.*, 6000). If specified, it will override the version read from the project.') .option('-c, --changeset ', 'The Unity changeset to get (e.g. 1234567890ab).') + .option('-a, --arch ', 'The Unity architecture to get (e.g. x86_64, arm64). Defaults to the architecture of the current process.') + .option('-b, --build-targets ', 'The Unity build target to get (e.g. iOS, Android).') .option('-m, --modules ', 'The Unity module to get (e.g. ios, android).') + .option('-i, --install-path ', 'The path to install the Unity Editor to. By default, it will be installed to the default Unity Hub location.') .option('--verbose', 'Enable verbose logging.') + .option('--json', 'Prints the last line of output as JSON string.') .action(async (options) => { if (options.verbose) { Logger.instance.logLevel = LogLevel.DEBUG; diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 43ccbee..5f6ec91 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -157,7 +157,7 @@ export class UnityHub { * Installs or updates the Unity Hub. * If the Unity Hub is already installed, it will be updated to the latest version. */ - public async Install(): Promise { + public async Install(): Promise { let isInstalled = false; try { await fs.promises.access(this.executable, fs.constants.X_OK); @@ -193,13 +193,11 @@ sudo apt-get update --allow-releaseinfo-change sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); } } else { - this.logger.info(`Unity Hub is up to date.`); + this.logger.info(`Unity Hub is already installed and up to date.`); } } - this.logger.info(`Unity Hub Path:\n > ${this.executable}`); - - await this.Exec(['help']); + return this.executable; } private async installHub(): Promise { From 3a46459a0e0d51808f6a0d57e1f0e6a3d24ae93a Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 18 Sep 2025 22:44:40 -0400 Subject: [PATCH 14/90] add unity-project standardize outputs with --json output --- src/android-sdk.ts | 19 +++++++++++-- src/index.ts | 30 ++++++++++++++++++-- src/unity-hub.ts | 16 +++++++---- src/unity-project.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++ tests/basic.test.js | 7 ----- 5 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 src/unity-project.ts delete mode 100644 tests/basic.test.js diff --git a/src/android-sdk.ts b/src/android-sdk.ts index ad53893..e01a1d2 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -1,14 +1,27 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { UnityEditor } from './unity-editor'; -import { ReadFileContents, ResolveGlobToPath } from './utilities'; -import { Logger, LogLevel } from './logging'; import { spawn } from 'child_process'; +import { UnityEditor } from './unity-editor'; +import { + ReadFileContents, + ResolveGlobToPath +} from './utilities'; +import { + Logger, + LogLevel +} from './logging'; const logger = Logger.instance; +/** + * Checks if the required Android SDK is installed for the given Unity Editor and Project. + * @param editorPath The path to the Unity Editor executable. + * @param projectPath The path to the Unity project. + * @returns A promise that resolves when the check is complete. + */ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: string): Promise { + logger.debug(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`); let sdkPath = undefined; await createRepositoryCfg(); const rootEditorPath = await UnityEditor.GetEditorRootPath(editorPath); diff --git a/src/index.ts b/src/index.ts index 264237a..1535c72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,9 @@ import { PromptForSecretInput } from './utilities'; import { UnityHub } from './unity-hub'; import { Logger, LogLevel } from './logging'; import { UnityVersion } from './unity-version'; +import { UnityProject } from './unity-project'; +import { UnityEditor } from './unity-editor'; +import { CheckAndroidSdkInstalled } from './android-sdk'; const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); @@ -151,10 +154,31 @@ program.command('setup-unity') const unityVersion = new UnityVersion(options.unityVersion, options.changeset); const modules: string[] = options.modules ? options.modules.split(',').split(' ') : []; - const hub = new UnityHub(); + const unityHub = new UnityHub(); + + const output: { [key: string]: string } = {}; + + output['UNITY_HUB_PATH'] = unityHub.executable; + + const editorPath = await unityHub.GetEditor(unityVersion, modules); + + output['UNITY_EDITOR'] = editorPath; + + let unityProject: UnityProject | undefined; + + if (options.unityProject) { + unityProject = await UnityProject.GetProject(options.unityProject); + + if (unityProject) { + output['UNITY_PROJECT'] = unityProject.projectPath; + + if (modules.includes('android')) { + await CheckAndroidSdkInstalled(editorPath, unityProject.projectPath); + } + } + } - const editorPath = await hub.GetEditor(unityVersion, modules); - process.stdout.write(editorPath); + process.stdout.write(JSON.stringify(output)); }); program.parse(process.argv); diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 5f6ec91..e87a991 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -32,10 +32,10 @@ export class UnityHub { constructor() { switch (process.platform) { case 'win32': - this.executable = process.env.UNITY_HUB_PATH || 'C:/Program Files/Unity Hub/Unity Hub.exe'; + this.executable = process.env.UNITY_HUB_PATH || 'C:\\Program Files\\Unity Hub\\Unity Hub.exe'; this.rootDirectory = path.join(this.executable, '../'); - this.editorInstallationDirectory = 'C:/Program Files/Unity/Hub/Editor/'; - this.editorFileExtension = '/Editor/Unity.exe'; + this.editorInstallationDirectory = 'C:\\Program Files\\Unity\\Hub\\Editor\\'; + this.editorFileExtension = '\\Editor\\Unity.exe'; break; case 'darwin': this.executable = process.env.UNITY_HUB_PATH || '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'; @@ -467,7 +467,7 @@ chmod -R 777 "$hubPath"`]); } } - return editorPath; + return path.normalize(editorPath); } /** @@ -475,7 +475,13 @@ chmod -R 777 "$hubPath"`]); * @returns A list of installed Unity Editor versions and their paths. */ public async ListInstalledEditors(): Promise { - return (await this.Exec(['editors', '-i'], { silent: this.logger.logLevel !== LogLevel.DEBUG, showCommand: this.logger.logLevel === LogLevel.DEBUG })).split('\n').filter(line => line.trim().length > 0).map(line => line.trim()); + const output = await this.Exec(['editors', '-i'], { + silent: this.logger.logLevel !== LogLevel.DEBUG, + showCommand: this.logger.logLevel === LogLevel.DEBUG + }); + return output.split('\n') + .filter(line => line.trim().length > 0) + .map(line => line.trim()); } private async checkInstalledEditors(unityVersion: UnityVersion, failOnEmpty: boolean, installPath: string | undefined = undefined): Promise { diff --git a/src/unity-project.ts b/src/unity-project.ts new file mode 100644 index 0000000..1658889 --- /dev/null +++ b/src/unity-project.ts @@ -0,0 +1,67 @@ +import os from 'os'; +import fs from 'fs'; +import path from 'path'; +import { Logger } from './logging'; +import { ResolveGlobToPath } from './utilities'; + +export class UnityProject { + public static readonly DefaultModules: string[] = (() => { + switch (os.type()) { + case 'Linux': return ['linux-il2cpp']; + case 'Darwin': return ['mac-il2cpp']; + case 'Windows_NT': return ['windows-il2cpp']; + default: throw Error(`${os.type()} not supported`); + } + })(); + + public static readonly BuildTargetModuleMap: { [key: string]: string } = (() => { + switch (os.type()) { + case 'Linux': return { + StandaloneLinux64: "linux-il2cpp", + Android: "android", + WebGL: "webgl", + iOS: "ios", + }; + case 'Darwin': return { + StandaloneOSX: "mac-il2cpp", + iOS: "ios", + Android: "android", + tvOS: "appletv", + StandaloneLinux64: "linux-il2cpp", + WebGL: "webgl", + VisionOS: "visionos" + }; + case 'Windows_NT': return { + StandaloneWindows64: "windows-il2cpp", + WSAPlayer: "universal-windows-platform", + Android: "android", + iOS: "ios", + tvOS: "appletv", + StandaloneLinux64: "linux-il2cpp", + Lumin: "lumin", + WebGL: "webgl", + }; + default: throw Error(`${os.type()} not supported`); + } + })(); + + private logger: Logger = Logger.instance; + + public readonly projectVersionPath: string; + + constructor(public readonly projectPath: string) { + fs.accessSync(projectPath, fs.constants.R_OK); + this.projectVersionPath = path.join(this.projectPath, 'ProjectSettings', 'ProjectVersion.txt'); + fs.accessSync(this.projectVersionPath, fs.constants.R_OK); + } + + public static async GetProject(projectPath: string | undefined = undefined): Promise { + if (!projectPath) { + projectPath = process.cwd(); + const versionFilePath = await ResolveGlobToPath([projectPath, '**', 'ProjectVersion.txt']); + projectPath = path.join(versionFilePath, '..', '..'); + } + + return new UnityProject(projectPath); + } +} \ No newline at end of file diff --git a/tests/basic.test.js b/tests/basic.test.js deleted file mode 100644 index bf0a0d9..0000000 --- a/tests/basic.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from '@jest/globals'; -describe('unity-cli basic test', () => { - it('should pass a simple test', () => { - expect(true).toBe(true); - }); -}); -//# sourceMappingURL=basic.test.js.map \ No newline at end of file From 802ac3ee1f660607be40c600c6c8ea384d19ccc4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 09:03:35 -0400 Subject: [PATCH 15/90] update unit tests --- tests/basic.test.ts | 7 ------- tests/cli-options.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) delete mode 100644 tests/basic.test.ts create mode 100644 tests/cli-options.test.ts diff --git a/tests/basic.test.ts b/tests/basic.test.ts deleted file mode 100644 index ac3ecc9..0000000 --- a/tests/basic.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from '@jest/globals'; - -describe('unity-cli basic test', () => { - it('should pass a simple test', () => { - expect(true).toBe(true); - }); -}); diff --git a/tests/cli-options.test.ts b/tests/cli-options.test.ts new file mode 100644 index 0000000..9454cd3 --- /dev/null +++ b/tests/cli-options.test.ts @@ -0,0 +1,28 @@ +import { execSync } from 'child_process'; + +describe('unity-cli CLI options', () => { + const runCli = (args: string) => { + try { + return execSync(`unity-cli ${args}`, { encoding: 'utf-8' }); + } catch (error: any) { + return error.stdout || error.message; + } + }; + + it('should show help with --help', () => { + const output = runCli('--help'); + expect(output).toMatch(/Usage|Help|Options/i); + }); + + it('should show version with --version', () => { + const output = runCli('--version'); + expect(output).toMatch(/\d+\.\d+\.\d+/); + }); + + // Add more tests for each CLI option as implemented + // Example: + // it('should handle --some-option', () => { + // const output = runCli('--some-option'); + // expect(output).toContain('expected output'); + // }); +}); From 0ff05be3cdd2b339f53b30f85e0cd1b5a9f7ff6e Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 09:05:46 -0400 Subject: [PATCH 16/90] update package name to include namespace --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bf8896..c41e261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "unity-cli", + "name": "@rageagainstthepixel/unity-cli", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "unity-cli", + "name": "@rageagainstthepixel/unity-cli", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -5064,4 +5064,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index beb89ed..2f7cc5d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "unity-cli", + "name": "@rageagainstthepixel/unity-cli", "version": "1.0.0", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", From 007eea44cfe193e93591b4e20dbadd1b226761d4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 09:19:46 -0400 Subject: [PATCH 17/90] fix namespace update deps --- package-lock.json | 66 +++++++++++++++++++++++------------------------ package.json | 10 +++---- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index c41e261..8f731d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@rageagainstthepixel/unity-cli", + "name": "@rage-against-the-pixel/unity-cli", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@rageagainstthepixel/unity-cli", + "name": "@rage-against-the-pixel/unity-cli", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -22,10 +22,10 @@ "devDependencies": { "@types/commander": "^2.12.0", "@types/jest": "^30.0.0", - "@types/node": "^24.4.0", - "@types/semver": "^7.7.0", - "jest": "^30.1.1", - "ts-jest": "^29.4.1", + "@types/node": "^24.5.2", + "@types/semver": "^7.7.1", + "jest": "^30.1.3", + "ts-jest": "^29.4.3", "ts-node": "^10.9.2", "typescript": "^5.9.2" } @@ -1381,13 +1381,13 @@ } }, "node_modules/@types/node": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz", - "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.11.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/semver": { @@ -1906,9 +1906,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", - "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1939,9 +1939,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, "funding": [ { @@ -1959,7 +1959,7 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", @@ -2023,9 +2023,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "dev": true, "funding": [ { @@ -2321,9 +2321,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", "dev": true, "license": "ISC" }, @@ -2347,9 +2347,9 @@ "license": "MIT" }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4533,9 +4533,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.3.tgz", + "integrity": "sha512-KTWbK2Wot8VXargsLoxhSoEQ9OyMdzQXQoUDeIulWu2Tf7gghuBHeg+agZqVLdTOHhQHVKAaeuctBDRkhWE7hg==", "dev": true, "license": "MIT", "dependencies": { @@ -4702,9 +4702,9 @@ } }, "node_modules/undici-types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", - "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "dev": true, "license": "MIT" }, @@ -5064,4 +5064,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 2f7cc5d..928c3ed 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@rageagainstthepixel/unity-cli", + "name": "@rage-against-the-pixel/unity-cli", "version": "1.0.0", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", @@ -36,10 +36,10 @@ "devDependencies": { "@types/commander": "^2.12.0", "@types/jest": "^30.0.0", - "@types/node": "^24.4.0", - "@types/semver": "^7.7.0", - "jest": "^30.1.1", - "ts-jest": "^29.4.1", + "@types/node": "^24.5.2", + "@types/semver": "^7.7.1", + "jest": "^30.1.3", + "ts-jest": "^29.4.3", "ts-node": "^10.9.2", "typescript": "^5.9.2" } From 7fb40681412efe1418356ea0c4593fa3dbff0b82 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 10:33:35 -0400 Subject: [PATCH 18/90] updated trace logging fix glob lookup for android sdk --- package-lock.json | 21 +++++++++++++++------ package.json | 3 ++- src/android-sdk.ts | 2 +- src/index.ts | 29 +++++++++++++++-------------- src/unity-project.ts | 18 ++++++++++++++++++ src/utilities.ts | 4 ++-- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f731d3..30582d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "commander": "^14.0.1", "glob": "^11.0.3", "semver": "^7.7.2", + "source-map-support": "^0.5.21", "yaml": "^2.8.1" }, "bin": { @@ -1999,7 +2000,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/callsites": { @@ -3331,6 +3331,17 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/jest-runtime": { "version": "30.1.3", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", @@ -4219,17 +4230,15 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", diff --git a/package.json b/package.json index 928c3ed..f7a5eab 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "commander": "^14.0.1", "glob": "^11.0.3", "semver": "^7.7.2", + "source-map-support": "^0.5.21", "yaml": "^2.8.1" }, "devDependencies": { @@ -43,4 +44,4 @@ "ts-node": "^10.9.2", "typescript": "^5.9.2" } -} \ No newline at end of file +} diff --git a/src/android-sdk.ts b/src/android-sdk.ts index e01a1d2..403fdf6 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -102,7 +102,7 @@ async function getSdkManager(rootEditorPath: string): Promise { async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise { logger.debug(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); - const sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'AndroidPlayer', '**', `android-${androidTargetSdk}`]); + const sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); try { await fs.promises.access(sdkPath, fs.constants.R_OK); } catch (error) { diff --git a/src/index.ts b/src/index.ts index 1535c72..8086cd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import 'source-map-support/register'; import { Command } from 'commander'; import { readFileSync } from 'fs'; import { join } from 'path'; @@ -9,7 +10,6 @@ import { UnityHub } from './unity-hub'; import { Logger, LogLevel } from './logging'; import { UnityVersion } from './unity-version'; import { UnityProject } from './unity-project'; -import { UnityEditor } from './unity-editor'; import { CheckAndroidSdkInstalled } from './android-sdk'; const pkgPath = join(__dirname, '..', 'package.json'); @@ -148,12 +148,19 @@ program.command('setup-unity') Logger.instance.debug(JSON.stringify(options)); - if (!options.unityVersion) { + + let unityProject: UnityProject | undefined; + + if (options.unityProject) { + unityProject = await UnityProject.GetProject(options.unityProject); + } + + if (!options.unityVersion && !unityProject) { throw new Error('You must specify a Unity version with -u or --unity-version.'); } - const unityVersion = new UnityVersion(options.unityVersion, options.changeset); - const modules: string[] = options.modules ? options.modules.split(',').split(' ') : []; + const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); + const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; const unityHub = new UnityHub(); const output: { [key: string]: string } = {}; @@ -164,17 +171,11 @@ program.command('setup-unity') output['UNITY_EDITOR'] = editorPath; - let unityProject: UnityProject | undefined; - - if (options.unityProject) { - unityProject = await UnityProject.GetProject(options.unityProject); - - if (unityProject) { - output['UNITY_PROJECT'] = unityProject.projectPath; + if (unityProject) { + output['UNITY_PROJECT'] = unityProject.projectPath; - if (modules.includes('android')) { - await CheckAndroidSdkInstalled(editorPath, unityProject.projectPath); - } + if (modules.includes('android')) { + await CheckAndroidSdkInstalled(editorPath, unityProject.projectPath); } } diff --git a/src/unity-project.ts b/src/unity-project.ts index 1658889..5a53276 100644 --- a/src/unity-project.ts +++ b/src/unity-project.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import { Logger } from './logging'; import { ResolveGlobToPath } from './utilities'; +import { UnityVersion } from './unity-version'; export class UnityProject { public static readonly DefaultModules: string[] = (() => { @@ -48,11 +49,28 @@ export class UnityProject { private logger: Logger = Logger.instance; public readonly projectVersionPath: string; + public readonly version: UnityVersion; constructor(public readonly projectPath: string) { fs.accessSync(projectPath, fs.constants.R_OK); this.projectVersionPath = path.join(this.projectPath, 'ProjectSettings', 'ProjectVersion.txt'); fs.accessSync(this.projectVersionPath, fs.constants.R_OK); + const versionText = fs.readFileSync(this.projectVersionPath, 'utf-8'); + const match = versionText.match(/m_EditorVersionWithRevision: (?(?:(?\d+)\.)?(?:(?\d+)\.)?(?:(?\d+[abcfpx]\d+)\b))\s?(?:\((?\w+)\))?/); + + if (!match) { + throw Error(`No version match found!`); + } + + if (!match.groups?.version) { + throw Error(`No version group found!`); + } + + if (!match.groups?.changeset) { + throw Error(`No changeset group found!`); + } + + this.version = new UnityVersion(match.groups.version, match.groups.changeset, undefined); } public static async GetProject(projectPath: string | undefined = undefined): Promise { diff --git a/src/utilities.ts b/src/utilities.ts index 9802b76..796c055 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -17,7 +17,7 @@ const logger = Logger.instance; export async function ResolveGlobToPath(globs: string[]): Promise { const globPath: string = path.join(...globs).split(path.sep).join('/'); logger.debug(`glob: ${globPath}`); - const paths: string[] = await glob.glob(globPath, { nodir: true }); + const paths: string[] = await glob.glob(globPath); logger.debug(`Resolved "${globPath}" to ${paths.length} paths:\n > ${paths.join('\n > ')}`); @@ -26,7 +26,7 @@ export async function ResolveGlobToPath(globs: string[]): Promise { return path; } - throw new Error(`No accessible file found for glob pattern: ${globPath}`); + throw new Error(`No accessible file found for glob pattern: ${path.normalize(globPath)}`); } export async function PromptForSecretInput(prompt: string): Promise { From 768e5f8ee4b729f9a179098341d183a5ce27a2c4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 13:30:54 -0400 Subject: [PATCH 19/90] cleanup and refactoring --- src/android-sdk.ts | 4 +- src/index.ts | 6 +-- src/license-client.ts | 24 +++++------ src/unity-editor.ts | 2 - src/unity-hub.ts | 94 ++++++++++++++++--------------------------- src/utilities.ts | 37 +++++++++-------- 6 files changed, 71 insertions(+), 96 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 403fdf6..170c969 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -33,7 +33,6 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: if (androidTargetSdk === undefined || androidTargetSdk === 0) { return; } - logger.debug('Validating Android Target SDK Installed...'); sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); if (sdkPath) { @@ -103,13 +102,14 @@ async function getSdkManager(rootEditorPath: string): Promise { async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise { logger.debug(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); const sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); + try { await fs.promises.access(sdkPath, fs.constants.R_OK); } catch (error) { logger.debug(`android-${androidTargetSdk} not installed`); return undefined; } - logger.debug(`sdkPath:\n > "${sdkPath}"`); + return sdkPath; } diff --git a/src/index.ts b/src/index.ts index 8086cd1..716b422 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,7 +87,8 @@ program.command('hub-version') .description('Print the version of the Unity Hub.') .action(async () => { const unityHub = new UnityHub(); - await unityHub.Version(); + const version = await unityHub.Version(); + process.stdout.write(version); }); program.command('hub-install') @@ -148,7 +149,6 @@ program.command('setup-unity') Logger.instance.debug(JSON.stringify(options)); - let unityProject: UnityProject | undefined; if (options.unityProject) { @@ -156,7 +156,7 @@ program.command('setup-unity') } if (!options.unityVersion && !unityProject) { - throw new Error('You must specify a Unity version with -u or --unity-version.'); + throw new Error('You must specify a Unity version or project with -u, --unity-version, -p, --unity-project.'); } const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); diff --git a/src/license-client.ts b/src/license-client.ts index 6c0eee0..f7e2e29 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { spawn } from 'child_process'; import { Logger } from './logging'; import { UnityHub } from './unity-hub'; -import { ResolveGlobToPath } from './utilities'; +import { ExecOptions, ResolveGlobToPath } from './utilities'; export enum LicenseType { personal = 'personal', @@ -227,27 +227,25 @@ export class LicensingClient { this.licenseClientPath = await this.init(); } - await fs.promises.access(this.licenseClientPath, fs.constants.X_OK); + await fs.promises.access(this.licenseClientPath!, fs.constants.R_OK | fs.constants.X_OK); let output: string = ''; let exitCode: number = 0; + function processOutput(data: Buffer) { + const chunk = data.toString(); + output += chunk; + process.stdout.write(chunk); + } + try { exitCode = await new Promise((resolve, reject) => { this.logger.info(`\x1b[34m${this.licenseClientPath} ${args.join(' ')}\x1b[0m`); - const child = spawn(this.licenseClientPath!, args, { stdio: ['ignore', 'pipe', 'pipe'] }); - child.stdout.on('data', (data) => { - const chunk = data.toString(); - output += chunk; - this.logger.info(chunk); - }); + const child = spawn(this.licenseClientPath!, args, { stdio: ['ignore', 'pipe', 'pipe'] }); - child.stderr.on('data', (data) => { - const chunk = data.toString(); - output += chunk; - this.logger.error(chunk); - }); + child.stdout.on('data', processOutput); + child.stderr.on('data', processOutput); child.on('error', (error) => { reject(error); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index b4b47e2..f069ffc 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -6,7 +6,6 @@ export class UnityEditor { private static logger: Logger = Logger.instance; static async GetEditorRootPath(editorPath: string): Promise { - this.logger.debug(`searching for editor root path: ${editorPath}`); let editorRootPath = editorPath; switch (process.platform) { case 'darwin': @@ -20,7 +19,6 @@ export class UnityEditor { break } await fs.promises.access(editorRootPath, fs.constants.R_OK); - this.logger.debug(`found editor root path: ${editorRootPath}`); return editorRootPath; } } \ No newline at end of file diff --git a/src/unity-hub.ts b/src/unity-hub.ts index e87a991..0a39dd4 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -14,6 +14,7 @@ import { DeleteDirectory, DownloadFile, Exec, + ExecOptions, ReadFileContents } from './utilities'; import { UnityVersion } from './unity-version'; @@ -60,38 +61,20 @@ export class UnityHub { * @param silent If true, suppresses output logging. * @returns The output from the command. */ - public async Exec(args: string[], options: { silent: boolean, showCommand: boolean } = { silent: false, showCommand: true }): Promise { + public async Exec(args: string[], options: ExecOptions = { silent: this.logger.logLevel !== LogLevel.DEBUG, showCommand: this.logger.logLevel === LogLevel.DEBUG }): Promise { await fs.promises.access(this.executable, fs.constants.X_OK); let output: string = ''; let exitCode: number = 0; - // These lines are commonly found in stderr but can be ignored - const ignoredLines = [ - `This error originated either by throwing inside of an async function without a catch block`, - `Unexpected error attempting to determine if executable file exists`, - `dri3 extension not supported`, - `Failed to connect to the bus:`, - `Checking for beta autoupdate feature for deb/rpm distributions`, - `Found package-type: deb`, - `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` - ]; - - const processOutput = (data: Buffer) => { - const line = data.toString(); - - if (line && line.trim().length > 0) { - if (ignoredLines.some(ignored => line.includes(ignored))) { - return; - } - - if (!options.silent) { - this.logger.info(line); - } + function processOutput(data: Buffer) { + const chunk = data.toString(); + output += chunk; - output += `${line}\n`; + if (!options.silent) { + process.stdout.write(chunk); } - }; + } try { exitCode = await new Promise((resolve, reject) => { @@ -117,28 +100,24 @@ export class UnityHub { }); }); } finally { - if (exitCode !== 0) { - throw new Error(`License command failed with exit code ${exitCode}`); - } - } + const match = output.match(/Assertion (?.+) failed/g); - const match = output.match(/Assertion (?.+) failed/g); - - if (match || - output.includes('async hook stack has become corrupted')) { - this.logger.warn(`Install failed, retrying...`); - return await this.Exec(args); - } + if (match || + output.includes('async hook stack has become corrupted')) { + this.logger.warn(`Install failed, retrying...`); + return await this.Exec(args); + } - if (output.includes('Error:')) { - const error = output.match(/Error: (.+)/); - const errorMessage = error && error[1] ? error[1] : 'Unknown Error'; + if (exitCode > 0 || output.includes('Error:')) { + const error = output.match(/Error: (.+)/); + const errorMessage = error && error[1] ? error[1] : 'Unknown Error'; - switch (errorMessage) { - case 'No modules found to install.': - return output; - default: - throw new Error(`Failed to execute Unity Hub: ${errorMessage}`); + switch (errorMessage) { + case 'No modules found to install.': + return output; + default: + throw new Error(`Failed to execute Unity Hub: [${exitCode}] ${errorMessage}`); + } } } @@ -148,9 +127,9 @@ export class UnityHub { /** * Prints the installed Unity Hub version. */ - public async Version(): Promise { + public async Version(): Promise { const version = await this.getInstalledHubVersion(); - this.logger.info(`Unity Hub Version: ${version.version}`); + return version.version; } /** @@ -351,6 +330,7 @@ chmod -R 777 "$hubPath"`]); */ public async GetInstallPath(): Promise { const result = (await this.Exec(['install-path', '--get'])).trim(); + if (!result || result.length === 0) { throw new Error('Failed to get Unity Hub install path.'); } @@ -443,21 +423,21 @@ chmod -R 777 "$hubPath"`]); } try { - this.logger.info(`Checking installed modules for Unity ${unityVersion.toString()}...`); + this.logger.debug(`Checking installed modules for Unity ${unityVersion.toString()}...`); const [installedModules, additionalModules] = await this.checkEditorModules(editorPath, unityVersion, modules); if (installedModules && installedModules.length > 0) { - this.logger.info(`Installed Modules:`); + this.logger.debug(`Installed Modules:`); for (const module of installedModules) { - this.logger.info(` > ${module}`); + this.logger.debug(` > ${module}`); } } if (additionalModules && additionalModules.length > 0) { - this.logger.info(`Additional Modules:`); + this.logger.debug(`Additional Modules:`); for (const module of additionalModules) { - this.logger.info(` > ${module}`); + this.logger.debug(` > ${module}`); } } } catch (error: Error | any) { @@ -475,10 +455,7 @@ chmod -R 777 "$hubPath"`]); * @returns A list of installed Unity Editor versions and their paths. */ public async ListInstalledEditors(): Promise { - const output = await this.Exec(['editors', '-i'], { - silent: this.logger.logLevel !== LogLevel.DEBUG, - showCommand: this.logger.logLevel === LogLevel.DEBUG - }); + const output = await this.Exec(['editors', '-i']); return output.split('\n') .filter(line => line.trim().length > 0) .map(line => line.trim()); @@ -488,11 +465,10 @@ chmod -R 777 "$hubPath"`]); let editorPath = undefined; if (!installPath) { const paths: string[] = await this.ListInstalledEditors(); - this.logger.debug(`Paths: ${JSON.stringify(paths, null, 2)}`); + if (paths && paths.length > 0) { const pattern = /(?\d+\.\d+\.\d+[abcfpx]?\d*)\s*(?:\((?Apple silicon|Intel)\))?\s*,? installed at (?.*)/; const matches = paths.map(path => path.match(pattern)).filter(match => match && match.groups); - this.logger.debug(`Matches: ${JSON.stringify(matches, null, 2)}`); if (paths.length !== matches.length) { throw new Error(`Failed to parse all installed Unity Editors!`); @@ -506,7 +482,6 @@ chmod -R 777 "$hubPath"`]); } else { // Fallback: semver satisfies const versionMatches = matches.filter(match => match?.groups?.version && unityVersion.satisfies(match.groups.version)); - this.logger.debug(`Version Matches: ${JSON.stringify(versionMatches, null, 2)}`); if (versionMatches.length === 0) { return undefined; @@ -745,10 +720,11 @@ done args.push('-m', module); } - const output = await this.Exec([...args, '--cm']); const editorRootPath = await UnityEditor.GetEditorRootPath(editorPath); const modulesPath = path.join(editorRootPath, 'modules.json'); this.logger.debug(`Editor Modules Manifest:\n > "${modulesPath}"`); + + const output = await this.Exec([...args, '--cm']); const moduleMatches = output.matchAll(/Omitting module (?.+) because it's already installed/g); if (moduleMatches) { diff --git a/src/utilities.ts b/src/utilities.ts index 796c055..d21dda0 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -16,10 +16,10 @@ const logger = Logger.instance; */ export async function ResolveGlobToPath(globs: string[]): Promise { const globPath: string = path.join(...globs).split(path.sep).join('/'); - logger.debug(`glob: ${globPath}`); + // logger.debug(`glob: ${globPath}`); const paths: string[] = await glob.glob(globPath); - logger.debug(`Resolved "${globPath}" to ${paths.length} paths:\n > ${paths.join('\n > ')}`); + // logger.debug(`Resolved "${globPath}" to ${paths.length} paths:\n > ${paths.join('\n > ')}`); for (const path of paths) { await fs.promises.access(path, fs.constants.R_OK); @@ -49,32 +49,35 @@ export async function PromptForSecretInput(prompt: string): Promise { }); } -export async function Exec(command: string, args: string[], options: { silent: boolean, showCommand: boolean } = { silent: false, showCommand: true }): Promise { +export type ExecOptions = { + silent?: boolean; + showCommand?: boolean; +} + +export async function Exec(command: string, args: string[], options: ExecOptions = { silent: false, showCommand: true }): Promise { let output: string = ''; let exitCode: number = 0; + function processOutput(data: Buffer) { + const chunk = data.toString(); + output += chunk; + + if (!options.silent) { + process.stdout.write(chunk); + } + } + try { exitCode = await new Promise((resolve, reject) => { if (options.showCommand) { logger.info(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); } + fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); - child.stdout.on('data', (data) => { - const chunk = data.toString(); - output += chunk; - - if (!options.silent) { - logger.info(chunk); - } - }); - - child.stderr.on('data', (data) => { - const chunk = data.toString(); - output += chunk; - logger.error(chunk); - }); + child.stdout.on('data', processOutput); + child.stderr.on('data', processOutput); child.on('error', (error) => { reject(error); From 172ce956cd2fe171da92104eb3be9e5e923e086f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 19 Sep 2025 18:49:40 -0400 Subject: [PATCH 20/90] added ci logging option --- src/android-sdk.ts | 27 +++++++++++---------- src/logging.ts | 60 ++++++++++++++++++++++++++++++++++++++++++---- src/unity-hub.ts | 31 ++++++++++++------------ 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 170c969..7e5d1bc 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -21,7 +21,7 @@ const logger = Logger.instance; * @returns A promise that resolves when the check is complete. */ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: string): Promise { - logger.debug(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`); + logger.ci(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`); let sdkPath = undefined; await createRepositoryCfg(); const rootEditorPath = await UnityEditor.GetEditorRootPath(editorPath); @@ -29,18 +29,18 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: const projectSettingsContent = await ReadFileContents(projectSettingsPath); const matchResult = projectSettingsContent.match(/(?<=AndroidTargetSdkVersion: )\d+/); const androidTargetSdk = matchResult ? parseInt(matchResult[0]) : 0; - logger.debug(`AndroidTargetSdkVersion:\n > ${androidTargetSdk}`); + logger.ci(`AndroidTargetSdkVersion:\n > ${androidTargetSdk}`); if (androidTargetSdk === undefined || androidTargetSdk === 0) { return; } sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); if (sdkPath) { - logger.debug(`Target Android SDK android-${androidTargetSdk} Installed in:\n > "${sdkPath}"`); + logger.ci(`Target Android SDK android-${androidTargetSdk} Installed in:\n > "${sdkPath}"`); return; } - logger.debug(`Installing Android Target SDK:\n > android-${androidTargetSdk}`); + logger.info(`Installing Android Target SDK:\n > android-${androidTargetSdk}`); const sdkManagerPath = await getSdkManager(rootEditorPath); const javaSdk = await getJDKPath(rootEditorPath); await execSdkManager(sdkManagerPath, javaSdk, ['--licenses']); @@ -52,7 +52,7 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: throw new Error(`Failed to install android-${androidTargetSdk} in ${rootEditorPath}`); } - logger.debug(`Target Android SDK Installed in:\n > "${sdkPath}"`); + logger.ci(`Target Android SDK Installed in:\n > "${sdkPath}"`); } @@ -71,7 +71,7 @@ async function getJDKPath(rootEditorPath: string): Promise { } await fs.promises.access(jdkPath, fs.constants.R_OK); - logger.debug(`jdkPath:\n > "${jdkPath}"`); + logger.ci(`jdkPath:\n > "${jdkPath}"`); return jdkPath; } @@ -95,12 +95,12 @@ async function getSdkManager(rootEditorPath: string): Promise { } await fs.promises.access(sdkmanagerPath, fs.constants.R_OK); - logger.debug(`sdkmanagerPath:\n > "${sdkmanagerPath}"`); + logger.ci(`sdkmanagerPath:\n > "${sdkmanagerPath}"`); return sdkmanagerPath; } async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise { - logger.debug(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); + logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); const sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); try { @@ -110,6 +110,7 @@ async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: numbe return undefined; } + logger.ci(`Android sdkPath:\n > "${sdkPath}"`); return sdkPath; } @@ -120,9 +121,7 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st try { exitCode = await new Promise((resolve, reject) => { - if (logger.logLevel === LogLevel.DEBUG) { - logger.info(`\x1b[34m${sdkManagerPath} ${args.join(' ')}\x1b[0m`); - } + logger.startGroup(`\x1b[34m${sdkManagerPath} ${args.join(' ')}\x1b[0m`); const child = spawn(sdkManagerPath, args, { stdio: ['pipe', 'pipe', 'pipe'], @@ -138,13 +137,13 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st output = ''; } - logger.debug(chunk); + process.stdout.write(chunk); }); child.stderr.on('data', (data: Buffer) => { const chunk = data.toString(); output += chunk; - logger.error(chunk); + process.stderr.write(chunk); }); child.on('error', (error: Error) => { @@ -156,6 +155,8 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st }); }); } finally { + logger.endGroup(); + if (exitCode !== 0) { throw new Error(`${sdkManagerPath} ${args.join(' ')} failed with exit code ${exitCode}`); } diff --git a/src/logging.ts b/src/logging.ts index 47a00f4..4e1448c 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,5 +1,6 @@ export enum LogLevel { DEBUG = 'debug', + CI = 'ci', INFO = 'info', WARN = 'warn', ERROR = 'error', @@ -7,12 +8,13 @@ export enum LogLevel { export class Logger { public logLevel: LogLevel = LogLevel.INFO; - public ci: string | undefined; + private _ci: string | undefined; static instance: Logger = new Logger(); private constructor() { if (process.env.GITHUB_ACTIONS) { - this.ci = 'GITHUB_ACTIONS'; + this._ci = 'GITHUB_ACTIONS'; + this.logLevel = LogLevel.CI; } Logger.instance = this; @@ -26,8 +28,12 @@ export class Logger { */ public log(level: LogLevel, message: any, optionalParams: any[] = []): void { if (this.shouldLog(level)) { - switch (this.ci) { + switch (this._ci) { case 'GITHUB_ACTIONS': { + if (level === LogLevel.CI) { + level = LogLevel.INFO; + } + console.log(`::${level}::${message}`, ...optionalParams); break; } @@ -35,16 +41,59 @@ export class Logger { const clear = '\x1b[0m'; const stringColor: string = { [LogLevel.DEBUG]: '\x1b[35m', // Purple - [LogLevel.INFO]: clear, // No color / White + [LogLevel.INFO]: clear, // No color / White + [LogLevel.CI]: clear, // No color / White [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red - }[level] || clear; // Default + }[level] || clear; // Default to no color / White console.log(`${stringColor}${message}${clear}`, ...optionalParams); + break; } } } } + /** + * Starts a log group. In CI environments that support grouping, this will create a collapsible group. + */ + public startGroup(message: any, optionalParams: any[] = [], logLevel: LogLevel = LogLevel.INFO): void { + switch (this._ci) { + case 'GITHUB_ACTIONS': { + console.log(`::group::${message}`, ...optionalParams); + break; + } + default: { + // No grouping in standard console + this.log(logLevel, message, optionalParams); + break; + } + } + } + + /** + * Ends a log group. In CI environments that support grouping, this will end the current group. + */ + public endGroup(): void { + switch (this._ci) { + case 'GITHUB_ACTIONS': { + console.log('::endgroup::'); + break; + } + default: { + break; // No grouping in standard console + } + } + } + + /** + * + * @param message + * @param optionalParams + */ + public ci(message: any, ...optionalParams: any[]): void { + this.log(LogLevel.CI, message, optionalParams); + } + public debug(message: any, ...optionalParams: any[]): void { this.log(LogLevel.DEBUG, message, optionalParams); } @@ -62,6 +111,7 @@ export class Logger { } private shouldLog(level: LogLevel): boolean { + if (level === LogLevel.CI) { return true; } const levelOrder = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; return levelOrder.indexOf(level) >= levelOrder.indexOf(this.logLevel); } diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 0a39dd4..ededd06 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -61,7 +61,7 @@ export class UnityHub { * @param silent If true, suppresses output logging. * @returns The output from the command. */ - public async Exec(args: string[], options: ExecOptions = { silent: this.logger.logLevel !== LogLevel.DEBUG, showCommand: this.logger.logLevel === LogLevel.DEBUG }): Promise { + public async Exec(args: string[], options: ExecOptions = { silent: this.logger.logLevel > LogLevel.CI, showCommand: this.logger.logLevel <= LogLevel.CI }): Promise { await fs.promises.access(this.executable, fs.constants.X_OK); let output: string = ''; @@ -83,7 +83,7 @@ export class UnityHub { const execArgs = process.platform === 'linux' ? ['--headless', ...filteredArgs] : ['--', '--headless', ...filteredArgs]; if (options.showCommand) { - this.logger.info(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); + this.logger.startGroup(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); } const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); @@ -100,6 +100,7 @@ export class UnityHub { }); }); } finally { + this.logger.endGroup(); const match = output.match(/Assertion (?.+) failed/g); if (match || @@ -147,12 +148,12 @@ export class UnityHub { if (isInstalled) { const installedVersion: SemVer = await this.getInstalledHubVersion(); - this.logger.debug(`Installed Unity Hub version: ${installedVersion.version}`); + this.logger.ci(`Installed Unity Hub version: ${installedVersion.version}`); let latestVersion: SemVer | undefined = undefined; try { latestVersion = await this.getLatestHubVersion(); - this.logger.debug(`Latest Unity Hub version: ${latestVersion.version}`); + this.logger.ci(`Latest Unity Hub version: ${latestVersion.version}`); } catch (error) { this.logger.warn(`Failed to get latest Unity Hub version: ${error}`); } @@ -368,7 +369,7 @@ chmod -R 777 "$hubPath"`]); 'failed to download. Error given: Request timeout' ]; - this.logger.debug(`Getting release info for Unity ${unityVersion.toString()}...`); + this.logger.ci(`Getting release info for Unity ${unityVersion.toString()}...`); let editorPath = await this.checkInstalledEditors(unityVersion, false); // attempt to resolve the full version with the changeset if we don't have one already @@ -423,21 +424,21 @@ chmod -R 777 "$hubPath"`]); } try { - this.logger.debug(`Checking installed modules for Unity ${unityVersion.toString()}...`); + this.logger.ci(`Checking installed modules for Unity ${unityVersion.toString()}...`); const [installedModules, additionalModules] = await this.checkEditorModules(editorPath, unityVersion, modules); if (installedModules && installedModules.length > 0) { - this.logger.debug(`Installed Modules:`); + this.logger.ci(`Installed Modules:`); for (const module of installedModules) { - this.logger.debug(` > ${module}`); + this.logger.ci(` > ${module}`); } } if (additionalModules && additionalModules.length > 0) { - this.logger.debug(`Additional Modules:`); + this.logger.ci(`Additional Modules:`); for (const module of additionalModules) { - this.logger.debug(` > ${module}`); + this.logger.ci(` > ${module}`); } } } catch (error: Error | any) { @@ -533,12 +534,12 @@ chmod -R 777 "$hubPath"`]); } try { - await fs.promises.access(editorPath, fs.constants.R_OK); + await fs.promises.access(editorPath, fs.constants.R_OK | fs.constants.X_OK); } catch (error) { throw new Error(`Failed to find installed Unity Editor: ${unityVersion.toString()}\n > ${error}`); } - this.logger.debug(`Found installed Unity Editor: ${editorPath}`); + this.logger.ci(`Found installed Unity Editor: ${editorPath}`); return editorPath; } @@ -572,7 +573,7 @@ chmod -R 777 "$hubPath"`]); const beeBackend = path.join(dataPath, 'bee_backend'); const dotBeeBackend = path.join(dataPath, '.bee_backend'); if (fs.existsSync(beeBackend) && !fs.existsSync(dotBeeBackend)) { - this.logger.debug(`Patching Unity Linux Editor for Bee Backend...`); + this.logger.ci(`Patching Unity Linux Editor for Bee Backend...`); await fs.promises.rename(beeBackend, dotBeeBackend); const wrapperSource: string = `#!/bin/bash # https://discussions.unity.com/t/linux-editor-stuck-on-loading-because-of-bee-backend-w-workaround/854480 @@ -801,7 +802,7 @@ done if (modules.length > 0) { for (const module of modules) { - this.logger.info(` > with module: ${module}`); + this.logger.ci(` > with module: ${module}`); args.push('-m', module); } @@ -809,7 +810,7 @@ done } this.logger.info(`Installing Unity ${unityVersion.toString()}...`); - const output = await this.Exec(args); + const output = await this.Exec(args, { showCommand: true, silent: false }); if (output.includes(`Error while installing an editor or a module from changeset`)) { throw new Error(`Failed to install Unity ${unityVersion.toString()}`); From 742df69430f62cf359482e56327af4373abc3dee Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 10:03:04 -0400 Subject: [PATCH 21/90] add simple test workflow --- .github/workflows/build-options.json | 27 +++++++++++++++ .github/workflows/integration-tests.yml | 42 +++++++++++++++++++++++ .github/workflows/publish.yml | 29 ++++++++++++++++ .github/workflows/unity-build.yml | 44 +++++++++++++++++++++++++ package.json | 2 +- 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-options.json create mode 100644 .github/workflows/integration-tests.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/unity-build.yml diff --git a/.github/workflows/build-options.json b/.github/workflows/build-options.json new file mode 100644 index 0000000..f2f8750 --- /dev/null +++ b/.github/workflows/build-options.json @@ -0,0 +1,27 @@ +{ + "os": [ + "ubuntu-latest", + "windows-latest", + "macos-latest" + ], + "unity-version": [ + "2022.3.x", + "6000.0.x", + "6000.1.x", + "6000" + ], + "include": [ + { + "os": "ubuntu-latest", + "build-target": "StandaloneLinux64" + }, + { + "os": "windows-latest", + "build-target": "StandaloneWindows64" + }, + { + "os": "macos-latest", + "build-target": "StandaloneOSX" + } + ] +} \ No newline at end of file diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..0a994a7 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,42 @@ +name: tests +on: + push: + branches: ['main'] + pull_request: + branches: ['*'] + types: [opened, reopened, synchronize, ready_for_review] + workflow_dispatch: # Allows you to run this workflow manually from the Actions tab +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + setup: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github/ + - uses: RageAgainstThePixel/job-builder@v1 + id: setup-jobs + with: + build-options: ./.github/workflows/build-options.json + group-by: 'unity-version' + outputs: + jobs: ${{ steps.setup-jobs.outputs.jobs }} + validate: + if: ${{ needs.setup.outputs.jobs }} + needs: setup + name: build ${{ matrix.jobs.name }} + permissions: + contents: read + strategy: + matrix: ${{ fromJSON(needs.setup.outputs.jobs) }} + fail-fast: false + max-parallel: 1 + secrets: inherit + uses: ./.github/workflows/unity-build.yml + with: + matrix: ${{ toJSON(matrix.jobs.matrix) }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..d0ba111 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish +on: + push: + branches: [main] + workflow_dispatch: +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + - run: | + npm ci + npm run build + # check if the last release is the same as the current version + lastVersion=$(npm show @rage-against-the-pixel/unity-releases-api version) + version=$(node -p "require('./package.json').version") + if [ "$version" = "$lastVersion" ]; then + echo "No changes detected" + exit 0 + fi + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml new file mode 100644 index 0000000..31bc9b7 --- /dev/null +++ b/.github/workflows/unity-build.yml @@ -0,0 +1,44 @@ +name: unity-build +permissions: + contents: read +on: + workflow_call: + inputs: + matrix: + required: true + type: string + secrets: + UNITY_USERNAME: + required: true + UNITY_PASSWORD: + required: true +jobs: + build: + name: ${{ matrix.name }} + strategy: + matrix: ${{ fromJSON(inputs.matrix) }} + fail-fast: false + runs-on: ${{ matrix.os }} + permissions: + contents: read + env: + VERSION: '' + EXPORT_OPTION: '' + BUILD_DIRECTORY: '' + UNITY_PROJECT_PATH: ${{ github.workspace }}/UnityProject + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + - run: | + npm ci + npm run build + npm run link + unity-cli --version + unity-cli hub-install + unity-cli activate-license --username ${{ secrets.UNITY_USERNAME }} --password ${{ secrets.UNITY_PASSWORD }} + - name: Post Run + if: always() + run: | + unity-cli return-license \ No newline at end of file diff --git a/package.json b/package.json index f7a5eab..7cb77e9 100644 --- a/package.json +++ b/package.json @@ -44,4 +44,4 @@ "ts-node": "^10.9.2", "typescript": "^5.9.2" } -} +} \ No newline at end of file From 8453e554340f768ee779f6706e9d9a6212b15342 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 10:07:32 -0400 Subject: [PATCH 22/90] use bash shell --- .github/workflows/unity-build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 31bc9b7..8106941 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -31,7 +31,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '20.x' - - run: | + - name: unity-cli + shell: bash + run: | npm ci npm run build npm run link @@ -40,5 +42,6 @@ jobs: unity-cli activate-license --username ${{ secrets.UNITY_USERNAME }} --password ${{ secrets.UNITY_PASSWORD }} - name: Post Run if: always() + shell: bash run: | unity-cli return-license \ No newline at end of file From e6cf0c19ef8c2b6e504b886ba5b175740428fd63 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 10:10:18 -0400 Subject: [PATCH 23/90] set -xe --- .github/workflows/unity-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 8106941..ffe010e 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -34,6 +34,7 @@ jobs: - name: unity-cli shell: bash run: | + set -xe npm ci npm run build npm run link @@ -44,4 +45,5 @@ jobs: if: always() shell: bash run: | + set -xe unity-cli return-license \ No newline at end of file From 1638110fdccc6f485e9c4cdcee6f194872bfb3db Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:25:14 -0400 Subject: [PATCH 24/90] update packages --- package-lock.json | 45 +++++++++++++++++---------------------------- package.json | 6 +++--- src/unity-hub.ts | 2 +- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30582d2..32de46f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", - "@rage-against-the-pixel/unity-releases-api": "^1.0.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.2", "commander": "^14.0.1", "glob": "^11.0.3", "semver": "^7.7.2", @@ -26,7 +26,7 @@ "@types/node": "^24.5.2", "@types/semver": "^7.7.1", "jest": "^30.1.3", - "ts-jest": "^29.4.3", + "ts-jest": "^29.4.4", "ts-node": "^10.9.2", "typescript": "^5.9.2" } @@ -631,16 +631,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@hey-api/client-fetch": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.4.tgz", - "integrity": "sha512-ebh1JjUdMAqes/Rg8OvbjDqGWGNhgHgmPtHlkIOUtj3y2mUXqX2g9sVoI/rSKW/FdADPng/90k5AL7bwT8W2lA==", - "deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/hey-api" - } - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -1213,12 +1203,11 @@ } }, "node_modules/@rage-against-the-pixel/unity-releases-api": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/unity-releases-api/-/unity-releases-api-1.0.1.tgz", - "integrity": "sha512-ubhZf78D7HTyGiQeQo9e6UIDzbRLgSP2HkPcb+fH8K9tbV0MkX0xhiYGnGPq94swZlVm52c5mZBE9hi4K2F3lg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/unity-releases-api/-/unity-releases-api-1.0.2.tgz", + "integrity": "sha512-mC0VT/SPnkOjhVpQXQTsSlo4+EYZRm4x8OazENXza94BVaDujr4OTAJBfuqzcl3dDO0DwPgzFa2nCaVQg8R3DQ==", "license": "MIT", "dependencies": { - "@hey-api/client-fetch": "^0.4.4", "jose": "^5.10.0" } }, @@ -2023,9 +2012,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "dev": true, "funding": [ { @@ -2321,9 +2310,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.223", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz", + "integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==", "dev": true, "license": "ISC" }, @@ -4035,9 +4024,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", - "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "license": "ISC", "engines": { "node": "20 || >=22" @@ -4542,9 +4531,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.3.tgz", - "integrity": "sha512-KTWbK2Wot8VXargsLoxhSoEQ9OyMdzQXQoUDeIulWu2Tf7gghuBHeg+agZqVLdTOHhQHVKAaeuctBDRkhWE7hg==", + "version": "29.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", + "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7cb77e9..8d29ac4 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@electron/asar": "^4.0.1", - "@rage-against-the-pixel/unity-releases-api": "^1.0.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.2", "commander": "^14.0.1", "glob": "^11.0.3", "semver": "^7.7.2", @@ -40,8 +40,8 @@ "@types/node": "^24.5.2", "@types/semver": "^7.7.1", "jest": "^30.1.3", - "ts-jest": "^29.4.3", + "ts-jest": "^29.4.4", "ts-node": "^10.9.2", "typescript": "^5.9.2" } -} \ No newline at end of file +} diff --git a/src/unity-hub.ts b/src/unity-hub.ts index ededd06..8d47290 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'yaml'; -import * as asar from '@electron/asar'; import { spawn } from 'child_process'; import { Logger, LogLevel } from './logging'; import { @@ -285,6 +284,7 @@ chmod -R 777 "$hubPath"`]); } await fs.promises.access(asarPath, fs.constants.R_OK); + const asar = await import('@electron/asar'); const fileBuffer = asar.extractFile(asarPath, 'package.json'); const packageJson = JSON.parse(fileBuffer.toString()); const version = coerce(packageJson.version); From acd300c988e5ae31eee2c913b9266ee58735077f Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:30:23 -0400 Subject: [PATCH 25/90] which sudo --- src/unity-hub.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 8d47290..5a72014 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -239,7 +239,8 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); break; } case 'linux': { - await Exec('sudo', ['sh', '-c', `#!/bin/bash + const sudoPath = await Exec('which', ['sudo'], { silent: true, showCommand: false }); + await Exec(sudoPath, ['sh', '-c', `#!/bin/bash set -e dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null From 7b5ab95ae7838b857179040d6a78ec4c6b338de7 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:37:22 -0400 Subject: [PATCH 26/90] get temp dir for CI env --- src/unity-hub.ts | 15 ++++++++------- src/utilities.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 5a72014..d0ea575 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -14,7 +14,8 @@ import { DownloadFile, Exec, ExecOptions, - ReadFileContents + ReadFileContents, + GetTempDir } from './utilities'; import { UnityVersion } from './unity-version'; import { GetUnityReleasesData, UnityRelease } from '@rage-against-the-pixel/unity-releases-api/dist/unity-releases-api/types.gen'; @@ -183,7 +184,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); switch (process.platform) { case 'win32': { const url = 'https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe'; - const downloadPath = path.join(process.env.TEMP || '.', 'UnityHubSetup.exe'); + const downloadPath = path.join(GetTempDir(), 'UnityHubSetup.exe'); await DownloadFile(url, downloadPath); this.logger.info(`Running Unity Hub installer...`); @@ -199,7 +200,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); case 'darwin': { const baseUrl = 'https://public-cdn.cloud.unity3d.com/hub/prod'; const url = `${baseUrl}/UnityHubSetup-${process.arch}.dmg`; - const downloadPath = path.join(process.env.TEMP || '.', `UnityHubSetup-${process.arch}.dmg`); + const downloadPath = path.join(GetTempDir(), `UnityHubSetup-${process.arch}.dmg`); this.logger.info(`Downloading Unity Hub from ${url} to ${downloadPath}`); await DownloadFile(url, downloadPath); @@ -770,7 +771,7 @@ done if (['2019.1', '2019.2'].some(v => unityVersion.version.startsWith(v))) { const url = `https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`; - const downloadPath = path.join(process.env.TEMP || '.', `libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`); + const downloadPath = path.join(GetTempDir(), `libssl1.0.0_1.0.2g-1ubuntu4.20_${arch}.deb`); await DownloadFile(url, downloadPath); try { @@ -780,7 +781,7 @@ done } } else if (['2019.3', '2019.4'].some(v => unityVersion.version.startsWith(v)) || unityVersion.version.startsWith('2020.')) { const url = `https://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`; - const downloadPath = path.join(process.env.TEMP || '.', `libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`); + const downloadPath = path.join(GetTempDir(), `libssl1.1_1.1.0g-2ubuntu4_${arch}.deb`); await DownloadFile(url, downloadPath); try { @@ -827,7 +828,7 @@ done if (!fs.existsSync(installPath)) { const url = `https://beta.unity3d.com/download/UnitySetup-${unityVersion.version}.exe`; - const installerPath = path.join(process.env.TEMP || '.', `UnitySetup-${unityVersion.version}.exe`); + const installerPath = path.join(GetTempDir(), `UnitySetup-${unityVersion.version}.exe`); await DownloadFile(url, installerPath); this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); @@ -849,7 +850,7 @@ done if (!fs.existsSync(installPath)) { const url = `https://beta.unity3d.com/download/unity-${unityVersion.version}.dmg`; - const installerPath = path.join(process.env.TEMP || '.', `UnitySetup-${unityVersion.version}.dmg`); + const installerPath = path.join(GetTempDir(), `UnitySetup-${unityVersion.version}.dmg`); await DownloadFile(url, installerPath); this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); diff --git a/src/utilities.ts b/src/utilities.ts index d21dda0..f514494 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -128,4 +128,18 @@ export async function ReadFileContents(filePath: string): Promise { } finally { await fileHandle.close(); } +} + +export function GetTempDir(): string { + if (process.env['RUNNER_TEMP']) { + return process.env['RUNNER_TEMP']!; + } else if (process.env['TMPDIR']) { + return process.env['TMPDIR']!; + } else if (process.env['TEMP']) { + return process.env['TEMP']!; + } else if (process.env['TMP']) { + return process.env['TMP']!; + } + // fallback to current directory + return '.'; } \ No newline at end of file From 0971ffc34af1d9318854948aad563d6fbfe5b297 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:40:33 -0400 Subject: [PATCH 27/90] inherit env? --- src/unity-hub.ts | 4 +++- src/utilities.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index d0ea575..f30eab0 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -86,7 +86,9 @@ export class UnityHub { this.logger.startGroup(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); } - const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); + const child = spawn(executable, execArgs, { + stdio: ['ignore', 'pipe', 'pipe'], + }); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); diff --git a/src/utilities.ts b/src/utilities.ts index f514494..93eb842 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -74,7 +74,10 @@ export async function Exec(command: string, args: string[], options: ExecOptions } fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); - const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + const child = spawn(command, args, { + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); From c7d31cae7f80f0a102c49757b9ee12f23e610048 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:43:44 -0400 Subject: [PATCH 28/90] revert --- src/unity-hub.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index f30eab0..82e726a 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -242,8 +242,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); break; } case 'linux': { - const sudoPath = await Exec('which', ['sudo'], { silent: true, showCommand: false }); - await Exec(sudoPath, ['sh', '-c', `#!/bin/bash + await Exec('sudo', ['sh', '-c', `#!/bin/bash set -e dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null From 31e8afe62ec9e18b077db7e6839eb6478dda45b9 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:44:15 -0400 Subject: [PATCH 29/90] chmod --- src/unity-hub.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 82e726a..9a6999b 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -206,6 +206,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.info(`Downloading Unity Hub from ${url} to ${downloadPath}`); await DownloadFile(url, downloadPath); + await fs.promises.chmod(downloadPath, 0o777); let mountPoint = ''; this.logger.debug(`Mounting DMG...`); From 252b335fe026f75c3800ac720b233ad0a8e9fcfe Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:47:42 -0400 Subject: [PATCH 30/90] verify dir exists before downloading --- src/utilities.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utilities.ts b/src/utilities.ts index 93eb842..96bf139 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -4,6 +4,7 @@ import * as glob from 'glob'; import * as fs from 'fs'; import * as https from 'https'; import * as readline from 'readline'; +import * as os from 'os'; import { spawn } from 'child_process'; import { Logger } from './logging'; @@ -101,6 +102,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions export async function DownloadFile(url: string, downloadPath: string): Promise { logger.debug(`Downloading from ${url} to ${downloadPath}...`); + await fs.promises.mkdir(path.dirname(downloadPath), { recursive: true }); await new Promise((resolve, reject) => { const file = fs.createWriteStream(downloadPath); https.get(url, (response) => { @@ -144,5 +146,5 @@ export function GetTempDir(): string { return process.env['TMP']!; } // fallback to current directory - return '.'; + return os.tmpdir(); } \ No newline at end of file From ff1df1a833ec5ea269536e63bfcef1a5653648f2 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:49:21 -0400 Subject: [PATCH 31/90] update download file permissions --- src/utilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities.ts b/src/utilities.ts index 96bf139..34c9130 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -104,7 +104,7 @@ export async function DownloadFile(url: string, downloadPath: string): Promise((resolve, reject) => { - const file = fs.createWriteStream(downloadPath); + const file = fs.createWriteStream(downloadPath, { mode: 0o755 }); https.get(url, (response) => { response.pipe(file); file.on('finish', () => { From ac62079483d2a103a6eb875542102c684c431c8f Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:53:20 -0400 Subject: [PATCH 32/90] only check access if it is a full path --- src/unity-hub.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 9a6999b..6b7bcf3 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -62,7 +62,11 @@ export class UnityHub { * @returns The output from the command. */ public async Exec(args: string[], options: ExecOptions = { silent: this.logger.logLevel > LogLevel.CI, showCommand: this.logger.logLevel <= LogLevel.CI }): Promise { - await fs.promises.access(this.executable, fs.constants.X_OK); + const isPath = this.executable.includes(path.sep); + + if (isPath) { + await fs.promises.access(this.executable, fs.constants.X_OK); + } let output: string = ''; let exitCode: number = 0; From 7e2cfa28c20d3476fb4d8e5f6afe778e3552e387 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:55:49 -0400 Subject: [PATCH 33/90] remove access check in exec --- src/utilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities.ts b/src/utilities.ts index 34c9130..6f8edeb 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -74,7 +74,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions logger.info(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); } - fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); + // fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); const child = spawn(command, args, { env: process.env, stdio: ['ignore', 'pipe', 'pipe'], From 28246a349a2466cd0a4f030adfe3c0e4e95e78e1 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 10:58:38 -0400 Subject: [PATCH 34/90] quote string inputs --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index ffe010e..f863946 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -40,7 +40,7 @@ jobs: npm run link unity-cli --version unity-cli hub-install - unity-cli activate-license --username ${{ secrets.UNITY_USERNAME }} --password ${{ secrets.UNITY_PASSWORD }} + unity-cli activate-license --username "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" - name: Post Run if: always() shell: bash From 3a994d37fa6471bff04d9f5e7048d6488377de7b Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:02:34 -0400 Subject: [PATCH 35/90] install commands with 777 --- src/unity-hub.ts | 4 ++-- src/utilities.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 6b7bcf3..1a17da8 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -231,9 +231,9 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.debug(`Copying ${appPath} to /Applications...`); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); - await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); + await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true, mode: 0o777 }); await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); - await fs.promises.mkdir('/Library/Application Support/Unity', { recursive: true }); + await fs.promises.mkdir('/Library/Application Support/Unity', { recursive: true, mode: 0o777 }); await fs.promises.chmod('/Library/Application Support/Unity', 0o777); } finally { try { diff --git a/src/utilities.ts b/src/utilities.ts index 6f8edeb..1bce1f1 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -74,7 +74,10 @@ export async function Exec(command: string, args: string[], options: ExecOptions logger.info(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); } - // fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); + if (command.includes(path.sep)) { + fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); + } + const child = spawn(command, args, { env: process.env, stdio: ['ignore', 'pipe', 'pipe'], From 833e805875cb207d55614d77414496c9b69d19a0 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:05:39 -0400 Subject: [PATCH 36/90] revert --- src/unity-hub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 1a17da8..1ea91a4 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -231,7 +231,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.debug(`Copying ${appPath} to /Applications...`); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); - await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true, mode: 0o777 }); + await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); await fs.promises.mkdir('/Library/Application Support/Unity', { recursive: true, mode: 0o777 }); await fs.promises.chmod('/Library/Application Support/Unity', 0o777); From bcf8599476a28a481df774675f82061eab66dfbe Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:07:50 -0400 Subject: [PATCH 37/90] use direct sudo commands --- src/unity-hub.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 1ea91a4..fd46d32 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -233,8 +233,8 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); - await fs.promises.mkdir('/Library/Application Support/Unity', { recursive: true, mode: 0o777 }); - await fs.promises.chmod('/Library/Application Support/Unity', 0o777); + await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity']); + await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity']); } finally { try { if (mountPoint && mountPoint.length > 0) { From 40430868d650e7444b0e66b1a57772e783a26c28 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:08:37 -0400 Subject: [PATCH 38/90] use email --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index f863946..fd397df 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -40,7 +40,7 @@ jobs: npm run link unity-cli --version unity-cli hub-install - unity-cli activate-license --username "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" + unity-cli activate-license --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" - name: Post Run if: always() shell: bash From d0b2ebbb1d6ef16cab3695c81892bd8e671072d4 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:13:30 -0400 Subject: [PATCH 39/90] update license input string --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 716b422..c41855c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,13 @@ program.command('activate-license') } const client = new LicensingClient(); - const licenseType: LicenseType = options.license.toString().toLowerCase() as LicenseType; + const licenseStr: string = options.license?.toString()?.trim(); + + if (!licenseStr || licenseStr.length === 0) { + throw new Error('License type is required. Use -l or --license to specify it.'); + } + + const licenseType: LicenseType = options.license.toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); From 1e07fa5573bb5d4f444baa779bfa765d6f4aee44 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:15:25 -0400 Subject: [PATCH 40/90] add license --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index fd397df..65682f1 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -40,7 +40,7 @@ jobs: npm run link unity-cli --version unity-cli hub-install - unity-cli activate-license --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" + unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" - name: Post Run if: always() shell: bash From 04dcceb09f5ac72e176275d130a1f6e777850eef Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:18:19 -0400 Subject: [PATCH 41/90] update return license --- .github/workflows/unity-build.yml | 2 +- src/index.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 65682f1..a775444 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -46,4 +46,4 @@ jobs: shell: bash run: | set -xe - unity-cli return-license \ No newline at end of file + unity-cli return-license --license personal \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c41855c..51ba04d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,7 +80,13 @@ program.command('return-license') } const client = new LicensingClient(); - const licenseType: LicenseType = options.license.toString().toLowerCase() as LicenseType; + const licenseStr: string = options.license?.toString()?.trim(); + + if (!licenseStr || licenseStr.length === 0) { + throw new Error('License type is required. Use -l or --license to specify it.'); + } + + const licenseType: LicenseType = licenseStr.toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { throw new Error(`Invalid license type: ${licenseType}`); From aff92654cd2ee4dcea69ba05a1105bef25cc6a52 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:20:11 -0400 Subject: [PATCH 42/90] update exec logging --- src/utilities.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utilities.ts b/src/utilities.ts index 1bce1f1..d878dc9 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -68,12 +68,12 @@ export async function Exec(command: string, args: string[], options: ExecOptions } } + if (options.showCommand) { + logger.startGroup(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); + } + try { exitCode = await new Promise((resolve, reject) => { - if (options.showCommand) { - logger.info(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); - } - if (command.includes(path.sep)) { fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); } @@ -95,6 +95,9 @@ export async function Exec(command: string, args: string[], options: ExecOptions }); }); } finally { + if (options.showCommand) { + logger.endGroup(); + } if (exitCode !== 0) { throw new Error(`${command} failed with exit code ${exitCode}`); } From 9b27aec106fa0c30c4b63cea934bdb9b060d1f92 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:35:28 -0400 Subject: [PATCH 43/90] rework logging for exec --- src/android-sdk.ts | 8 ++++++-- src/license-client.ts | 14 +++++++++----- src/logging.ts | 12 +++++++----- src/unity-hub.ts | 25 ++++++++++++------------- src/utilities.ts | 8 ++++---- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 7e5d1bc..c16fc55 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -119,10 +119,14 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st let output = ''; let exitCode = 0; + logger.startGroup(`\x1b[34m${sdkManagerPath} ${args.join(' ')}\x1b[0m`); + + if (sdkManagerPath.includes(path.sep)) { + fs.accessSync(sdkManagerPath, fs.constants.R_OK | fs.constants.X_OK); + } + try { exitCode = await new Promise((resolve, reject) => { - logger.startGroup(`\x1b[34m${sdkManagerPath} ${args.join(' ')}\x1b[0m`); - const child = spawn(sdkManagerPath, args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, JAVA_HOME: javaPath } diff --git a/src/license-client.ts b/src/license-client.ts index f7e2e29..73d054b 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -227,8 +227,6 @@ export class LicensingClient { this.licenseClientPath = await this.init(); } - await fs.promises.access(this.licenseClientPath!, fs.constants.R_OK | fs.constants.X_OK); - let output: string = ''; let exitCode: number = 0; @@ -238,11 +236,15 @@ export class LicensingClient { process.stdout.write(chunk); } + this.logger.startGroup(`\x1b[34m${this.licenseClientPath} ${args.join(' ')}\x1b[0m`); + await fs.promises.access(this.licenseClientPath!, fs.constants.R_OK | fs.constants.X_OK); + try { exitCode = await new Promise((resolve, reject) => { - this.logger.info(`\x1b[34m${this.licenseClientPath} ${args.join(' ')}\x1b[0m`); - - const child = spawn(this.licenseClientPath!, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + fs.accessSync(this.licenseClientPath!, fs.constants.R_OK | fs.constants.X_OK); + const child = spawn(this.licenseClientPath!, args, { + stdio: ['ignore', 'pipe', 'pipe'] + }); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); @@ -256,6 +258,8 @@ export class LicensingClient { }); }); } finally { + this.logger.endGroup(); + if (exitCode !== 0) { const message = this.getExitCodeMessage(exitCode); throw new Error(`License command failed with exit code ${exitCode}: ${message}`); diff --git a/src/logging.ts b/src/logging.ts index 4e1448c..76daaf4 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -32,9 +32,11 @@ export class Logger { case 'GITHUB_ACTIONS': { if (level === LogLevel.CI) { level = LogLevel.INFO; + process.stdout.write(`${message}`, ...optionalParams); + break; } - console.log(`::${level}::${message}`, ...optionalParams); + process.stdout.write(`::${level}::${message}`, ...optionalParams); break; } default: { @@ -46,7 +48,7 @@ export class Logger { [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red }[level] || clear; // Default to no color / White - console.log(`${stringColor}${message}${clear}`, ...optionalParams); + process.stdout.write(`${stringColor}${message}${clear}`, ...optionalParams); break; } } @@ -59,7 +61,7 @@ export class Logger { public startGroup(message: any, optionalParams: any[] = [], logLevel: LogLevel = LogLevel.INFO): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - console.log(`::group::${message}`, ...optionalParams); + process.stdout.write(`::group::${message}`, ...optionalParams); break; } default: { @@ -76,7 +78,7 @@ export class Logger { public endGroup(): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - console.log('::endgroup::'); + process.stdout.write('::endgroup::'); break; } default: { @@ -86,7 +88,7 @@ export class Logger { } /** - * + * Logs a message with CI level. * @param message * @param optionalParams */ diff --git a/src/unity-hub.ts b/src/unity-hub.ts index fd46d32..e81a573 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -62,12 +62,6 @@ export class UnityHub { * @returns The output from the command. */ public async Exec(args: string[], options: ExecOptions = { silent: this.logger.logLevel > LogLevel.CI, showCommand: this.logger.logLevel <= LogLevel.CI }): Promise { - const isPath = this.executable.includes(path.sep); - - if (isPath) { - await fs.promises.access(this.executable, fs.constants.X_OK); - } - let output: string = ''; let exitCode: number = 0; @@ -80,15 +74,20 @@ export class UnityHub { } } + const filteredArgs = args.filter(arg => arg !== '--headless' && arg !== '--'); + const executable = process.platform === 'linux' ? 'unity-hub' : this.executable; + const execArgs = process.platform === 'linux' ? ['--headless', ...filteredArgs] : ['--', '--headless', ...filteredArgs]; + + if (options.showCommand) { + this.logger.startGroup(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); + } + + if (this.executable.includes(path.sep)) { + fs.accessSync(this.executable, fs.constants.R_OK | fs.constants.X_OK); + } + try { exitCode = await new Promise((resolve, reject) => { - const filteredArgs = args.filter(arg => arg !== '--headless' && arg !== '--'); - const executable = process.platform === 'linux' ? 'unity-hub' : this.executable; - const execArgs = process.platform === 'linux' ? ['--headless', ...filteredArgs] : ['--', '--headless', ...filteredArgs]; - - if (options.showCommand) { - this.logger.startGroup(`\x1b[34m${executable} ${execArgs.join(' ')}\x1b[0m`); - } const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'], diff --git a/src/utilities.ts b/src/utilities.ts index d878dc9..d69d605 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -72,12 +72,12 @@ export async function Exec(command: string, args: string[], options: ExecOptions logger.startGroup(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); } + if (command.includes(path.sep)) { + fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); + } + try { exitCode = await new Promise((resolve, reject) => { - if (command.includes(path.sep)) { - fs.accessSync(command, fs.constants.R_OK | fs.constants.X_OK); - } - const child = spawn(command, args, { env: process.env, stdio: ['ignore', 'pipe', 'pipe'], From 38ecdeaf2a4c12500b082de8bf441c9b09e8db2d Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:38:47 -0400 Subject: [PATCH 44/90] new lines --- src/logging.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 76daaf4..187af31 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -32,11 +32,11 @@ export class Logger { case 'GITHUB_ACTIONS': { if (level === LogLevel.CI) { level = LogLevel.INFO; - process.stdout.write(`${message}`, ...optionalParams); + process.stdout.write(`${message}\n`, ...optionalParams); break; } - process.stdout.write(`::${level}::${message}`, ...optionalParams); + process.stdout.write(`::${level}::${message}\n`, ...optionalParams); break; } default: { @@ -48,7 +48,7 @@ export class Logger { [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red }[level] || clear; // Default to no color / White - process.stdout.write(`${stringColor}${message}${clear}`, ...optionalParams); + process.stdout.write(`${stringColor}${message}${clear}\n`, ...optionalParams); break; } } @@ -61,7 +61,7 @@ export class Logger { public startGroup(message: any, optionalParams: any[] = [], logLevel: LogLevel = LogLevel.INFO): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - process.stdout.write(`::group::${message}`, ...optionalParams); + process.stdout.write(`::group::${message}\n`, ...optionalParams); break; } default: { @@ -78,7 +78,7 @@ export class Logger { public endGroup(): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - process.stdout.write('::endgroup::'); + process.stdout.write('::endgroup::\n'); break; } default: { From 5b334bab36ba74457146ac0045d33b3d2ffd88fa Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:45:43 -0400 Subject: [PATCH 45/90] EOL --- src/android-sdk.ts | 1 + src/license-client.ts | 1 + src/unity-hub.ts | 3 ++- src/utilities.ts | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index c16fc55..004dcb4 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -155,6 +155,7 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st }); child.on('close', (code: number | null) => { + process.stdout.write(os.EOL); resolve(code === null ? 0 : code); }); }); diff --git a/src/license-client.ts b/src/license-client.ts index 73d054b..7e1ed25 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -254,6 +254,7 @@ export class LicensingClient { }); child.on('close', (code) => { + process.stdout.write(os.EOL); resolve(code === null ? 0 : code); }); }); diff --git a/src/unity-hub.ts b/src/unity-hub.ts index e81a573..cc93185 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as yaml from 'yaml'; import { spawn } from 'child_process'; @@ -88,7 +89,6 @@ export class UnityHub { try { exitCode = await new Promise((resolve, reject) => { - const child = spawn(executable, execArgs, { stdio: ['ignore', 'pipe', 'pipe'], }); @@ -101,6 +101,7 @@ export class UnityHub { }); child.on('close', (code) => { + process.stdout.write(os.EOL); resolve(code === null ? 0 : code); }); }); diff --git a/src/utilities.ts b/src/utilities.ts index d69d605..0da57ea 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -91,6 +91,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions }); child.on('close', (code) => { + process.stdout.write(os.EOL); resolve(code === null ? 0 : code); }); }); From 6d16dae04c9b4724b1283837f232134238c43426 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:47:52 -0400 Subject: [PATCH 46/90] spit group lines --- src/logging.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/logging.ts b/src/logging.ts index 187af31..cde96eb 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -61,7 +61,14 @@ export class Logger { public startGroup(message: any, optionalParams: any[] = [], logLevel: LogLevel = LogLevel.INFO): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - process.stdout.write(`::group::${message}\n`, ...optionalParams); + // if there is newline in message, only use the first line for group title + // then print the rest of the lines inside the group in cyan color + const firstLine = message.toString().split('\n')[0]; + const restLines = message.toString().split('\n').slice(1).join('\n'); + const cyan = '\x1b[36m'; + const clear = '\x1b[0m'; + process.stdout.write(`::group::${firstLine}\n`, ...optionalParams); + process.stdout.write(`${cyan}${restLines}${clear}\n`, ...optionalParams); break; } default: { From 53def9f8dcb1254af36edb3a3e3e3f0c3d99ffa9 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:50:42 -0400 Subject: [PATCH 47/90] rework group --- src/logging.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index cde96eb..be421a0 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -63,12 +63,14 @@ export class Logger { case 'GITHUB_ACTIONS': { // if there is newline in message, only use the first line for group title // then print the rest of the lines inside the group in cyan color - const firstLine = message.toString().split('\n')[0]; - const restLines = message.toString().split('\n').slice(1).join('\n'); + const firstLine: string = message.toString().split('\n')[0]; + const restLines: string[] = message.toString().split('\n').slice(1); const cyan = '\x1b[36m'; const clear = '\x1b[0m'; process.stdout.write(`::group::${firstLine}\n`, ...optionalParams); - process.stdout.write(`${cyan}${restLines}${clear}\n`, ...optionalParams); + restLines.forEach(line => { + process.stdout.write(`${cyan}${line}${clear}\n`, ...optionalParams); + }); break; } default: { From 74c05786b5c520dd4de2c6a807aa85e62ac01d84 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 11:58:20 -0400 Subject: [PATCH 48/90] EOL --- src/index.ts | 11 ++++++----- src/logging.ts | 22 ++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 51ba04d..96e14af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import 'source-map-support/register'; +import * as os from 'os'; import { Command } from 'commander'; import { readFileSync } from 'fs'; import { join } from 'path'; @@ -100,7 +101,7 @@ program.command('hub-version') .action(async () => { const unityHub = new UnityHub(); const version = await unityHub.Version(); - process.stdout.write(version); + process.stdout.write(`${version}${os.EOL}`); }); program.command('hub-install') @@ -116,9 +117,9 @@ program.command('hub-install') const hubPath = await unityHub.Install(); if (options.json) { - process.stdout.write(JSON.stringify({ UNITY_HUB: hubPath })); + process.stdout.write(`$${JSON.stringify({ UNITY_HUB: hubPath })}${os.EOL}`); } else { - process.stdout.write(hubPath); + process.stdout.write(`${hubPath}${os.EOL}`); } }); @@ -126,7 +127,7 @@ program.command('hub-path') .description('Print the path to the Unity Hub executable.') .action(async () => { const hub = new UnityHub(); - process.stdout.write(hub.executable); + process.stdout.write(`${hub.executable}${os.EOL}`); }); program.command('hub') @@ -191,7 +192,7 @@ program.command('setup-unity') } } - process.stdout.write(JSON.stringify(output)); + process.stdout.write(`${JSON.stringify(output)}${os.EOL}`); }); program.parse(process.argv); diff --git a/src/logging.ts b/src/logging.ts index be421a0..3404fd9 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,3 +1,5 @@ +import * as os from 'os'; + export enum LogLevel { DEBUG = 'debug', CI = 'ci', @@ -30,13 +32,13 @@ export class Logger { if (this.shouldLog(level)) { switch (this._ci) { case 'GITHUB_ACTIONS': { - if (level === LogLevel.CI) { - level = LogLevel.INFO; - process.stdout.write(`${message}\n`, ...optionalParams); + if (level === LogLevel.CI || + level === LogLevel.INFO) { + process.stdout.write(`${message}${os.EOL}`, ...optionalParams); break; } - process.stdout.write(`::${level}::${message}\n`, ...optionalParams); + process.stdout.write(`::${level}::${message}${os.EOL}`, ...optionalParams); break; } default: { @@ -48,7 +50,7 @@ export class Logger { [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red }[level] || clear; // Default to no color / White - process.stdout.write(`${stringColor}${message}${clear}\n`, ...optionalParams); + process.stdout.write(`${stringColor}${message}${clear}${os.EOL}`, ...optionalParams); break; } } @@ -63,13 +65,13 @@ export class Logger { case 'GITHUB_ACTIONS': { // if there is newline in message, only use the first line for group title // then print the rest of the lines inside the group in cyan color - const firstLine: string = message.toString().split('\n')[0]; - const restLines: string[] = message.toString().split('\n').slice(1); + const firstLine: string = message.toString().split(os.EOL)[0]; + const restLines: string[] = message.toString().split(os.EOL).slice(1); const cyan = '\x1b[36m'; const clear = '\x1b[0m'; - process.stdout.write(`::group::${firstLine}\n`, ...optionalParams); + process.stdout.write(`::group::${firstLine}${os.EOL}`, ...optionalParams); restLines.forEach(line => { - process.stdout.write(`${cyan}${line}${clear}\n`, ...optionalParams); + process.stdout.write(`${cyan}${line}${clear}${os.EOL}`, ...optionalParams); }); break; } @@ -87,7 +89,7 @@ export class Logger { public endGroup(): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - process.stdout.write('::endgroup::\n'); + process.stdout.write(`::endgroup::${os.EOL}`); break; } default: { From 40fb0c0b364ba55a6b755939f1bf241e25523db2 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 14:20:36 -0400 Subject: [PATCH 49/90] attempt to install unity version --- .github/workflows/unity-build.yml | 1 + src/index.ts | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index a775444..269dea9 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -41,6 +41,7 @@ jobs: unity-cli --version unity-cli hub-install unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" + unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" - name: Post Run if: always() shell: bash diff --git a/src/index.ts b/src/index.ts index 96e14af..1c27e3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -175,14 +175,11 @@ program.command('setup-unity') const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; const unityHub = new UnityHub(); - - const output: { [key: string]: string } = {}; - - output['UNITY_HUB_PATH'] = unityHub.executable; - const editorPath = await unityHub.GetEditor(unityVersion, modules); - - output['UNITY_EDITOR'] = editorPath; + const output: { [key: string]: string } = { + 'UNITY_HUB_PATH': unityHub.executable, + 'UNITY_EDITOR': editorPath + }; if (unityProject) { output['UNITY_PROJECT'] = unityProject.projectPath; From c58afe17a20201b64fb5f14bb711ed351f03d729 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 14:30:21 -0400 Subject: [PATCH 50/90] update macos hub installer --- src/unity-hub.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index cc93185..d080ea7 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -231,7 +231,10 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.debug(`Copying ${appPath} to /Applications...`); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); - await fs.promises.cp(appPath, '/Applications/Unity Hub.app', { recursive: true }); + if (fs.existsSync('/Applications/Unity Hub.app')) { + await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app']); + } + await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app']); await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity']); await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity']); From 60fd2d77471f3d84151161509b25a79c756372fd Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 15:07:00 -0400 Subject: [PATCH 51/90] update hub permissions on install --- src/unity-hub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index d080ea7..fcf9c25 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -235,7 +235,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app']); } await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app']); - await fs.promises.chmod('/Applications/Unity Hub.app/Contents/MacOS/Unity Hub', 0o777); + await Exec('sudo', ['chmod', '777', '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub']); await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity']); await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity']); } finally { From 9fb1f8a8653f7ca9705b8cfb1b270d0c7effd7d3 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 15:57:57 -0400 Subject: [PATCH 52/90] ignored lines --- src/unity-hub.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index fcf9c25..974d869 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -66,11 +66,21 @@ export class UnityHub { let output: string = ''; let exitCode: number = 0; + const ignoredLines = [ + `This error originated either by throwing inside of an async function without a catch block`, + `Unexpected error attempting to determine if executable file exists`, + `dri3 extension not supported`, + `Failed to connect to the bus:`, + `Checking for beta autoupdate feature for deb/rpm distributions`, + `Found package-type: deb`, + `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` + ]; + function processOutput(data: Buffer) { const chunk = data.toString(); output += chunk; - if (!options.silent) { + if (!options.silent && !ignoredLines.some(line => chunk.includes(line))) { process.stdout.write(chunk); } } From 7d72f5a24e2c720a47645cb3a001f1452b26a266 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Thu, 25 Sep 2025 17:12:07 -0400 Subject: [PATCH 53/90] tweak hub stdout --- src/unity-hub.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 974d869..116abd9 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -80,7 +80,9 @@ export class UnityHub { const chunk = data.toString(); output += chunk; - if (!options.silent && !ignoredLines.some(line => chunk.includes(line))) { + if (!options.silent && + chunk.trim().length > 0 && + !ignoredLines.some(line => chunk.includes(line))) { process.stdout.write(chunk); } } @@ -478,7 +480,7 @@ chmod -R 777 "$hubPath"`]); */ public async ListInstalledEditors(): Promise { const output = await this.Exec(['editors', '-i']); - return output.split('\n') + return output.split(os.EOL) .filter(line => line.trim().length > 0) .map(line => line.trim()); } From 8886d8b8e1d035e427fb0f6c76a9578e9c147024 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 17:24:57 -0400 Subject: [PATCH 54/90] debug --- src/index.ts | 8 +++++--- src/unity-hub.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1c27e3e..10597b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,8 +140,8 @@ program.command('hub') Logger.instance.logLevel = LogLevel.DEBUG; } - const hub = new UnityHub(); - await hub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); + const unityHub = new UnityHub(); + await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); }); program.command('setup-unity') @@ -189,7 +189,9 @@ program.command('setup-unity') } } - process.stdout.write(`${JSON.stringify(output)}${os.EOL}`); + if (options.json) { + process.stdout.write(`$${JSON.stringify(output)}${os.EOL}`); + } }); program.parse(process.argv); diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 116abd9..bbf4ee7 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -495,7 +495,7 @@ chmod -R 777 "$hubPath"`]); const matches = paths.map(path => path.match(pattern)).filter(match => match && match.groups); if (paths.length !== matches.length) { - throw new Error(`Failed to parse all installed Unity Editors!`); + throw new Error(`Failed to parse all installed Unity Editors!\n > paths: ${JSON.stringify(paths)}\n > matches: ${JSON.stringify(matches)}`); } // Prefer exact version match first From 8680a11baef96ceb5d316a41a9f171ee2d165651 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 17:29:08 -0400 Subject: [PATCH 55/90] re-work how we filter output lines --- src/unity-hub.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index bbf4ee7..427289c 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -66,23 +66,11 @@ export class UnityHub { let output: string = ''; let exitCode: number = 0; - const ignoredLines = [ - `This error originated either by throwing inside of an async function without a catch block`, - `Unexpected error attempting to determine if executable file exists`, - `dri3 extension not supported`, - `Failed to connect to the bus:`, - `Checking for beta autoupdate feature for deb/rpm distributions`, - `Found package-type: deb`, - `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` - ]; - function processOutput(data: Buffer) { const chunk = data.toString(); output += chunk; - if (!options.silent && - chunk.trim().length > 0 && - !ignoredLines.some(line => chunk.includes(line))) { + if (!options.silent) { process.stdout.write(chunk); } } @@ -133,11 +121,22 @@ export class UnityHub { switch (errorMessage) { case 'No modules found to install.': - return output; + break; default: throw new Error(`Failed to execute Unity Hub: [${exitCode}] ${errorMessage}`); } } + + const ignoredLines = [ + `This error originated either by throwing inside of an async function without a catch block`, + `Unexpected error attempting to determine if executable file exists`, + `dri3 extension not supported`, + `Failed to connect to the bus:`, + `Checking for beta autoupdate feature for deb/rpm distributions`, + `Found package-type: deb`, + `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` + ]; + output = output.split(os.EOL).filter(line => !ignoredLines.some(ignored => line.includes(ignored))).join(os.EOL); } return output; From 676aa21ac25e38adbdb286cca953b6be1aa76199 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 17:29:46 -0400 Subject: [PATCH 56/90] format --- src/unity-hub.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 427289c..d768e0a 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -136,7 +136,10 @@ export class UnityHub { `Found package-type: deb`, `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` ]; - output = output.split(os.EOL).filter(line => !ignoredLines.some(ignored => line.includes(ignored))).join(os.EOL); + output = output.split(os.EOL) + .filter(line => line.trim().length > 0) + .filter(line => !ignoredLines.some(ignored => line.includes(ignored))) + .join(os.EOL); } return output; From 944f2ba40591c4a61e936862bc370d45b3b778f5 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 17:41:18 -0400 Subject: [PATCH 57/90] fix debug lines --- src/logging.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 3404fd9..4722450 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -32,13 +32,22 @@ export class Logger { if (this.shouldLog(level)) { switch (this._ci) { case 'GITHUB_ACTIONS': { - if (level === LogLevel.CI || - level === LogLevel.INFO) { - process.stdout.write(`${message}${os.EOL}`, ...optionalParams); - break; + switch (level) { + case LogLevel.DEBUG: { + message.toString().split(os.EOL).forEach((line: string) => { + process.stdout.write(`::debug::${line}${os.EOL}`, ...optionalParams); + }); + } + case LogLevel.CI: + case LogLevel.INFO: { + process.stdout.write(`${message}${os.EOL}`, ...optionalParams); + break; + } + default: { + process.stdout.write(`::${level}::${message}${os.EOL}`, ...optionalParams); + break; + } } - - process.stdout.write(`::${level}::${message}${os.EOL}`, ...optionalParams); break; } default: { From 4b827008033e4680e55c46ad31730334cd328f72 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 17:54:44 -0400 Subject: [PATCH 58/90] break --- src/logging.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logging.ts b/src/logging.ts index 4722450..b281911 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -37,6 +37,7 @@ export class Logger { message.toString().split(os.EOL).forEach((line: string) => { process.stdout.write(`::debug::${line}${os.EOL}`, ...optionalParams); }); + break; } case LogLevel.CI: case LogLevel.INFO: { From 91b42e4b88a11e846ce27cf5a9e303e2816791ea Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 19:44:05 -0400 Subject: [PATCH 59/90] enable debug --- src/logging.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging.ts b/src/logging.ts index b281911..5b63192 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -16,7 +16,7 @@ export class Logger { private constructor() { if (process.env.GITHUB_ACTIONS) { this._ci = 'GITHUB_ACTIONS'; - this.logLevel = LogLevel.CI; + this.logLevel = process.env.ACTIONS_STEP_DEBUG === 'true' ? LogLevel.DEBUG : LogLevel.CI; } Logger.instance = this; From 4b4f7dffdac4fc69df10fe2d2480b572a6217fae Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 20:27:56 -0400 Subject: [PATCH 60/90] new lines instead of EOL --- src/android-sdk.ts | 4 ++-- src/index.ts | 10 +++++----- src/license-client.ts | 2 +- src/logging.ts | 22 ++++++++++------------ src/unity-hub.ts | 8 ++++---- src/utilities.ts | 2 +- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 004dcb4..e0112ce 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -115,7 +115,7 @@ async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: numbe } async function execSdkManager(sdkManagerPath: string, javaPath: string, args: string[]): Promise { - const acceptBuffer = Buffer.from(Array(10).fill('y').join(os.EOL), 'utf8'); + const acceptBuffer = Buffer.from(Array(10).fill('y').join('\n'), 'utf8'); let output = ''; let exitCode = 0; @@ -155,7 +155,7 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st }); child.on('close', (code: number | null) => { - process.stdout.write(os.EOL); + process.stdout.write('\n'); resolve(code === null ? 0 : code); }); }); diff --git a/src/index.ts b/src/index.ts index 10597b3..43ee0e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ program.command('hub-version') .action(async () => { const unityHub = new UnityHub(); const version = await unityHub.Version(); - process.stdout.write(`${version}${os.EOL}`); + process.stdout.write(`${version}\n`); }); program.command('hub-install') @@ -117,9 +117,9 @@ program.command('hub-install') const hubPath = await unityHub.Install(); if (options.json) { - process.stdout.write(`$${JSON.stringify({ UNITY_HUB: hubPath })}${os.EOL}`); + process.stdout.write(`$${JSON.stringify({ UNITY_HUB: hubPath })}\n`); } else { - process.stdout.write(`${hubPath}${os.EOL}`); + process.stdout.write(`${hubPath}\n`); } }); @@ -127,7 +127,7 @@ program.command('hub-path') .description('Print the path to the Unity Hub executable.') .action(async () => { const hub = new UnityHub(); - process.stdout.write(`${hub.executable}${os.EOL}`); + process.stdout.write(`${hub.executable}\n`); }); program.command('hub') @@ -190,7 +190,7 @@ program.command('setup-unity') } if (options.json) { - process.stdout.write(`$${JSON.stringify(output)}${os.EOL}`); + process.stdout.write(`$${JSON.stringify(output)}\n`); } }); diff --git a/src/license-client.ts b/src/license-client.ts index 7e1ed25..0ffcd5d 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -254,7 +254,7 @@ export class LicensingClient { }); child.on('close', (code) => { - process.stdout.write(os.EOL); + process.stdout.write('\n'); resolve(code === null ? 0 : code); }); }); diff --git a/src/logging.ts b/src/logging.ts index 5b63192..de54207 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,5 +1,3 @@ -import * as os from 'os'; - export enum LogLevel { DEBUG = 'debug', CI = 'ci', @@ -34,18 +32,18 @@ export class Logger { case 'GITHUB_ACTIONS': { switch (level) { case LogLevel.DEBUG: { - message.toString().split(os.EOL).forEach((line: string) => { - process.stdout.write(`::debug::${line}${os.EOL}`, ...optionalParams); + message.toString().split('\n').forEach((line: string) => { + process.stdout.write(`::debug::${line}\n`, ...optionalParams); }); break; } case LogLevel.CI: case LogLevel.INFO: { - process.stdout.write(`${message}${os.EOL}`, ...optionalParams); + process.stdout.write(`${message}\n`, ...optionalParams); break; } default: { - process.stdout.write(`::${level}::${message}${os.EOL}`, ...optionalParams); + process.stdout.write(`::${level}::${message}\n`, ...optionalParams); break; } } @@ -60,7 +58,7 @@ export class Logger { [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red }[level] || clear; // Default to no color / White - process.stdout.write(`${stringColor}${message}${clear}${os.EOL}`, ...optionalParams); + process.stdout.write(`${stringColor}${message}${clear}\n`, ...optionalParams); break; } } @@ -75,13 +73,13 @@ export class Logger { case 'GITHUB_ACTIONS': { // if there is newline in message, only use the first line for group title // then print the rest of the lines inside the group in cyan color - const firstLine: string = message.toString().split(os.EOL)[0]; - const restLines: string[] = message.toString().split(os.EOL).slice(1); + const firstLine: string = message.toString().split('\n')[0]; + const restLines: string[] = message.toString().split('\n').slice(1); const cyan = '\x1b[36m'; const clear = '\x1b[0m'; - process.stdout.write(`::group::${firstLine}${os.EOL}`, ...optionalParams); + process.stdout.write(`::group::${firstLine}\n`, ...optionalParams); restLines.forEach(line => { - process.stdout.write(`${cyan}${line}${clear}${os.EOL}`, ...optionalParams); + process.stdout.write(`${cyan}${line}${clear}\n`, ...optionalParams); }); break; } @@ -99,7 +97,7 @@ export class Logger { public endGroup(): void { switch (this._ci) { case 'GITHUB_ACTIONS': { - process.stdout.write(`::endgroup::${os.EOL}`); + process.stdout.write(`::endgroup::\n`); break; } default: { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index d768e0a..904caf1 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -101,7 +101,7 @@ export class UnityHub { }); child.on('close', (code) => { - process.stdout.write(os.EOL); + process.stdout.write('\n'); resolve(code === null ? 0 : code); }); }); @@ -136,10 +136,10 @@ export class UnityHub { `Found package-type: deb`, `XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid` ]; - output = output.split(os.EOL) + output = output.split('\n') .filter(line => line.trim().length > 0) .filter(line => !ignoredLines.some(ignored => line.includes(ignored))) - .join(os.EOL); + .join('\n'); } return output; @@ -482,7 +482,7 @@ chmod -R 777 "$hubPath"`]); */ public async ListInstalledEditors(): Promise { const output = await this.Exec(['editors', '-i']); - return output.split(os.EOL) + return output.split('\n') .filter(line => line.trim().length > 0) .map(line => line.trim()); } diff --git a/src/utilities.ts b/src/utilities.ts index 0da57ea..6d30153 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -91,7 +91,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions }); child.on('close', (code) => { - process.stdout.write(os.EOL); + process.stdout.write('\n'); resolve(code === null ? 0 : code); }); }); From 60ad8a08ea131eb2c02af461a583db44824f0353 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 21:02:09 -0400 Subject: [PATCH 61/90] mask output for license client --- src/license-client.ts | 17 ++++++++++++++++- src/logging.ts | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/license-client.ts b/src/license-client.ts index 0ffcd5d..c389a0b 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -233,7 +233,6 @@ export class LicensingClient { function processOutput(data: Buffer) { const chunk = data.toString(); output += chunk; - process.stdout.write(chunk); } this.logger.startGroup(`\x1b[34m${this.licenseClientPath} ${args.join(' ')}\x1b[0m`); @@ -259,6 +258,14 @@ export class LicensingClient { }); }); } finally { + const maskedOutput = this.maskSerialInOutput(output); + const splitLines = maskedOutput.split(/\r?\n/); + + for (const line of splitLines) { + if (line === undefined || line.length === 0) { continue; } + this.logger.info(line); + } + this.logger.endGroup(); if (exitCode !== 0) { @@ -270,6 +277,14 @@ export class LicensingClient { return output; } + private maskSerialInOutput(output: string): string { + return output.replace(/([\w-]+-XXXX)/g, (_, serial) => { + const maskedSerial = serial.slice(0, -4) + `XXXX`; + this.logger.mask(maskedSerial); + return serial; + }); + } + public async Version(): Promise { await this.exec(['--version']); } diff --git a/src/logging.ts b/src/logging.ts index de54207..dd87da1 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -136,4 +136,17 @@ export class Logger { const levelOrder = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; return levelOrder.indexOf(level) >= levelOrder.indexOf(this.logLevel); } + + /** + * Masks a string in the console output in CI environments that support it. + * @param message The string to mask. + */ + public mask(message: string): void { + switch (this._ci) { + case 'GITHUB_ACTIONS': { + process.stdout.write(`::add-mask::${message}\n`); + break; + } + } + } } \ No newline at end of file From 40fb914d03d4290f1a07b78c1f83a01e4abc22c2 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 21:03:15 -0400 Subject: [PATCH 62/90] mask serial --- src/license-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/license-client.ts b/src/license-client.ts index c389a0b..646cf53 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { spawn } from 'child_process'; import { Logger } from './logging'; import { UnityHub } from './unity-hub'; -import { ExecOptions, ResolveGlobToPath } from './utilities'; +import { ResolveGlobToPath } from './utilities'; export enum LicenseType { personal = 'personal', @@ -405,6 +405,8 @@ export class LicensingClient { if (serial !== undefined && serial.length > 0) { serial = serial.trim(); args.push(`--serial`, serial); + const maskedSerial = serial.slice(0, -4) + `XXXX`; + this.logger.mask(maskedSerial); } if (licenseType === LicenseType.personal) { From f5924e30465c12ee8a439d52bf14323ac860a123 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 21:11:36 -0400 Subject: [PATCH 63/90] cleanup --- src/unity-editor.ts | 3 --- src/unity-hub.ts | 1 - src/unity-version.ts | 6 +++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index f069ffc..ef5cbe3 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -1,10 +1,7 @@ -import { Logger } from './logging.js'; import * as fs from 'fs'; import * as path from 'path'; export class UnityEditor { - private static logger: Logger = Logger.instance; - static async GetEditorRootPath(editorPath: string): Promise { let editorRootPath = editorPath; switch (process.platform) { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 904caf1..5a58c3f 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,5 +1,4 @@ import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; import * as yaml from 'yaml'; import { spawn } from 'child_process'; diff --git a/src/unity-version.ts b/src/unity-version.ts index 26db39f..ed67edd 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -1,11 +1,11 @@ +import * as os from 'os'; +import { Logger } from './logging'; import { SemVer, coerce, compare, satisfies } from 'semver'; -import { Logger } from './logging'; -import { arch } from 'os'; export class UnityVersion { public version: string; @@ -32,7 +32,7 @@ export class UnityVersion { this.semVer = coercedVersion; // Default to current architecture if not specified - architecture = architecture || (arch() === 'arm64' ? 'ARM64' : 'X86_64'); + architecture = architecture || (os.arch() === 'arm64' ? 'ARM64' : 'X86_64'); if (architecture === 'ARM64' && !this.isArmCompatible()) { this.architecture = 'X86_64'; From effeccf9c01abd0485e6cb4db0f094d68564c43f Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Thu, 25 Sep 2025 22:07:00 -0400 Subject: [PATCH 64/90] start --- src/android-sdk.ts | 2 +- src/index.ts | 22 ++++++++++++++++++ src/unity-editor.ts | 36 ++++++++++++++++++++++++++++-- src/unity-hub.ts | 2 +- src/utilities.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index e0112ce..84a4342 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -24,7 +24,7 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: logger.ci(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`); let sdkPath = undefined; await createRepositoryCfg(); - const rootEditorPath = await UnityEditor.GetEditorRootPath(editorPath); + const rootEditorPath = UnityEditor.GetEditorRootPath(editorPath); const projectSettingsPath = path.join(projectPath, 'ProjectSettings/ProjectSettings.asset'); const projectSettingsContent = await ReadFileContents(projectSettingsPath); const matchResult = projectSettingsContent.match(/(?<=AndroidTargetSdkVersion: )\d+/); diff --git a/src/index.ts b/src/index.ts index 43ee0e7..8141bd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { Logger, LogLevel } from './logging'; import { UnityVersion } from './unity-version'; import { UnityProject } from './unity-project'; import { CheckAndroidSdkInstalled } from './android-sdk'; +import { UnityEditor } from './unity-editor'; const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); @@ -194,4 +195,25 @@ program.command('setup-unity') } }); +program.command('run') + .description('Run command line args directly to the Unity Editor.') + .option('-e, --editor-path ', 'The path to the Unity Editor executable.') + .allowUnknownOption(true) + .argument('', 'Arguments to pass to the Unity Editor executable.') + .option('--verbose', 'Enable verbose logging.') + .action(async (args: string[], options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + + const editorPath = options.editorPath?.toString()?.trim() || process.env.UNITY_EDITOR; + + if (!editorPath || editorPath.length === 0) { + throw new Error('The Unity Editor path was not specified. Use -e or --editor-path to specify it, or set the UNITY_EDITOR environment variable.'); + } + + const unityEditor = new UnityEditor(editorPath); + await unityEditor.Exec(args); + }); + program.parse(process.argv); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index ef5cbe3..1aca0ef 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -1,8 +1,40 @@ import * as fs from 'fs'; import * as path from 'path'; +import { Logger } from './logging'; +import { getArgumentValue } from './utilities'; +import { + ChildProcessByStdio, + spawn +} from 'child_process'; export class UnityEditor { - static async GetEditorRootPath(editorPath: string): Promise { + public editorRootPath: string; + + private logger: Logger = Logger.instance; + + constructor(public editorPath: string) { + if (!fs.existsSync(editorPath)) { + throw new Error(`The Unity Editor path does not exist: ${editorPath}`); + } + + fs.accessSync(editorPath, fs.constants.X_OK); + this.editorRootPath = UnityEditor.GetEditorRootPath(editorPath); + } + + public async Exec(args: string[], options = { silent: false, showCommand: true }): Promise { + let output: string = ''; + let exitCode: number = 0; + + const logPath = getArgumentValue('-logFile', args); + + if (!logPath) { + throw Error('Log file path not specified in command arguments'); + } + + let unityProcess: ChildProcessByStdio; + } + + static GetEditorRootPath(editorPath: string): string { let editorRootPath = editorPath; switch (process.platform) { case 'darwin': @@ -15,7 +47,7 @@ export class UnityEditor { editorRootPath = path.join(editorPath, '../../'); break } - await fs.promises.access(editorRootPath, fs.constants.R_OK); + fs.accessSync(editorRootPath, fs.constants.R_OK); return editorRootPath; } } \ No newline at end of file diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 5a58c3f..553ba47 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -745,7 +745,7 @@ done args.push('-m', module); } - const editorRootPath = await UnityEditor.GetEditorRootPath(editorPath); + const editorRootPath = UnityEditor.GetEditorRootPath(editorPath); const modulesPath = path.join(editorRootPath, 'modules.json'); this.logger.debug(`Editor Modules Manifest:\n > "${modulesPath}"`); diff --git a/src/utilities.ts b/src/utilities.ts index 6d30153..2eb7995 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -154,4 +154,58 @@ export function GetTempDir(): string { } // fallback to current directory return os.tmpdir(); +} + +/** + * Get the value of a command line argument. + * @param value The name of the argument to retrieve. + * @param args The list of command line arguments. + * @returns The value of the argument or an error if not found. + */ +export function getArgumentValue(value: string, args: string[]): string | undefined { + const index = args.indexOf(value); + if (index === -1 || index === args.length - 1) { + throw Error(`Missing ${value} argument`); + } + return args[index + 1]; +} + +export interface ProcInfo { + pid: number; + ppid: number; + name: string; +} + +/** + * Attempts to kill a process with the given PID read from a PID file. + * @param pidFilePath The path to the PID file. + * @returns The PID of the killed process, or null if no process was killed. + */ +export async function tryKillPid(pidFilePath: string): Promise { + let pid: number | null = null; + try { + if (!fs.existsSync(pidFilePath)) { + logger.debug(`PID file does not exist: ${pidFilePath}`); + return null; + } + const fileHandle = await fs.promises.open(pidFilePath, 'r'); + try { + pid = parseInt(await fileHandle.readFile('utf8')); + logger.debug(`Killing process pid: ${pid}`); + process.kill(pid); + } catch (error) { + const nodeJsException = error as NodeJS.ErrnoException; + const errorCode = nodeJsException?.code; + if (errorCode !== 'ENOENT' && errorCode !== 'ESRCH') { + logger.error(`Failed to kill process:\n${JSON.stringify(error)}`); + } + } finally { + await fileHandle.close(); + await fs.promises.unlink(pidFilePath); + } + + } catch (error) { + // ignored + } + return pid; } \ No newline at end of file From 5a7db923b38b06a35b788e8d693e34d81aef6b6b Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 14:02:49 -0400 Subject: [PATCH 65/90] try to setup unity project --- .github/workflows/unity-build.yml | 8 +- src/android-sdk.ts | 9 +- src/index.ts | 87 ++++++++++- src/license-client.ts | 8 +- src/unity-editor.ts | 243 +++++++++++++++++++++++++++++- src/unity-hub.ts | 8 +- src/utilities.ts | 98 ++++++++---- 7 files changed, 400 insertions(+), 61 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 269dea9..da1a92f 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -21,11 +21,6 @@ jobs: runs-on: ${{ matrix.os }} permissions: contents: read - env: - VERSION: '' - EXPORT_OPTION: '' - BUILD_DIRECTORY: '' - UNITY_PROJECT_PATH: ${{ github.workspace }}/UnityProject steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -42,9 +37,10 @@ jobs: unity-cli hub-install unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" + unity-cli create-project --name "Unity Project" - name: Post Run if: always() shell: bash run: | set -xe - unity-cli return-license --license personal \ No newline at end of file + unity-cli return-license --license personal diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 84a4342..99b739d 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -132,6 +132,8 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st env: { ...process.env, JAVA_HOME: javaPath } }); + process.once('SIGINT', () => child.kill('SIGINT')); + process.once('SIGTERM', () => child.kill('SIGTERM')); child.stdout.on('data', (data: Buffer) => { const chunk = data.toString(); output += chunk; @@ -143,17 +145,12 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st process.stdout.write(chunk); }); - child.stderr.on('data', (data: Buffer) => { const chunk = data.toString(); output += chunk; process.stderr.write(chunk); }); - - child.on('error', (error: Error) => { - reject(error); - }); - + child.on('error', (error: Error) => reject(error)); child.on('close', (code: number | null) => { process.stdout.write('\n'); resolve(code === null ? 0 : code); diff --git a/src/index.ts b/src/index.ts index 8141bd6..8a9b25e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import 'source-map-support/register'; import * as os from 'os'; import { Command } from 'commander'; import { readFileSync } from 'fs'; -import { join } from 'path'; +import path, { join } from 'path'; import { LicenseType, LicensingClient } from './license-client'; import { PromptForSecretInput } from './utilities'; import { UnityHub } from './unity-hub'; @@ -42,6 +42,8 @@ program.command('activate-license') Logger.instance.logLevel = LogLevel.DEBUG; } + Logger.instance.debug(JSON.stringify(options)); + const client = new LicensingClient(); const licenseStr: string = options.license?.toString()?.trim(); @@ -81,6 +83,8 @@ program.command('return-license') Logger.instance.logLevel = LogLevel.DEBUG; } + Logger.instance.debug(JSON.stringify(options)); + const client = new LicensingClient(); const licenseStr: string = options.license?.toString()?.trim(); @@ -141,6 +145,8 @@ program.command('hub') Logger.instance.logLevel = LogLevel.DEBUG; } + Logger.instance.debug(JSON.stringify(options)); + const unityHub = new UnityHub(); await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); }); @@ -183,21 +189,82 @@ program.command('setup-unity') }; if (unityProject) { - output['UNITY_PROJECT'] = unityProject.projectPath; + output['UNITY_PROJECT_PATH'] = unityProject.projectPath; if (modules.includes('android')) { await CheckAndroidSdkInstalled(editorPath, unityProject.projectPath); } } + if (process.env.GITHUB_ACTIONS) { + process.env.UNITY_HUB_PATH = unityHub.executable; + process.env.UNITY_EDITOR = editorPath; + if (unityProject) { + process.env.UNITY_PROJECT_PATH = unityProject.projectPath; + } + } + if (options.json) { process.stdout.write(`$${JSON.stringify(output)}\n`); } }); +program.command('create-project') + .description('Create a new Unity project.') + .option('-n, --name ', 'The name of the new Unity project. If unspecified, the project will be created in the specified path or the current working directory.') + .option('-p, --path ', 'The path to create the new Unity project. If unspecified, the current working directory will be used.') + .option('-t, --template ', 'The name of the template package to use for creating the unity project. Supports regex patterns.', 'com.unity.template.3d(-cross-platform)?') + .option('-e, --unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') + .option('--verbose', 'Enable verbose logging.') + .option('--json', 'Prints the last line of output as JSON string.') + .action(async (options) => { + if (options.verbose) { + Logger.instance.logLevel = LogLevel.DEBUG; + } + + Logger.instance.debug(JSON.stringify(options)); + + const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; + + if (!editorPath || editorPath.length === 0) { + throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); + } + + const unityEditor = new UnityEditor(editorPath); + const templatePath = unityEditor.GetTemplatePath(options.template); + const projectName = options.name?.toString()?.trim(); + + let projectPath = options.path?.toString()?.trim() || process.cwd(); + + if (projectName && projectName.length > 0) { + projectPath = path.join(projectPath, projectName); + } + + await unityEditor.Run({ + editorPath: editorPath, + args: [ + '-quit', + '-nographics', + '-batchmode', + '-createProject', projectPath, + '-cloneFromTemplate', templatePath + ] + }); + + if (process.env.GITHUB_ACTIONS) { + process.env.UNITY_PROJECT_PATH = projectPath; + } + + if (options.json) { + process.stdout.write(`$${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`); + } + }); + program.command('run') .description('Run command line args directly to the Unity Editor.') - .option('-e, --editor-path ', 'The path to the Unity Editor executable.') + .option('-e, --editor-path ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') + .option('-p, --unity-project ', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.') + .option('-l, --log-name ', 'The name of the log file.') .allowUnknownOption(true) .argument('', 'Arguments to pass to the Unity Editor executable.') .option('--verbose', 'Enable verbose logging.') @@ -206,14 +273,26 @@ program.command('run') Logger.instance.logLevel = LogLevel.DEBUG; } + Logger.instance.debug(JSON.stringify(options)); + const editorPath = options.editorPath?.toString()?.trim() || process.env.UNITY_EDITOR; if (!editorPath || editorPath.length === 0) { throw new Error('The Unity Editor path was not specified. Use -e or --editor-path to specify it, or set the UNITY_EDITOR environment variable.'); } + const unityProjectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); + const unityProject = await UnityProject.GetProject(unityProjectPath); + + if (!unityProject) { + throw new Error(`The specified path is not a valid Unity project: ${unityProjectPath}`); + } + const unityEditor = new UnityEditor(editorPath); - await unityEditor.Exec(args); + await unityEditor.Run({ + editorPath: editorPath, + args: [...args] + }); }); program.parse(process.argv); diff --git a/src/license-client.ts b/src/license-client.ts index 646cf53..7e82bba 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -245,13 +245,11 @@ export class LicensingClient { stdio: ['ignore', 'pipe', 'pipe'] }); + process.once('SIGINT', () => child.kill('SIGINT')); + process.once('SIGTERM', () => child.kill('SIGTERM')); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - - child.on('error', (error) => { - reject(error); - }); - + child.on('error', (error) => reject(error)); child.on('close', (code) => { process.stdout.write('\n'); resolve(code === null ? 0 : code); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 1aca0ef..23e5f8b 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -1,15 +1,27 @@ import * as fs from 'fs'; import * as path from 'path'; import { Logger } from './logging'; -import { getArgumentValue } from './utilities'; import { + getArgumentValue, + killChildProcesses, + ProcInfo, + tryKillProcess +} from './utilities'; +import { + spawn, ChildProcessByStdio, - spawn } from 'child_process'; +export interface EditorCommand { + editorPath: string; + args: string[]; +} + export class UnityEditor { public editorRootPath: string; + private procInfo: ProcInfo | undefined; + private pidFile: string; private logger: Logger = Logger.instance; constructor(public editorPath: string) { @@ -19,19 +31,238 @@ export class UnityEditor { fs.accessSync(editorPath, fs.constants.X_OK); this.editorRootPath = UnityEditor.GetEditorRootPath(editorPath); + this.pidFile = path.join(process.env.RUNNER_TEMP || process.env.USERPROFILE || '.', '.unity', 'unity-editor-process-id.txt'); + } + + /** + * Get the full path to a Unity project template based on the provided template name or regex pattern. + * @param template The name or regex pattern of the template to find. + * @returns The full path to the matching template file. + * @throws Error if the template directory does not exist, no templates are found, or no matching template is found. + */ + public GetTemplatePath(template: string): string { + let templateDir: string; + let editorRoot = path.dirname(this.editorPath); + + if (process.platform === 'darwin') { + templateDir = path.join(path.dirname(editorRoot), 'Resources', 'PackageManager', 'ProjectTemplates'); + } else { + templateDir = path.join(editorRoot, 'Data', 'Resources', 'PackageManager', 'ProjectTemplates'); + } + + // Check if the template directory exists + if (!fs.existsSync(templateDir) || + !fs.statSync(templateDir).isDirectory()) { + throw new Error(`Template directory not found: ${templateDir}`); + } + + // Find all .tgz files in the template directory + const files = fs.readdirSync(templateDir) + .filter(f => f.endsWith('.tgz')) + .map(f => path.join(templateDir, f)); + + if (files.length === 0) { + throw new Error(`No templates found in ${templateDir}`); + } + + this.logger.ci(`Available templates:`); + files.forEach(f => this.logger.ci(` > ${path.basename(f)}`)); + + // Build a regex to match the template name and version (e.g., com.unity.template.3d.*[0-9]+\.[0-9]+\.[0-9]+\.tgz) + // Accepts either a full regex or a simple string + let regex: RegExp; + try { + regex = new RegExp(template + ".*[0-9]+\\.[0-9]+\\.[0-9]+\\.tgz"); + } catch (e) { + throw new Error(`Invalid template regex: ${template}`); + } + + // Filter files by regex + const matches = files.filter(f => regex.test(path.basename(f))); + + if (matches.length === 0) { + throw new Error(`${template} path not found in ${templateDir}!`); + } + + // Pick the longest match (as in the shell script: sort by length descending) + matches.sort((a, b) => b.length - a.length); + const templatePath = matches[0]; + + if (!templatePath) { + throw new Error('No matching template path found.'); + } + + return path.normalize(templatePath); } - public async Exec(args: string[], options = { silent: false, showCommand: true }): Promise { - let output: string = ''; - let exitCode: number = 0; + public async Run(command: EditorCommand): Promise { + let isCancelled = false; + const onCancel = async () => { + isCancelled = true; + await this.tryKillEditorProcess(); + }; + process.once('SIGINT', onCancel); + process.once('SIGTERM', onCancel); + let exitCode: number | undefined; + try { + this.logger.info(`[command]"${command.editorPath}" ${command.args.join(' ')}`); + exitCode = await this.exec(command, pInfo => { this.procInfo = pInfo; }); + } catch (error) { + this.logger.error(`Unity execution failed:\n${error}`); + if (!exitCode) { + exitCode = 1; + } + } finally { + if (!isCancelled) { + await this.tryKillEditorProcess(); - const logPath = getArgumentValue('-logFile', args); + if (exitCode !== 0) { + throw Error(`Unity failed with exit code ${exitCode}`); + } + } + } + } + + private async exec(command: EditorCommand, onPid: (pid: ProcInfo) => void): Promise { + const logPath = getArgumentValue('-logFile', command.args); if (!logPath) { throw Error('Log file path not specified in command arguments'); } let unityProcess: ChildProcessByStdio; + + if (process.platform === 'linux' && !command.args.includes('-nographics')) { + unityProcess = spawn( + 'xvfb-run', + [command.editorPath, ...command.args], { + stdio: ['ignore', 'ignore', 'ignore'], + detached: true, + env: { + ...process.env, + DISPLAY: ':99', + UNITY_THISISABUILDMACHINE: '1' + } + }); + } else { + unityProcess = spawn( + command.editorPath, + command.args, { + stdio: ['ignore', 'ignore', 'ignore'], + detached: true, + env: { + ...process.env, + UNITY_THISISABUILDMACHINE: '1' + } + }); + } + + const processId = unityProcess.pid; + + if (!processId) { + throw new Error('Failed to start Unity process!'); + } + + onPid({ pid: processId, ppid: process.pid, name: command.editorPath }); + this.logger.debug(`Unity process started with pid: ${processId}`); + // make sure the directory for the PID file exists + const pidDir = path.dirname(this.pidFile); + + if (!fs.existsSync(pidDir)) { + fs.mkdirSync(pidDir, { recursive: true }); + } else { + try { + await fs.promises.access(this.pidFile, fs.constants.R_OK | fs.constants.W_OK); + if (this.procInfo) { + const killedPid = await tryKillProcess(this.procInfo); + if (killedPid) { + this.logger.warn(`Killed existing Unity process with pid: ${killedPid}`); + } + } + } catch { + // PID file does not exist, continue + } + } + // Write the PID to the PID file + fs.writeFileSync(this.pidFile, String(processId)); + const logPollingInterval = 100; // milliseconds + // Wait for log file to appear + while (!fs.existsSync(logPath)) { + await new Promise(res => setTimeout(res, logPollingInterval)); + } + // Start tailing the log file + let lastSize = 0; + let logEnded = false; + + const tailLog = async () => { + while (!logEnded) { + try { + const stats = fs.statSync(logPath); + if (stats.size > lastSize) { + const fd = fs.openSync(logPath, 'r'); + const buffer = Buffer.alloc(stats.size - lastSize); + fs.readSync(fd, buffer, 0, buffer.length, lastSize); + process.stdout.write(buffer.toString('utf8')); + fs.closeSync(fd); + lastSize = stats.size; + } + } catch (error) { + // ignore read errors + } + await new Promise(res => setTimeout(res, logPollingInterval)); + } + // Write a newline at the end of the log tail + // prevents appending logs from being printed on the same line + process.stdout.write('\n'); + }; + const timeout = 10000; // 10 seconds + // Start log tailing in background + const tailPromise = tailLog(); + const exitCode: number = await new Promise((resolve, reject) => { + unityProcess.on('exit', (code: number) => { + setTimeout(() => { + logEnded = true; + resolve(code ?? 1); + }, timeout); + }); + unityProcess.on('error', (error: Error) => { + setTimeout(() => { + logEnded = true; + reject(error); + }, timeout); + }); + }); + // Wait for log tailing to finish + await tailPromise; + // Wait for log file to be unlocked + const start = Date.now(); + let fileLocked = true; + + while (fileLocked && Date.now() - start < timeout) { + try { + if (fs.existsSync(logPath)) { + const fd = fs.openSync(logPath, 'r+'); + fs.closeSync(fd); + fileLocked = false; + } else { + fileLocked = false; + } + } catch { + fileLocked = true; + await new Promise(res => setTimeout(res, logPollingInterval)); + } + } + + return exitCode; + } + + private async tryKillEditorProcess(): Promise { + if (this.procInfo) { + await tryKillProcess(this.procInfo); + await killChildProcesses(this.procInfo); + } else { + this.logger.debug('No Unity process info available to kill.'); + } } static GetEditorRootPath(editorPath: string): string { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 553ba47..8908d97 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -92,13 +92,11 @@ export class UnityHub { stdio: ['ignore', 'pipe', 'pipe'], }); + process.once('SIGINT', () => child.kill('SIGINT')); + process.once('SIGTERM', () => child.kill('SIGTERM')); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - - child.on('error', (error) => { - reject(error); - }); - + child.on('error', (error) => reject(error)); child.on('close', (code) => { process.stdout.write('\n'); resolve(code === null ? 0 : code); diff --git a/src/utilities.ts b/src/utilities.ts index 2eb7995..969032b 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -6,7 +6,7 @@ import * as https from 'https'; import * as readline from 'readline'; import * as os from 'os'; import { spawn } from 'child_process'; -import { Logger } from './logging'; +import { Logger, LogLevel } from './logging'; const logger = Logger.instance; @@ -17,11 +17,8 @@ const logger = Logger.instance; */ export async function ResolveGlobToPath(globs: string[]): Promise { const globPath: string = path.join(...globs).split(path.sep).join('/'); - // logger.debug(`glob: ${globPath}`); const paths: string[] = await glob.glob(globPath); - // logger.debug(`Resolved "${globPath}" to ${paths.length} paths:\n > ${paths.join('\n > ')}`); - for (const path of paths) { await fs.promises.access(path, fs.constants.R_OK); return path; @@ -55,6 +52,14 @@ export type ExecOptions = { showCommand?: boolean; } +/** + * Executes a command with arguments and options. + * @param command The command to execute. + * @param args The arguments for the command. + * @param options Options for the execution. `silent` controls console output, `showCommand` controls if the command is logged. If LogLevel is DEBUG, both are overridden to show command and not be silent. + * @returns The output of the command. + * @throws An error if the command returns a non-zero exit code. + */ export async function Exec(command: string, args: string[], options: ExecOptions = { silent: false, showCommand: true }): Promise { let output: string = ''; let exitCode: number = 0; @@ -63,12 +68,12 @@ export async function Exec(command: string, args: string[], options: ExecOptions const chunk = data.toString(); output += chunk; - if (!options.silent) { + if (!options.silent || logger.logLevel === LogLevel.DEBUG) { process.stdout.write(chunk); } } - if (options.showCommand) { + if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { logger.startGroup(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); } @@ -82,23 +87,21 @@ export async function Exec(command: string, args: string[], options: ExecOptions env: process.env, stdio: ['ignore', 'pipe', 'pipe'], }); - + process.once('SIGINT', () => child.kill('SIGINT')); + process.once('SIGTERM', () => child.kill('SIGTERM')); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - - child.on('error', (error) => { - reject(error); - }); - + child.on('error', (error) => reject(error)); child.on('close', (code) => { process.stdout.write('\n'); resolve(code === null ? 0 : code); }); }); } finally { - if (options.showCommand) { + if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { logger.endGroup(); } + if (exitCode !== 0) { throw new Error(`${command} failed with exit code ${exitCode}`); } @@ -164,9 +167,11 @@ export function GetTempDir(): string { */ export function getArgumentValue(value: string, args: string[]): string | undefined { const index = args.indexOf(value); + if (index === -1 || index === args.length - 1) { throw Error(`Missing ${value} argument`); } + return args[index + 1]; } @@ -177,35 +182,70 @@ export interface ProcInfo { } /** - * Attempts to kill a process with the given PID read from a PID file. - * @param pidFilePath The path to the PID file. - * @returns The PID of the killed process, or null if no process was killed. + * Attempts to kill a process with the given ProcInfo. + * @param procInfo The process information containing the PID. + * @returns The PID of the killed process, or undefined if no process was killed. */ -export async function tryKillPid(pidFilePath: string): Promise { - let pid: number | null = null; +export async function tryKillProcess(procInfo: ProcInfo): Promise { + let pid: number | undefined; + + try { + pid = procInfo.pid; + logger.debug(`Killing process pid: ${pid}`); + process.kill(pid); + } catch (error) { + const nodeJsException = error as NodeJS.ErrnoException; + const errorCode = nodeJsException?.code; + + if (errorCode !== 'ENOENT' && errorCode !== 'ESRCH') { + logger.error(`Failed to kill process:\n${JSON.stringify(error)}`); + } + } + + return pid; +} + +export async function readPidFile(pidFilePath: string): Promise { + let procInfo: ProcInfo | undefined; try { if (!fs.existsSync(pidFilePath)) { logger.debug(`PID file does not exist: ${pidFilePath}`); - return null; + return procInfo; } + const fileHandle = await fs.promises.open(pidFilePath, 'r'); try { - pid = parseInt(await fileHandle.readFile('utf8')); - logger.debug(`Killing process pid: ${pid}`); - process.kill(pid); - } catch (error) { - const nodeJsException = error as NodeJS.ErrnoException; - const errorCode = nodeJsException?.code; - if (errorCode !== 'ENOENT' && errorCode !== 'ESRCH') { - logger.error(`Failed to kill process:\n${JSON.stringify(error)}`); + const pid = parseInt(await fileHandle.readFile('utf8')); + + if (isNaN(pid)) { + logger.error(`Invalid PID in file: ${pidFilePath}`); + return procInfo; } + + procInfo = { pid, ppid: 0, name: '' }; + } catch (error) { + logger.error(`Failed to read PID file: ${pidFilePath}\n${error}`); } finally { await fileHandle.close(); await fs.promises.unlink(pidFilePath); } - } catch (error) { // ignored } - return pid; + + return procInfo; +} + +export async function killChildProcesses(procInfo: ProcInfo): Promise { + try { + if (process.platform === 'win32') { + const pwshCommand = 'powershell -Command "Get-CimInstance Win32_Process -Filter \'ParentProcessId=' + procInfo.pid + '\' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"'; + await Exec('cmd', ['/c', pwshCommand]); + } else { // linux and macos + const unixCommand = `pgrep -P ${procInfo.pid} | xargs -r kill`; + await Exec('sudo', ['sh', '-c', unixCommand]); + } + } catch (error) { + logger.error(`Failed to kill child processes of pid ${procInfo.pid}:\n${JSON.stringify(error)}`); + } } \ No newline at end of file From cb19468fcc39eb7a4bc39f4a4271d02d1b721048 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 14:47:19 -0400 Subject: [PATCH 66/90] tail last line --- .github/workflows/unity-build.yml | 7 ++++--- src/index.ts | 14 +------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index da1a92f..0b4fb2b 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: 20.x - name: unity-cli shell: bash run: | @@ -36,8 +36,9 @@ jobs: unity-cli --version unity-cli hub-install unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" - unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" - unity-cli create-project --name "Unity Project" + setup_output=$(unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" --json) + unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') + project_path=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') - name: Post Run if: always() shell: bash diff --git a/src/index.ts b/src/index.ts index 8a9b25e..2495e63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -148,7 +148,7 @@ program.command('hub') Logger.instance.debug(JSON.stringify(options)); const unityHub = new UnityHub(); - await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); + await unityHub.Exec(args, { silent: false, showCommand: false }); }); program.command('setup-unity') @@ -196,14 +196,6 @@ program.command('setup-unity') } } - if (process.env.GITHUB_ACTIONS) { - process.env.UNITY_HUB_PATH = unityHub.executable; - process.env.UNITY_EDITOR = editorPath; - if (unityProject) { - process.env.UNITY_PROJECT_PATH = unityProject.projectPath; - } - } - if (options.json) { process.stdout.write(`$${JSON.stringify(output)}\n`); } @@ -251,10 +243,6 @@ program.command('create-project') ] }); - if (process.env.GITHUB_ACTIONS) { - process.env.UNITY_PROJECT_PATH = projectPath; - } - if (options.json) { process.stdout.write(`$${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`); } From cab5eb11646fc519960e0aef6a967f81f39711bd Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 14:59:37 -0400 Subject: [PATCH 67/90] print output --- .github/workflows/unity-build.yml | 7 ++++++- src/index.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 0b4fb2b..69259c0 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -37,8 +37,13 @@ jobs: unity-cli hub-install unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" setup_output=$(unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" --json) + echo "$setup_output" unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') - project_path=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') + echo "$unity_editor_path" + create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path") + echo "$create_project_output" + project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') + echo "$project_path" - name: Post Run if: always() shell: bash diff --git a/src/index.ts b/src/index.ts index 2495e63..69d4bd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,7 +197,7 @@ program.command('setup-unity') } if (options.json) { - process.stdout.write(`$${JSON.stringify(output)}\n`); + process.stdout.write(`\n${JSON.stringify(output)}`); } }); @@ -244,7 +244,7 @@ program.command('create-project') }); if (options.json) { - process.stdout.write(`$${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`); + process.stdout.write(`\n${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}`); } }); From 8998cd1115007548d317355918e2dfac5e52ac17 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 15:34:40 -0400 Subject: [PATCH 68/90] rework some internals for editor cli --- src/index.ts | 3 +- src/unity-editor.ts | 71 ++++++++++++++++++++++++++++++++++++++------- src/utilities.ts | 4 +-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 69d4bd0..b9597f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -233,7 +233,7 @@ program.command('create-project') } await unityEditor.Run({ - editorPath: editorPath, + projectPath: projectPath, args: [ '-quit', '-nographics', @@ -278,7 +278,6 @@ program.command('run') const unityEditor = new UnityEditor(editorPath); await unityEditor.Run({ - editorPath: editorPath, args: [...args] }); }); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 23e5f8b..e67e710 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { Logger } from './logging'; import { - getArgumentValue, + getArgumentValueAsString, killChildProcesses, ProcInfo, tryKillProcess @@ -11,10 +11,11 @@ import { spawn, ChildProcessByStdio, } from 'child_process'; +import { UnityVersion } from './unity-version'; export interface EditorCommand { - editorPath: string; args: string[]; + projectPath?: string; } export class UnityEditor { @@ -23,6 +24,7 @@ export class UnityEditor { private procInfo: ProcInfo | undefined; private pidFile: string; private logger: Logger = Logger.instance; + private autoAddNoGraphics: boolean; constructor(public editorPath: string) { if (!fs.existsSync(editorPath)) { @@ -32,6 +34,20 @@ export class UnityEditor { fs.accessSync(editorPath, fs.constants.X_OK); this.editorRootPath = UnityEditor.GetEditorRootPath(editorPath); this.pidFile = path.join(process.env.RUNNER_TEMP || process.env.USERPROFILE || '.', '.unity', 'unity-editor-process-id.txt'); + + const match = editorPath.match(/(?\d+)\.(?\d+)\.(?\d+)/); + + if (!match) { + throw Error(`Invalid Unity Editor Path: ${editorPath}`); + } + + const unityMajorVersion = match.groups?.major; + + if (!unityMajorVersion) { + throw Error(`Invalid Unity Major Version: ${editorPath}`); + } + + this.autoAddNoGraphics = parseInt(unityMajorVersion, 10) > 2018; } /** @@ -105,10 +121,13 @@ export class UnityEditor { process.once('SIGTERM', onCancel); let exitCode: number | undefined; try { - this.logger.info(`[command]"${command.editorPath}" ${command.args.join(' ')}`); + this.logger.info(`[command]"${this.editorPath}" ${command.args.join(' ')}`); exitCode = await this.exec(command, pInfo => { this.procInfo = pInfo; }); } catch (error) { - this.logger.error(`Unity execution failed:\n${error}`); + if (error instanceof Error) { + this.logger.error(error.toString()); + } + if (!exitCode) { exitCode = 1; } @@ -124,18 +143,50 @@ export class UnityEditor { } private async exec(command: EditorCommand, onPid: (pid: ProcInfo) => void): Promise { - const logPath = getArgumentValue('-logFile', command.args); + if (!command.args || command.args.length === 0) { + throw Error('No command arguments provided for Unity execution'); + } - if (!logPath) { - throw Error('Log file path not specified in command arguments'); + if (!command.args.includes(`-automated`)) { + command.args.push(`-automated`); } + if (!command.args.includes(`-batchmode`)) { + command.args.push(`-batchmode`); + } + + if (this.autoAddNoGraphics && + !command.args.includes(`-nographics`) && + !command.args.includes(`-force-graphics`)) { + command.args.push(`-nographics`); + } + + if (!command.args.includes('-logFile')) { + const logsDir = command.projectPath !== undefined + ? path.join(command.projectPath, 'Builds', 'Logs') + : path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'Logs'); + + try { + await fs.promises.access(logsDir, fs.constants.R_OK); + } catch (error) { + this.logger.debug(`Creating Logs Directory:\n > "${logsDir}"`); + await fs.promises.mkdir(logsDir, { recursive: true }); + } + + const timestamp = new Date().toISOString().replace(/[-:]/g, ``).replace(/\..+/, ``); + const generatedLogPath = path.join(logsDir, `Unity-${timestamp}.log`); + this.logger.debug(`Log File Path:\n > "${generatedLogPath}"`); + command.args.push('-logFile', generatedLogPath); + } + + const logPath: string = getArgumentValueAsString('-logFile', command.args); + let unityProcess: ChildProcessByStdio; if (process.platform === 'linux' && !command.args.includes('-nographics')) { unityProcess = spawn( 'xvfb-run', - [command.editorPath, ...command.args], { + [this.editorPath, ...command.args], { stdio: ['ignore', 'ignore', 'ignore'], detached: true, env: { @@ -146,7 +197,7 @@ export class UnityEditor { }); } else { unityProcess = spawn( - command.editorPath, + this.editorPath, command.args, { stdio: ['ignore', 'ignore', 'ignore'], detached: true, @@ -163,7 +214,7 @@ export class UnityEditor { throw new Error('Failed to start Unity process!'); } - onPid({ pid: processId, ppid: process.pid, name: command.editorPath }); + onPid({ pid: processId, ppid: process.pid, name: this.editorPath }); this.logger.debug(`Unity process started with pid: ${processId}`); // make sure the directory for the PID file exists const pidDir = path.dirname(this.pidFile); diff --git a/src/utilities.ts b/src/utilities.ts index 969032b..b8d4370 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -165,14 +165,14 @@ export function GetTempDir(): string { * @param args The list of command line arguments. * @returns The value of the argument or an error if not found. */ -export function getArgumentValue(value: string, args: string[]): string | undefined { +export function getArgumentValueAsString(value: string, args: string[]): string { const index = args.indexOf(value); if (index === -1 || index === args.length - 1) { throw Error(`Missing ${value} argument`); } - return args[index + 1]; + return args[index + 1] as string; } export interface ProcInfo { From 3ca241d642ea5aa42e555a932e8d9c1a20f10363 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 15:49:48 -0400 Subject: [PATCH 69/90] set a small timeout after download --- src/utilities.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utilities.ts b/src/utilities.ts index b8d4370..af5abf8 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -125,6 +125,8 @@ export async function DownloadFile(url: string, downloadPath: string): Promise reject(`Download failed: ${error}`)); }); }); + // make sure the file is closed and accessible + await new Promise((r) => setTimeout(r, 100)); await fs.promises.access(downloadPath, fs.constants.R_OK | fs.constants.X_OK); } From e9c31333b88fff221571890da41bf4b77f742660 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 15:51:44 -0400 Subject: [PATCH 70/90] ignore if no child processes --- src/utilities.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utilities.ts b/src/utilities.ts index af5abf8..2da284e 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -245,7 +245,17 @@ export async function killChildProcesses(procInfo: ProcInfo): Promise { await Exec('cmd', ['/c', pwshCommand]); } else { // linux and macos const unixCommand = `pgrep -P ${procInfo.pid} | xargs -r kill`; - await Exec('sudo', ['sh', '-c', unixCommand]); + try { + await Exec('sudo', ['sh', '-c', unixCommand]); + } catch (error: any) { + // Accept exit code 5 (no child processes found) as non-error + if (error.message && + error.message.includes('exit code 5')) { + logger.debug(`No child processes found for pid ${procInfo.pid}.`); + } else { + throw error; + } + } } } catch (error) { logger.error(`Failed to kill child processes of pid ${procInfo.pid}:\n${JSON.stringify(error)}`); From 625213e6f239d4c38dce32dd7a32f9ec1665fd8f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:03:17 -0400 Subject: [PATCH 71/90] silence some outputs try killing child processes differently on linux/macOS --- src/unity-hub.ts | 24 ++++++++++++------------ src/utilities.ts | 39 ++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 8908d97..8b24d5a 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -207,7 +207,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.info(`Running Unity Hub installer...`); try { - await Exec(downloadPath, ['/S']); + await Exec(downloadPath, ['/S'], { silent: true }); } finally { fs.promises.unlink(downloadPath); } @@ -227,7 +227,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.debug(`Mounting DMG...`); try { - const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse']); + const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse'], { silent: true }); // can be "/Volumes/Unity Hub 3.13.1-arm64" or "/Volumes/Unity Hub 3.13.1" const mountPointMatch = output.match(/\/Volumes\/Unity Hub.*$/m); @@ -243,16 +243,16 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); if (fs.existsSync('/Applications/Unity Hub.app')) { - await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app']); + await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app'], { silent: true }); } - await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app']); - await Exec('sudo', ['chmod', '777', '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub']); - await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity']); - await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity']); + await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app'], { silent: true }); + await Exec('sudo', ['chmod', '777', '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'], { silent: true }); + await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity'], { silent: true }); + await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity'], { silent: true }); } finally { try { if (mountPoint && mountPoint.length > 0) { - await Exec('hdiutil', ['detach', mountPoint, '-quiet']); + await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true }); } } finally { await fs.promises.unlink(downloadPath); @@ -854,7 +854,7 @@ done this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); try { - await Exec(installerPath, ['/S', `/D=${installPath}`, '-Wait', '-NoNewWindow']); + await Exec(installerPath, ['/S', `/D=${installPath}`, '-Wait', '-NoNewWindow'], { silent: true }); } catch (error) { this.logger.error(`Failed to install Unity ${unityVersion.toString()}: ${error}`); } finally { @@ -878,7 +878,7 @@ done let mountPoint = ''; try { - const output = await Exec('hdiutil', ['attach', installerPath, '-nobrowse']); + const output = await Exec('hdiutil', ['attach', installerPath, '-nobrowse'], { silent: true }); const mountPointMatch = output.match(/\/Volumes\/Unity Installer.*$/m); if (!mountPointMatch || mountPointMatch.length === 0) { @@ -892,7 +892,7 @@ done await fs.promises.access(pkgPath, fs.constants.R_OK); this.logger.debug(`Found .pkg installer: ${pkgPath}`); - await Exec('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR']); + await Exec('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR'], { silent: true }); const unityAppPath = path.join('/Applications', 'Unity'); const targetPath = path.join(installDir, `Unity ${unityVersion.version}`); @@ -921,7 +921,7 @@ done } finally { try { if (mountPoint && mountPoint.length > 0) { - await Exec('hdiutil', ['detach', mountPoint, '-quiet']); + await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true }); } } finally { await fs.promises.unlink(installerPath); diff --git a/src/utilities.ts b/src/utilities.ts index 2da284e..3ba4cc4 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -64,17 +64,25 @@ export async function Exec(command: string, args: string[], options: ExecOptions let output: string = ''; let exitCode: number = 0; + const isSilent = options.silent || logger.logLevel !== LogLevel.DEBUG; + function processOutput(data: Buffer) { const chunk = data.toString(); output += chunk; - if (!options.silent || logger.logLevel === LogLevel.DEBUG) { + if (!isSilent) { process.stdout.write(chunk); } } if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { - logger.startGroup(`\x1b[34m${command} ${args.join(' ')}\x1b[0m`); + const commandStr = `\x1b[34m${command} ${args.join(' ')}\x1b[0m`; + + if (isSilent) { + logger.info(commandStr); + } else { + logger.startGroup(commandStr); + } } if (command.includes(path.sep)) { @@ -99,7 +107,9 @@ export async function Exec(command: string, args: string[], options: ExecOptions }); } finally { if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { - logger.endGroup(); + if (!isSilent) { + logger.endGroup(); + } } if (exitCode !== 0) { @@ -244,16 +254,19 @@ export async function killChildProcesses(procInfo: ProcInfo): Promise { const pwshCommand = 'powershell -Command "Get-CimInstance Win32_Process -Filter \'ParentProcessId=' + procInfo.pid + '\' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"'; await Exec('cmd', ['/c', pwshCommand]); } else { // linux and macos - const unixCommand = `pgrep -P ${procInfo.pid} | xargs -r kill`; - try { - await Exec('sudo', ['sh', '-c', unixCommand]); - } catch (error: any) { - // Accept exit code 5 (no child processes found) as non-error - if (error.message && - error.message.includes('exit code 5')) { - logger.debug(`No child processes found for pid ${procInfo.pid}.`); - } else { - throw error; + const psOutput = await Exec('ps', ['-eo', 'pid,ppid,comm']); + const lines = psOutput.split('\n').slice(1); // Skip header line + + for (const line of lines) { + const parts = line.trim().split(/\s+/, 3); + if (parts.length === 3) { + const pid = parseInt(parts[0]!, 10); + const ppid = parseInt(parts[1]!, 10); + const name = parts[2]!; + + if (ppid === procInfo.pid) { + await tryKillProcess({ pid, ppid, name }); + } } } } From df44961d413a3e237447da375186ad28d0218400 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:04:22 -0400 Subject: [PATCH 72/90] print json --- .github/workflows/unity-build.yml | 2 +- src/utilities.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 69259c0..b011234 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -40,7 +40,7 @@ jobs: echo "$setup_output" unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') echo "$unity_editor_path" - create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path") + create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" --json) echo "$create_project_output" project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') echo "$project_path" diff --git a/src/utilities.ts b/src/utilities.ts index 3ba4cc4..8263809 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -252,7 +252,7 @@ export async function killChildProcesses(procInfo: ProcInfo): Promise { try { if (process.platform === 'win32') { const pwshCommand = 'powershell -Command "Get-CimInstance Win32_Process -Filter \'ParentProcessId=' + procInfo.pid + '\' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"'; - await Exec('cmd', ['/c', pwshCommand]); + await Exec('cmd', ['/c', pwshCommand], { silent: true }); } else { // linux and macos const psOutput = await Exec('ps', ['-eo', 'pid,ppid,comm']); const lines = psOutput.split('\n').slice(1); // Skip header line From 74503a4122e86c56c8dc26284f347e5e4ffd1bc7 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:10:05 -0400 Subject: [PATCH 73/90] update exec output logic --- src/utilities.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utilities.ts b/src/utilities.ts index 8263809..dc6dfee 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -64,7 +64,9 @@ export async function Exec(command: string, args: string[], options: ExecOptions let output: string = ''; let exitCode: number = 0; - const isSilent = options.silent || logger.logLevel !== LogLevel.DEBUG; + const isDebug = logger.logLevel === LogLevel.DEBUG; + const isSilent = isDebug ? false : !!options.silent; + const mustShowCommand = isDebug ? true : !!options.showCommand; function processOutput(data: Buffer) { const chunk = data.toString(); @@ -75,7 +77,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions } } - if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { + if (mustShowCommand) { const commandStr = `\x1b[34m${command} ${args.join(' ')}\x1b[0m`; if (isSilent) { @@ -106,7 +108,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions }); }); } finally { - if (options.showCommand || logger.logLevel === LogLevel.DEBUG) { + if (mustShowCommand) { if (!isSilent) { logger.endGroup(); } From 51b140322385c696b2f81d45ac56b4203f755772 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:18:30 -0400 Subject: [PATCH 74/90] cleanup --- .github/workflows/unity-build.yml | 2 -- src/unity-editor.ts | 3 +-- src/utilities.ts | 8 ++++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index b011234..89cea25 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -37,11 +37,9 @@ jobs: unity-cli hub-install unity-cli activate-license --license personal --email "${{ secrets.UNITY_USERNAME }}" --password "${{ secrets.UNITY_PASSWORD }}" setup_output=$(unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" --json) - echo "$setup_output" unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') echo "$unity_editor_path" create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" --json) - echo "$create_project_output" project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') echo "$project_path" - name: Post Run diff --git a/src/unity-editor.ts b/src/unity-editor.ts index e67e710..114b861 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -11,7 +11,6 @@ import { spawn, ChildProcessByStdio, } from 'child_process'; -import { UnityVersion } from './unity-version'; export interface EditorCommand { args: string[]; @@ -300,7 +299,7 @@ export class UnityEditor { } } catch { fileLocked = true; - await new Promise(res => setTimeout(res, logPollingInterval)); + await new Promise(r => setTimeout(r, logPollingInterval)); } } diff --git a/src/utilities.ts b/src/utilities.ts index dc6dfee..84fbc17 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -65,8 +65,8 @@ export async function Exec(command: string, args: string[], options: ExecOptions let exitCode: number = 0; const isDebug = logger.logLevel === LogLevel.DEBUG; - const isSilent = isDebug ? false : !!options.silent; - const mustShowCommand = isDebug ? true : !!options.showCommand; + const isSilent = isDebug ? false : options.silent ? options.silent : false; + const mustShowCommand = isDebug ? true : options.showCommand ? options.showCommand : false; function processOutput(data: Buffer) { const chunk = data.toString(); @@ -205,7 +205,7 @@ export async function tryKillProcess(procInfo: ProcInfo): Promise { const pwshCommand = 'powershell -Command "Get-CimInstance Win32_Process -Filter \'ParentProcessId=' + procInfo.pid + '\' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"'; await Exec('cmd', ['/c', pwshCommand], { silent: true }); } else { // linux and macos - const psOutput = await Exec('ps', ['-eo', 'pid,ppid,comm']); + const psOutput = await Exec('ps', ['-eo', 'pid,ppid,comm'], { silent: true }); const lines = psOutput.split('\n').slice(1); // Skip header line for (const line of lines) { From a70b5c59de7f128880adea7cda611f4e12caa8d4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:53:48 -0400 Subject: [PATCH 75/90] cleanup --- src/index.ts | 21 +++++++++++++-------- src/utilities.ts | 9 +++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index b9597f2..8e66cff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,7 +106,7 @@ program.command('hub-version') .action(async () => { const unityHub = new UnityHub(); const version = await unityHub.Version(); - process.stdout.write(`${version}\n`); + process.stdout.write(version); }); program.command('hub-install') @@ -122,17 +122,22 @@ program.command('hub-install') const hubPath = await unityHub.Install(); if (options.json) { - process.stdout.write(`$${JSON.stringify({ UNITY_HUB: hubPath })}\n`); + process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hubPath })}`); } else { - process.stdout.write(`${hubPath}\n`); + process.stdout.write(hubPath); } }); program.command('hub-path') .description('Print the path to the Unity Hub executable.') - .action(async () => { + .option('--json', 'Prints the last line of output as JSON string.') + .action(async (options) => { const hub = new UnityHub(); - process.stdout.write(`${hub.executable}\n`); + if (options.json) { + process.stdout.write(`${JSON.stringify({ UNITY_HUB_PATH: hub.executable })}`); + } else { + process.stdout.write(hub.executable); + } }); program.command('hub') @@ -145,10 +150,10 @@ program.command('hub') Logger.instance.logLevel = LogLevel.DEBUG; } - Logger.instance.debug(JSON.stringify(options)); + Logger.instance.debug(JSON.stringify({ args, options })); const unityHub = new UnityHub(); - await unityHub.Exec(args, { silent: false, showCommand: false }); + await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); }); program.command('setup-unity') @@ -261,7 +266,7 @@ program.command('run') Logger.instance.logLevel = LogLevel.DEBUG; } - Logger.instance.debug(JSON.stringify(options)); + Logger.instance.debug(JSON.stringify({ options, args })); const editorPath = options.editorPath?.toString()?.trim() || process.env.UNITY_EDITOR; diff --git a/src/utilities.ts b/src/utilities.ts index 84fbc17..4b17205 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -205,7 +205,7 @@ export async function tryKillProcess(procInfo: ProcInfo): Promise { + logger.debug(`Killing child processes of ${procInfo.name} with pid: ${procInfo.pid}...`); try { if (process.platform === 'win32') { const pwshCommand = 'powershell -Command "Get-CimInstance Win32_Process -Filter \'ParentProcessId=' + procInfo.pid + '\' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"'; @@ -262,9 +263,9 @@ export async function killChildProcesses(procInfo: ProcInfo): Promise { for (const line of lines) { const parts = line.trim().split(/\s+/, 3); if (parts.length === 3) { - const pid = parseInt(parts[0]!, 10); - const ppid = parseInt(parts[1]!, 10); - const name = parts[2]!; + const pid: number = parseInt(parts[0]!, 10); + const ppid: number = parseInt(parts[1]!, 10); + const name: string = parts[2]!; if (ppid === procInfo.pid) { await tryKillProcess({ pid, ppid, name }); From 5369938ea6315699b0853e59855d8a521912a051 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 16:55:38 -0400 Subject: [PATCH 76/90] group editor cli exec --- src/unity-editor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 114b861..0c46d52 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -120,7 +120,8 @@ export class UnityEditor { process.once('SIGTERM', onCancel); let exitCode: number | undefined; try { - this.logger.info(`[command]"${this.editorPath}" ${command.args.join(' ')}`); + const commandStr = `\x1b[34m${this.editorPath} ${command.args.join(' ')}\x1b[0m`; + this.logger.startGroup(commandStr); exitCode = await this.exec(command, pInfo => { this.procInfo = pInfo; }); } catch (error) { if (error instanceof Error) { @@ -131,6 +132,7 @@ export class UnityEditor { exitCode = 1; } } finally { + this.logger.endGroup(); if (!isCancelled) { await this.tryKillEditorProcess(); From de7156b6228c5f01172f7f8e8a1bb70ad238ea74 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 17:00:02 -0400 Subject: [PATCH 77/90] line endings --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8e66cff..0abac3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,7 +106,7 @@ program.command('hub-version') .action(async () => { const unityHub = new UnityHub(); const version = await unityHub.Version(); - process.stdout.write(version); + process.stdout.write(`${version}\n`); }); program.command('hub-install') @@ -122,9 +122,9 @@ program.command('hub-install') const hubPath = await unityHub.Install(); if (options.json) { - process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hubPath })}`); + process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hubPath })}\n`); } else { - process.stdout.write(hubPath); + process.stdout.write(`${hubPath}\n`); } }); @@ -134,9 +134,9 @@ program.command('hub-path') .action(async (options) => { const hub = new UnityHub(); if (options.json) { - process.stdout.write(`${JSON.stringify({ UNITY_HUB_PATH: hub.executable })}`); + process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hub.executable })}\n`); } else { - process.stdout.write(hub.executable); + process.stdout.write(`${hub.executable}\n`); } }); @@ -202,7 +202,7 @@ program.command('setup-unity') } if (options.json) { - process.stdout.write(`\n${JSON.stringify(output)}`); + process.stdout.write(`\n${JSON.stringify(output)}\n`); } }); @@ -249,7 +249,7 @@ program.command('create-project') }); if (options.json) { - process.stdout.write(`\n${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}`); + process.stdout.write(`\n${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`); } }); From 01375d4641726dada038c562de2cef183cd9a243 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 17:29:22 -0400 Subject: [PATCH 78/90] build with node 22 --- .github/workflows/publish.yml | 6 +++--- .github/workflows/unity-build.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d0ba111..74891bb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,8 +12,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' + node-version: 22.x + registry-url: "https://registry.npmjs.org" - run: | npm ci npm run build @@ -26,4 +26,4 @@ jobs: fi npm publish --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 89cea25..4583ff0 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: unity-cli shell: bash run: | From 15a2ba23cfe0b3a0b4e0fb09de17e16ef9779840 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 17:49:43 -0400 Subject: [PATCH 79/90] add validate and build commands --- .github/workflows/unity-build.yml | 6 +++++ src/index.ts | 6 +++++ src/unity-editor.ts | 37 ++++++++++++++++++------------- src/unity-project.ts | 4 ++++ 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 4583ff0..3a896b5 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -42,6 +42,12 @@ jobs: create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" --json) project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') echo "$project_path" + npm install -g openupm-cli + openupm add com.utilities.buildpipeline -chdir "$project_path" + validate_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) + echo "$validate_output" + build_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) + echo "$build_output" - name: Post Run if: always() shell: bash diff --git a/src/index.ts b/src/index.ts index 0abac3a..c1522d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -282,6 +282,12 @@ program.command('run') } const unityEditor = new UnityEditor(editorPath); + + if (!args.includes('-logFile')) { + const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName || 'Unity'); + args.push('-logFile', logPath); + } + await unityEditor.Run({ args: [...args] }); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 0c46d52..e31d03d 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -143,6 +143,27 @@ export class UnityEditor { } } + public GetLogsDirectory(projectPath: string | undefined): string { + const logsDir = projectPath !== undefined + ? path.join(projectPath, 'Builds', 'Logs') + : path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'Logs'); + + try { + fs.accessSync(logsDir, fs.constants.R_OK); + } catch (error) { + this.logger.debug(`Creating Logs Directory:\n > "${logsDir}"`); + fs.mkdirSync(logsDir, { recursive: true }); + } + + return logsDir; + } + + public GenerateLogFilePath(projectPath: string | undefined, prefix: string | undefined = undefined): string { + const logsDir = this.GetLogsDirectory(projectPath); + const timestamp = new Date().toISOString().replace(/[-:]/g, ``).replace(/\..+/, ``); + return path.join(logsDir, `${prefix ? prefix + '-' : ''}Unity-${timestamp}.log`); + } + private async exec(command: EditorCommand, onPid: (pid: ProcInfo) => void): Promise { if (!command.args || command.args.length === 0) { throw Error('No command arguments provided for Unity execution'); @@ -163,21 +184,7 @@ export class UnityEditor { } if (!command.args.includes('-logFile')) { - const logsDir = command.projectPath !== undefined - ? path.join(command.projectPath, 'Builds', 'Logs') - : path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'Logs'); - - try { - await fs.promises.access(logsDir, fs.constants.R_OK); - } catch (error) { - this.logger.debug(`Creating Logs Directory:\n > "${logsDir}"`); - await fs.promises.mkdir(logsDir, { recursive: true }); - } - - const timestamp = new Date().toISOString().replace(/[-:]/g, ``).replace(/\..+/, ``); - const generatedLogPath = path.join(logsDir, `Unity-${timestamp}.log`); - this.logger.debug(`Log File Path:\n > "${generatedLogPath}"`); - command.args.push('-logFile', generatedLogPath); + command.args.push('-logFile', this.GenerateLogFilePath(command.projectPath)); } const logPath: string = getArgumentValueAsString('-logFile', command.args); diff --git a/src/unity-project.ts b/src/unity-project.ts index 5a53276..c38f37e 100644 --- a/src/unity-project.ts +++ b/src/unity-project.ts @@ -80,6 +80,10 @@ export class UnityProject { projectPath = path.join(versionFilePath, '..', '..'); } + if (process.platform === `win32` && projectPath.endsWith(`\\`)) { + projectPath = projectPath.slice(0, -1); + } + return new UnityProject(projectPath); } } \ No newline at end of file From 99b195eb340f8ef6bd9a773163010501365189ed Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 17:50:27 -0400 Subject: [PATCH 80/90] cd --- .github/workflows/unity-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 3a896b5..9e2a1dd 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -43,7 +43,9 @@ jobs: project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') echo "$project_path" npm install -g openupm-cli - openupm add com.utilities.buildpipeline -chdir "$project_path" + cd "$project_path" + openupm add com.utilities.buildpipeline + cd ${{ github.workspace }} validate_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) echo "$validate_output" build_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) From b924991d89d0249f784036394d38e0840a48e8c8 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 18:12:23 -0400 Subject: [PATCH 81/90] tweak variables --- .github/workflows/unity-build.yml | 10 +++++----- src/index.ts | 11 +++++------ src/unity-editor.ts | 7 ++++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 9e2a1dd..a4c00f0 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -39,16 +39,16 @@ jobs: setup_output=$(unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" --json) unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') echo "$unity_editor_path" - create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "$unity_editor_path" --json) + create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "${unity_editor_path}" --json) project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') - echo "$project_path" + echo "${project_path}" npm install -g openupm-cli - cd "$project_path" + cd "${project_path}" openupm add com.utilities.buildpipeline cd ${{ github.workspace }} - validate_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) + validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) echo "$validate_output" - build_output=$(unity-cli run -e "$unity_editor_path" -p "$project_path" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) + build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) echo "$build_output" - name: Post Run if: always() diff --git a/src/index.ts b/src/index.ts index c1522d0..7c8462c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -221,7 +221,7 @@ program.command('create-project') Logger.instance.debug(JSON.stringify(options)); - const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; + const editorPath = options.unityEditorPath?.toString()?.trim() || process.env.UNITY_EDITOR; if (!editorPath || editorPath.length === 0) { throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); @@ -255,7 +255,7 @@ program.command('create-project') program.command('run') .description('Run command line args directly to the Unity Editor.') - .option('-e, --editor-path ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') + .option('-e, --unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') .option('-p, --unity-project ', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.') .option('-l, --log-name ', 'The name of the log file.') .allowUnknownOption(true) @@ -268,12 +268,13 @@ program.command('run') Logger.instance.debug(JSON.stringify({ options, args })); - const editorPath = options.editorPath?.toString()?.trim() || process.env.UNITY_EDITOR; + const editorPath = options.unityEditorPath?.toString()?.trim() || process.env.UNITY_EDITOR; if (!editorPath || editorPath.length === 0) { - throw new Error('The Unity Editor path was not specified. Use -e or --editor-path to specify it, or set the UNITY_EDITOR environment variable.'); + throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); } + const unityEditor = new UnityEditor(editorPath); const unityProjectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); const unityProject = await UnityProject.GetProject(unityProjectPath); @@ -281,8 +282,6 @@ program.command('run') throw new Error(`The specified path is not a valid Unity project: ${unityProjectPath}`); } - const unityEditor = new UnityEditor(editorPath); - if (!args.includes('-logFile')) { const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName || 'Unity'); args.push('-logFile', logPath); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index e31d03d..98a1c97 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -5,6 +5,7 @@ import { getArgumentValueAsString, killChildProcesses, ProcInfo, + readPidFile, tryKillProcess } from './utilities'; import { @@ -231,9 +232,9 @@ export class UnityEditor { fs.mkdirSync(pidDir, { recursive: true }); } else { try { - await fs.promises.access(this.pidFile, fs.constants.R_OK | fs.constants.W_OK); - if (this.procInfo) { - const killedPid = await tryKillProcess(this.procInfo); + var existingProcInfo = await readPidFile(this.pidFile); + if (existingProcInfo) { + const killedPid = await tryKillProcess(existingProcInfo); if (killedPid) { this.logger.warn(`Killed existing Unity process with pid: ${killedPid}`); } From d4fc17c25ac56ffbd3653f004072286a7cac45ac Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 18:24:24 -0400 Subject: [PATCH 82/90] rename input args --- src/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7c8462c..cc5ea4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -186,6 +186,7 @@ program.command('setup-unity') const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; + // todo support build-targets to modules mapping const unityHub = new UnityHub(); const editorPath = await unityHub.GetEditor(unityVersion, modules); const output: { [key: string]: string } = { @@ -228,10 +229,10 @@ program.command('create-project') } const unityEditor = new UnityEditor(editorPath); - const templatePath = unityEditor.GetTemplatePath(options.template); - const projectName = options.name?.toString()?.trim(); + const templatePath = unityEditor.GetTemplatePath(options.projectTemplate); + const projectName = options.projectName?.toString()?.trim(); - let projectPath = options.path?.toString()?.trim() || process.cwd(); + let projectPath = options.projectTemplate?.toString()?.trim() || process.cwd(); if (projectName && projectName.length > 0) { projectPath = path.join(projectPath, projectName); @@ -275,11 +276,11 @@ program.command('run') } const unityEditor = new UnityEditor(editorPath); - const unityProjectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); - const unityProject = await UnityProject.GetProject(unityProjectPath); + const projectPath = options.unityProjectPath?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); + const unityProject = await UnityProject.GetProject(projectPath); if (!unityProject) { - throw new Error(`The specified path is not a valid Unity project: ${unityProjectPath}`); + throw new Error(`The specified path is not a valid Unity project: ${projectPath}`); } if (!args.includes('-logFile')) { From 0f94655e137411bc101dccf639658b8cda9b68be Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 18:25:02 -0400 Subject: [PATCH 83/90] verbose --- .github/workflows/unity-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index a4c00f0..b3822f8 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -39,16 +39,16 @@ jobs: setup_output=$(unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-targets }}" --json) unity_editor_path=$(echo "$setup_output" | tail -n 1 | jq -r '.UNITY_EDITOR') echo "$unity_editor_path" - create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "${unity_editor_path}" --json) + create_project_output=$(unity-cli create-project --name "Unity Project" --unity-editor "${unity_editor_path}" --json --verbose) project_path=$(echo "$create_project_output" | tail -n 1 | jq -r '.UNITY_PROJECT_PATH') echo "${project_path}" npm install -g openupm-cli cd "${project_path}" openupm add com.utilities.buildpipeline cd ${{ github.workspace }} - validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) + validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset --verbose) echo "$validate_output" - build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) + build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity --verbose) echo "$build_output" - name: Post Run if: always() From 130ca39187732b08038be934bae41663d08ca685 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 18:40:20 -0400 Subject: [PATCH 84/90] fix input arg names --- src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index cc5ea4c..59b395b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -222,7 +222,7 @@ program.command('create-project') Logger.instance.debug(JSON.stringify(options)); - const editorPath = options.unityEditorPath?.toString()?.trim() || process.env.UNITY_EDITOR; + const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; if (!editorPath || editorPath.length === 0) { throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); @@ -230,9 +230,9 @@ program.command('create-project') const unityEditor = new UnityEditor(editorPath); const templatePath = unityEditor.GetTemplatePath(options.projectTemplate); - const projectName = options.projectName?.toString()?.trim(); + const projectName = options.name?.toString()?.trim(); - let projectPath = options.projectTemplate?.toString()?.trim() || process.cwd(); + let projectPath = options.path?.toString()?.trim() || process.cwd(); if (projectName && projectName.length > 0) { projectPath = path.join(projectPath, projectName); @@ -269,14 +269,14 @@ program.command('run') Logger.instance.debug(JSON.stringify({ options, args })); - const editorPath = options.unityEditorPath?.toString()?.trim() || process.env.UNITY_EDITOR; + const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR; if (!editorPath || editorPath.length === 0) { throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR environment variable.'); } const unityEditor = new UnityEditor(editorPath); - const projectPath = options.unityProjectPath?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); + const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); const unityProject = await UnityProject.GetProject(projectPath); if (!unityProject) { From 0ad0c5de2896542acd33313e9db4a97952b90764 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 19:00:10 -0400 Subject: [PATCH 85/90] verify template arg --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 59b395b..5504340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -229,7 +229,12 @@ program.command('create-project') } const unityEditor = new UnityEditor(editorPath); - const templatePath = unityEditor.GetTemplatePath(options.projectTemplate); + + if (!options.template || options.template.length === 0) { + throw new Error('The project template name was not specified. Use -t or --template to specify it.'); + } + + const templatePath = unityEditor.GetTemplatePath(options.template); const projectName = options.name?.toString()?.trim(); let projectPath = options.path?.toString()?.trim() || process.cwd(); From 632ac10b729c1c8eefbed049040d2a902961d756 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 20:43:27 -0400 Subject: [PATCH 86/90] fix commands --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5504340..7b71be4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -209,10 +209,10 @@ program.command('setup-unity') program.command('create-project') .description('Create a new Unity project.') - .option('-n, --name ', 'The name of the new Unity project. If unspecified, the project will be created in the specified path or the current working directory.') - .option('-p, --path ', 'The path to create the new Unity project. If unspecified, the current working directory will be used.') - .option('-t, --template ', 'The name of the template package to use for creating the unity project. Supports regex patterns.', 'com.unity.template.3d(-cross-platform)?') - .option('-e, --unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') + .option('--name ', 'The name of the new Unity project. If unspecified, the project will be created in the specified path or the current working directory.') + .option('--path ', 'The path to create the new Unity project. If unspecified, the current working directory will be used.') + .option('--template ', 'The name of the template package to use for creating the unity project. Supports regex patterns.', 'com.unity.template.3d(-cross-platform)?') + .option('--unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') .option('--verbose', 'Enable verbose logging.') .option('--json', 'Prints the last line of output as JSON string.') .action(async (options) => { @@ -261,9 +261,9 @@ program.command('create-project') program.command('run') .description('Run command line args directly to the Unity Editor.') - .option('-e, --unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') - .option('-p, --unity-project ', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.') - .option('-l, --log-name ', 'The name of the log file.') + .option('--unity-editor ', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR environment variable must be set.') + .option('--unity-project ', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.') + .option('--log-name ', 'The name of the log file.') .allowUnknownOption(true) .argument('', 'Arguments to pass to the Unity Editor executable.') .option('--verbose', 'Enable verbose logging.') From e9a6ef8d68a06f2b378e12f8264730184f710c1e Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 21:16:51 -0400 Subject: [PATCH 87/90] turn off verbose --- .github/workflows/unity-build.yml | 5 ++--- src/index.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index b3822f8..f22f4a4 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -45,10 +45,9 @@ jobs: npm install -g openupm-cli cd "${project_path}" openupm add com.utilities.buildpipeline - cd ${{ github.workspace }} - validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset --verbose) + validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) echo "$validate_output" - build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" -l Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity --verbose) + build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) echo "$build_output" - name: Post Run if: always() diff --git a/src/index.ts b/src/index.ts index 7b71be4..4c844f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -289,7 +289,7 @@ program.command('run') } if (!args.includes('-logFile')) { - const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName || 'Unity'); + const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName); args.push('-logFile', logPath); } From 33320d52369bc7951846241084540ff581cc5489 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 21:17:37 -0400 Subject: [PATCH 88/90] log editor cli exec directly to stdout --- .github/workflows/unity-build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index f22f4a4..72cecee 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -45,10 +45,8 @@ jobs: npm install -g openupm-cli cd "${project_path}" openupm add com.utilities.buildpipeline - validate_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset) - echo "$validate_output" - build_output=$(unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity) - echo "$build_output" + unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset + unity-cli run --unity-editor "${unity_editor_path}" --unity-project "${project_path}" --log-name Build -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity - name: Post Run if: always() shell: bash From 93586b286b2d6666c983bda79e5d02ec7a7e70d0 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 21:20:01 -0400 Subject: [PATCH 89/90] cleaner install cancellatino --- src/unity-hub.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 8b24d5a..ab40ab8 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -209,7 +209,9 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); try { await Exec(downloadPath, ['/S'], { silent: true }); } finally { - fs.promises.unlink(downloadPath); + if (fs.statSync(downloadPath).isFile()) { + await fs.promises.unlink(downloadPath); + } } break; @@ -255,7 +257,9 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true }); } } finally { - await fs.promises.unlink(downloadPath); + if (fs.statSync(downloadPath).isFile()) { + await fs.promises.unlink(downloadPath); + } } } break; From c2ae6cf8de687f038596c57fcddfb5d35cfee5a4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Fri, 26 Sep 2025 22:35:49 -0400 Subject: [PATCH 90/90] add support for build targets and updated readme --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 25 ++++++++++++++++++++- src/unity-hub.ts | 44 ++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ca4623a..e046630 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,63 @@ # unity-cli -A command line utility for the Unity Game Engine. This tool is primarily designed to help automate common tasks in Unity projects, such as building, testing, and managing assets in CI/CD devOps pipelines. +A powerful command line utility for the Unity Game Engine. Automate Unity project setup, editor installation, license management, building, and more—ideal for CI/CD pipelines and developer workflows. + +## Features + +- Install and manage Unity Hub and Unity Editors (multi-platform) +- Activate and return Unity licenses (personal, professional, floating) +- Create new Unity projects from templates +- Run Unity Editor commands and builds from the CLI +- Supports all modules, architectures, and build targets +- Works on Windows, macOS, and Linux +- Designed for automation and CI/CD ## Installation ```bash -npm install -g unity-cli +npm install -g @rage-against-the-pixel/unity-cli +``` + +## Usage + +```bash +unity-cli [command] [options] +``` + +### Common Commands + +- `unity-cli hub-install`: Install Unity Hub +- `unity-cli hub-version`: Print Unity Hub version +- `unity-cli hub-path`: Print Unity Hub executable path +- `unity-cli hub [args...]`: Run Unity Hub commands directly +- `unity-cli activate-license`: Activate a Unity license +- `unity-cli return-license`: Return a Unity license +- `unity-cli license-version`: Print Unity License Client version +- `unity-cli setup-unity`: Find or install Unity Editor for a project/version +- `unity-cli create-project`: Create a new Unity project from a template +- `unity-cli run [args...]`: Run commands in [Unity Editor Command Line Arguments](https://docs.unity3d.com/Manual/EditorCommandLineArguments.html) + +#### Install Unity Hub and Editor + +```bash +unity-cli hub-install +unity-cli setup-unity --unity-version 2022.3.x --modules android,ios --json +``` + +#### Activate a Unity License + +```bash +unity-cli activate-license --email --password --serial +``` + +#### Create a New Project from a Template + +```bash +unity-cli create-project --name "MyGame" --template com.unity.template.3d --unity-editor +``` + +#### Build a Project + +```bash +unity-cli run --unity-editor --unity-project -quit -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild ``` diff --git a/src/index.ts b/src/index.ts index 4c844f0..c00a107 100644 --- a/src/index.ts +++ b/src/index.ts @@ -186,7 +186,30 @@ program.command('setup-unity') const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; - // todo support build-targets to modules mapping + const buildTargets: string[] = options.buildTargets ? options.buildTargets.split(/[ ,]+/).filter(Boolean) : []; + const moduleBuildTargetMap = UnityHub.GetPlatformTargetModuleMap(); + + for (const target of buildTargets) { + const module = moduleBuildTargetMap[target]; + + if (module === undefined) { + if (target.toLowerCase() !== 'none') { + Logger.instance.warn(`${target} is not a valid build target for ${os.type()}`); + } + + continue; + } + + if (!modules.includes(module)) { + modules.push(module); + } + } + + if (modules.includes('none') || + modules.includes('None')) { + modules.length = 0; + } + const unityHub = new UnityHub(); const editorPath = await unityHub.GetEditor(unityVersion, modules); const output: { [key: string]: string } = { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index ab40ab8..f181da7 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as yaml from 'yaml'; import { spawn } from 'child_process'; @@ -940,4 +941,47 @@ done throw new Error(`Unity ${unityVersion.toString()} is not supported on ${process.platform}`); } } + + public static GetPlatformTargetModuleMap(): { [key: string]: string } { + const osType = os.type(); + let moduleMap: { [key: string]: string }; + + switch (osType) { + case 'Linux': + moduleMap = { + "StandaloneLinux64": "linux-il2cpp", + "Android": "android", + "WebGL": "webgl", + "iOS": "ios", + }; + break; + case 'Darwin': + moduleMap = { + "StandaloneOSX": "mac-il2cpp", + "iOS": "ios", + "Android": "android", + "tvOS": "appletv", + "StandaloneLinux64": "linux-il2cpp", + "WebGL": "webgl", + "VisionOS": "visionos" + }; + break; + case 'Windows_NT': + moduleMap = { + "StandaloneWindows64": "windows-il2cpp", + "WSAPlayer": "universal-windows-platform", + "Android": "android", + "iOS": "ios", + "tvOS": "appletv", + "StandaloneLinux64": "linux-il2cpp", + "Lumin": "lumin", + "WebGL": "webgl", + }; + break; + default: + throw Error(`${osType} not supported`); + } + + return moduleMap; + } } \ No newline at end of file