diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..491c983 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,160 @@ +name: Build and verify + +on: + push: + branches: + - main + - "codex/**" + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + public-checks: + name: Public source checks + runs-on: ubuntu-latest + steps: + - name: Checkout public source + uses: actions/checkout@v7 + with: + submodules: false + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + + - name: Run public source checks + run: scripts/ci/run-public-source-checks.sh + + - name: Upload authoring-tool packages + uses: actions/upload-artifact@v7 + with: + name: PalSchema-authoring-tools + if-no-files-found: error + retention-days: 7 + path: | + dist/palschema-vscode-*.vsix + dist/palschema-tools-*.tgz + + windows-msvc-shipping: + name: Windows MSVC Shipping baseline + if: >- + vars.PRIVATE_SUBMODULES_AVAILABLE == 'true' && + github.event_name == 'push' && + github.ref == 'refs/heads/main' + runs-on: windows-latest + defaults: + run: + shell: pwsh + steps: + - name: Checkout authorized recursive dependencies + uses: actions/checkout@v7 + with: + fetch-depth: 0 + submodules: recursive + token: ${{ secrets.UEPSEUDO_TOKEN }} + persist-credentials: false + + - name: Set up MSVC + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 + with: + arch: x64 + + - name: Add LLVM tools to PATH + run: | + $llvmPath = "C:\Program Files\LLVM\bin" + if (-not (Test-Path $llvmPath)) { + throw "LLVM was not found at $llvmPath" + } + $llvmPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install Rust target + run: rustup target add x86_64-pc-windows-msvc + + - name: Configure + run: cmake --preset win64-msvc-ci-shipping + + - name: Build PalSchema + run: cmake --build --preset win64-msvc-ci-shipping --target PalSchema + + - name: Verify PE contract + run: >- + python scripts/verify-win64-artifact.py + build/win64-msvc-ci-shipping/PalSchema.dll + --json-output build/win64-msvc-ci-shipping/pe-contract.json + + - name: Upload public build outputs + uses: actions/upload-artifact@v7 + with: + name: PalSchema-Win64-MSVC-Shipping + if-no-files-found: error + retention-days: 7 + path: | + build/win64-msvc-ci-shipping/PalSchema.dll + build/win64-msvc-ci-shipping/pe-contract.json + + linux-xwin: + name: Linux-hosted Win64 Dev and Shipping + if: >- + vars.PRIVATE_SUBMODULES_AVAILABLE == 'true' && + github.event_name == 'push' && + github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout authorized recursive dependencies + uses: actions/checkout@v7 + with: + fetch-depth: 0 + submodules: recursive + token: ${{ secrets.UEPSEUDO_TOKEN }} + persist-credentials: false + + - name: Install Linux cross-build tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + clang clang-tools cmake lld llvm ninja-build \ + python3 util-linux zip + + - name: Prepare isolated Rust and Microsoft SDK caches + run: >- + scripts/bootstrap-linux.sh + --install-rust-toolchain + --install-xwin + --prepare-sdk + --accept-microsoft-license + + - name: Build and verify Linux-hosted Win64 artifact + run: | + scripts/build-linux.sh dev + python3 scripts/verify-win64-artifact.py \ + build/win64-xwin-dev/PalSchema.dll \ + --json-output build/win64-xwin-dev/pe-contract.json + scripts/package-linux.sh dev --output-dir dist + scripts/build-linux.sh shipping + python3 scripts/verify-win64-artifact.py \ + build/win64-xwin-shipping/PalSchema.dll \ + --json-output build/win64-xwin-shipping/pe-contract.json + scripts/package-linux.sh shipping --output-dir dist + for archive in dist/PalSchema_*_Win64*.zip; do + unzip -l "$archive" + done + + - name: Upload Linux-hosted Win64 outputs + uses: actions/upload-artifact@v7 + with: + name: PalSchema-Win64-Linux-xwin + if-no-files-found: error + retention-days: 7 + path: | + build/win64-xwin-dev/PalSchema.dll + build/win64-xwin-dev/PalSchema.pdb + build/win64-xwin-dev/pe-contract.json + build/win64-xwin-shipping/PalSchema.dll + build/win64-xwin-shipping/pe-contract.json + dist/PalSchema_*_Win64*.zip diff --git a/.gitignore b/.gitignore index 70aab4e..443ce50 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ RC* # CMake build/ +dist/ +.xwin-cache/ CMakeLists.txt.user CMakeCache.txt CMakeFiles @@ -24,4 +26,11 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps -CMakeUserPresets.json \ No newline at end of file +CMakeUserPresets.json + +# Node authoring tools +node_modules/ + +# Python verification cache +__pycache__/ +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt index cf5542a..8fb8d89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,14 @@ cmake_minimum_required(VERSION 3.22) +project(PalSchema LANGUAGES C CXX) + set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) add_subdirectory(deps) set(TARGET PalSchema) -project(${TARGET}) file(GLOB SRC_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/dllmain.cpp" @@ -34,7 +37,37 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS ) add_library(${TARGET} SHARED ${SRC_FILES}) -target_include_directories(${TARGET} PRIVATE +if(WIN32) + target_sources(${TARGET} PRIVATE version.rc) + + # The RE-UE4SS xwin toolchain configures SDK headers for clang-cl, but + # CMake's llvm-rc preprocessing step needs the include paths separately. + if(CMAKE_CROSSCOMPILING AND DEFINED XWIN_CACHE_DIR) + set_property( + SOURCE version.rc + APPEND PROPERTY INCLUDE_DIRECTORIES + "${XWIN_CACHE_DIR}/crt/include" + "${XWIN_CACHE_DIR}/sdk/include/ucrt" + "${XWIN_CACHE_DIR}/sdk/include/shared" + "${XWIN_CACHE_DIR}/sdk/include/um" + "${XWIN_CACHE_DIR}/sdk/include/winrt" + ) + endif() +endif() + +target_include_directories(${TARGET} PRIVATE include/ ) -target_link_libraries(${TARGET} PRIVATE Zydis Zycore safetyhook nlohmann_json::nlohmann_json glaze::glaze UE4SS efsw) \ No newline at end of file +target_compile_definitions(${TARGET} PRIVATE PALSCHEMA_BUILDING_DLL=1) +target_link_libraries(${TARGET} PRIVATE Zydis Zycore safetyhook nlohmann_json::nlohmann_json glaze::glaze UE4SS efsw) + +if(MSVC) + # Keep Shipping DLLs reproducible across build times and checkout paths. + # /Brepro replaces the wall-clock PE/PDB identity with a content-derived + # value, while the alternate PDB path prevents the absolute build root + # from leaking into the CodeView record embedded in the DLL. + target_link_options(${TARGET} PRIVATE + "/Brepro" + "/PDBALTPATH:PalSchema.pdb" + ) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..fb6232c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,73 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 22, + "patch": 0 + }, + "configurePresets": [ + { + "name": "win64-xwin-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "${sourceDir}/cmake/toolchains/xwin-clang-cl.cmake", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "win64-xwin-dev", + "displayName": "Win64 Dev (Linux host, xwin + clang-cl)", + "description": "Cross-compile the PalSchema development DLL for Palworld under Proton.", + "inherits": "win64-xwin-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Game__Debug__Win64" + } + }, + { + "name": "win64-xwin-shipping", + "displayName": "Win64 Shipping (Linux host, xwin + clang-cl)", + "description": "Cross-compile the PalSchema shipping DLL for Palworld under Proton.", + "inherits": "win64-xwin-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Game__Shipping__Win64" + } + }, + { + "name": "win64-msvc-ci-shipping", + "displayName": "Win64 Shipping (Windows CI, MSVC)", + "description": "Build the Windows baseline used to compare the Linux-hosted cross-build.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Game__Shipping__Win64", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ], + "buildPresets": [ + { + "name": "win64-xwin-dev", + "configurePreset": "win64-xwin-dev" + }, + { + "name": "win64-xwin-shipping", + "configurePreset": "win64-xwin-shipping" + }, + { + "name": "win64-msvc-ci-shipping", + "configurePreset": "win64-msvc-ci-shipping" + } + ] +} diff --git a/README.md b/README.md index b463624..54eb51b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,23 @@ or with Ninja (single-configuration, faster) cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Game__Shipping__Win64 ``` +## Building the Win64 DLL from Linux + +PalSchema can be cross-compiled on Linux for the Windows Palworld client +running under Proton. The supported path uses xwin, clang-cl, LLD, Ninja, and +the repository-pinned RE-UE4SS toolchain. + +See [Building the Win64 PalSchema DLL on Linux](docs/linux/building-win64.md). +For a locked, interruption-recoverable install with a recorded rollback path, see +[Safe deployment to Palworld under Proton or Wine](docs/linux/proton-deployment.md). +For a separately managed Win64 dedicated server on a Linux host, see +[Dedicated server development on Linux](docs/linux/dedicated-server.md). +For the cross-platform JSON/JSONC validator, language server, and VS Code +extension, see [PalSchema authoring tools](docs/linux/authoring-tools.md). + +UE4SS remains a separate runtime installation and is not bundled with +PalSchema. + # Mods using PalSchema I'll only include one mod per author to avoid cluttering the list too much. diff --git a/assets/.vscode/settings.json b/assets/.vscode/settings.json index 792fdf1..fa7568a 100644 --- a/assets/.vscode/settings.json +++ b/assets/.vscode/settings.json @@ -1,24 +1,32 @@ { - "json.schemas": [ - { - "fileMatch": ["items/*.json"], - "url": "./schemas/items.schema.json" - }, - { - "fileMatch": ["pals/*.json"], - "url": "./schemas/pals.schema.json" - }, - { - "fileMatch": ["skins/*.json"], - "url": "./schemas/skins.schema.json" - }, - { - "fileMatch": ["buildings/*.json"], - "url": "./schemas/buildings.schema.json" - }, - { - "fileMatch": ["raw/*.json"], - "url": "./schemas/raw.schema.json" - }, - ] -} \ No newline at end of file + "json.schemas": [ + { + "fileMatch": [ + "**/items/**/*.json", + "**/items/**/*.jsonc" + ], + "url": "./schemas/items.schema.json" + }, + { + "fileMatch": [ + "**/pals/**/*.json", + "**/pals/**/*.jsonc" + ], + "url": "./schemas/pals.schema.json" + }, + { + "fileMatch": [ + "**/skins/**/*.json", + "**/skins/**/*.jsonc" + ], + "url": "./schemas/skins.schema.json" + }, + { + "fileMatch": [ + "**/buildings/**/*.json", + "**/buildings/**/*.jsonc" + ], + "url": "./schemas/buildings.schema.json" + } + ] +} diff --git a/assets/schemas/buildings.schema.json b/assets/schemas/buildings.schema.json index 4b1d9c2..08bfe57 100644 --- a/assets/schemas/buildings.schema.json +++ b/assets/schemas/buildings.schema.json @@ -1,5 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/buildings.schema.json", "title": "Buildings Schema", "type": "object", "additionalProperties": { @@ -359,4 +360,4 @@ } } } -} \ No newline at end of file +} diff --git a/assets/schemas/items.schema.json b/assets/schemas/items.schema.json index 7df692e..68ee64d 100644 --- a/assets/schemas/items.schema.json +++ b/assets/schemas/items.schema.json @@ -1,5 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/items.schema.json", "title": "Items Schema", "type": "object", "additionalProperties": { @@ -64,10 +65,7 @@ "Weight": { "type": "number", "description": "Must be a float value e.g. 0.0", - "minimum": 0.0, - "not": { - "type": "integer" - } + "minimum": 0.0 }, "VisualBlueprintClassSoft": { "type": "string", @@ -78,10 +76,7 @@ "type": "number", "minimum": 0.0, "default": 0.0, - "description": "Determines if the item should expire after a certain amount of time. The higher the value, the longer it takes for one item in the stack to expire.\n0.0 means it will never spoil.\nMust be a float value e.g. 0.0", - "not": { - "type": "integer" - } + "description": "Determines if the item should expire after a certain amount of time. The higher the value, the longer it takes for one item in the stack to expire.\n0.0 means it will never spoil.\nMust be a float value e.g. 0.0" }, "OverrideNameMsgID": { "type": "string", @@ -104,10 +99,7 @@ "type": "number", "minimum": 0.0, "default": 0.0, - "description": "Determines how long it should take to craft this recipe.\nMust be a float value e.g. 0.0", - "not": { - "type": "integer" - } + "description": "Determines how long it should take to craft this recipe.\nMust be a float value e.g. 0.0" }, "WorkableAttribute": { "type": "integer", @@ -174,8 +166,8 @@ "properties": { "Type": { "const": "Weapon" }, "AttackPower": { "type": "integer" }, - "SneakAttackRate": { "type": "number", "description": "Must be a float value e.g. 0.0", "default": 1.0, "not": { "type": "integer" } }, - "Durability": { "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, "default": 100.0, "not": { "type": "integer" } }, + "SneakAttackRate": { "type": "number", "description": "Must be a float value e.g. 0.0", "default": 1.0 }, + "Durability": { "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, "default": 100.0 }, "actorClass": { "type": "string", "description": "This should be path to the Weapon blueprint that defines your weapon's logic.\nExample of a correct path format: /Game/Pal/Blueprint/Weapon/BP_MyWeapon.BP_MyWeapon_C\nIn FModel or Unreal Editor, the path would look like Pal/Content/Pal/Blueprint/Weapon/BP_MyWeapon" @@ -193,7 +185,7 @@ "DefenseValue": { "type": "integer", "minimum": 0 }, "HPValue": { "type": "integer", "minimum": 0 }, "ShieldValue": { "type": "integer", "minimum": 0 }, - "Durability": { "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, "default": 100.0, "not": { "type": "integer" } }, + "Durability": { "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, "default": 100.0 }, "PassiveSkill": { "type": "string" }, "PassiveSkill2": { "type": "string" }, "PassiveSkill3": { "type": "string" }, @@ -203,4 +195,4 @@ } ] } -} \ No newline at end of file +} diff --git a/assets/schemas/pals.schema.json b/assets/schemas/pals.schema.json index f3ea896..4db5798 100644 --- a/assets/schemas/pals.schema.json +++ b/assets/schemas/pals.schema.json @@ -1,5 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/pals.schema.json", "title": "Pal Schema", "type": "object", "additionalProperties": { @@ -82,49 +83,42 @@ "type": "number", "description": "Health multiplier for wild pals, will not apply on player owned pals.\nMust be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "EnemyReceiveDamageRate": { "type": "number", "description": "Must be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "EnemyInflictDamageRate": { "type": "number", "description": "Must be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "CaptureRateCorrect": { "type": "number", "description": "Must be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "ExpRatio": { "type": "number", "description": "Must be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "Price": { "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, - "default": 0.0, - "not": { "type": "integer" } + "default": 0.0 }, "StatusResistUpRate": { "type": "number", "description": "Must be a float value e.g. 1.0", - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "AIResponse": { "type": "string", @@ -182,8 +176,7 @@ "type": "number", "description": "Must be a float value e.g. 1.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "FoodAmount": { "type": "integer" @@ -198,8 +191,7 @@ "type": "number", "description": "Must be a float value e.g. 0.0", "minimum": 0.0, - "default": 1.0, - "not": { "type": "integer" } + "default": 1.0 }, "NooseTrap": { "type": "boolean", @@ -362,8 +354,7 @@ "minimum": 0.0, "default": 0.00, "multipleOf": 0.1, - "examples": [0.00, 1.5, 2.3], - "not": { "type": "integer" } + "examples": [0.00, 1.5, 2.3] }, "Min": { "type": "integer" @@ -377,4 +368,4 @@ } } } -} \ No newline at end of file +} diff --git a/assets/schemas/schema-index.json b/assets/schemas/schema-index.json new file mode 100644 index 0000000..5f5b297 --- /dev/null +++ b/assets/schemas/schema-index.json @@ -0,0 +1,107 @@ +{ + "formatVersion": 1, + "palSchemaVersion": "0.6.1", + "baseId": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/", + "schemas": [ + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/buildings.schema.json", + "file": "buildings.schema.json", + "folder": "buildings", + "patterns": [ + "**/buildings/**/*.json", + "**/buildings/**/*.jsonc" + ], + "generated": false, + "dependencies": [ + "enums.schema.json" + ], + "supportPatterns": [], + "sha256": "26368e4f00ecf3503da4226dc1b25c014fe19d952e3161504f016c80343bfa52" + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/items.schema.json", + "file": "items.schema.json", + "folder": "items", + "patterns": [ + "**/items/**/*.json", + "**/items/**/*.jsonc" + ], + "generated": false, + "dependencies": [ + "enums.schema.json" + ], + "supportPatterns": [], + "sha256": "1d07f4983a270f43f20fe618b1cfca0dfba3be698998e19129e3fa3957ab1e74" + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/pals.schema.json", + "file": "pals.schema.json", + "folder": "pals", + "patterns": [ + "**/pals/**/*.json", + "**/pals/**/*.jsonc" + ], + "generated": false, + "dependencies": [ + "enums.schema.json" + ], + "supportPatterns": [], + "sha256": "f510386ac1a4a50242de45bb90f921ab381cbca40a0d52ef730578bd255e340e" + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/skins.schema.json", + "file": "skins.schema.json", + "folder": "skins", + "patterns": [ + "**/skins/**/*.json", + "**/skins/**/*.jsonc" + ], + "generated": false, + "dependencies": [ + "enums.schema.json" + ], + "supportPatterns": [], + "sha256": "361c6c625c914a8880a11e03015dd9180ee772f71848d104fea0fff14bcfb43b" + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/utility.schema.json", + "file": "utility.schema.json", + "folder": null, + "patterns": [], + "generated": false, + "dependencies": [], + "supportPatterns": [], + "sha256": "e754d5c2f060871e130d8b65c88f74ff6916319bdf48074e43b53c9a70254390" + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/enums.schema.json", + "file": "enums.schema.json", + "folder": "enums", + "patterns": [ + "**/enums/**/*.json", + "**/enums/**/*.jsonc" + ], + "generated": true, + "dependencies": [], + "supportPatterns": [], + "sha256": null + }, + { + "id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/raw.schema.json", + "file": "raw.schema.json", + "folder": "raw", + "patterns": [ + "**/raw/**/*.json", + "**/raw/**/*.jsonc" + ], + "generated": true, + "dependencies": [ + "enums.schema.json" + ], + "supportPatterns": [ + "raw/*.schema.json" + ], + "sha256": null + } + ] +} diff --git a/assets/schemas/skins.schema.json b/assets/schemas/skins.schema.json index 53d6445..0c1df71 100644 --- a/assets/schemas/skins.schema.json +++ b/assets/schemas/skins.schema.json @@ -1,5 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/skins.schema.json", "title": "Skin Schema", "type": "object", "additionalProperties": { @@ -35,4 +36,4 @@ }, "required": ["NormalCharacterClass", "BossCharacterClass", "SkinType", "TargetPalName", "bAutoGetItem", "IconTexture"] } -} \ No newline at end of file +} diff --git a/assets/schemas/utility.schema.json b/assets/schemas/utility.schema.json index 278b4a7..e127339 100644 --- a/assets/schemas/utility.schema.json +++ b/assets/schemas/utility.schema.json @@ -1,4 +1,6 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://okaetsu.github.io/PalSchema/schemas/0.6.1/utility.schema.json", "definitions": { "ObjectPathRegex": { "description": "Object Path must start with /Game/ and end with AssetName.AssetName\nExample: /Game/Others/InventoryItemIcon/Texture/T_itemicon_Armor_Head015.T_itemicon_Armor_Head015", @@ -9,4 +11,4 @@ "pattern": "^/Game(?:/[^/]+)*/([A-Za-z0-9_]+)\\.\\1_C$" } } -} \ No newline at end of file +} diff --git a/cmake/toolchains/xwin-clang-cl.cmake b/cmake/toolchains/xwin-clang-cl.cmake new file mode 100644 index 0000000..a26ed4a --- /dev/null +++ b/cmake/toolchains/xwin-clang-cl.cmake @@ -0,0 +1,31 @@ +# PalSchema wrapper around the pinned RE-UE4SS xwin toolchain. +# +# Keep the compiler and SDK behavior owned by RE-UE4SS. This wrapper only +# provides a distro-neutral default cache location when XWIN_DIR is unset. + +if(NOT DEFINED ENV{XWIN_DIR} OR "$ENV{XWIN_DIR}" STREQUAL "") + if(DEFINED ENV{XDG_CACHE_HOME} AND NOT "$ENV{XDG_CACHE_HOME}" STREQUAL "") + set(_palschema_xwin_dir "$ENV{XDG_CACHE_HOME}/palschema/xwin") + elseif(DEFINED ENV{HOME} AND NOT "$ENV{HOME}" STREQUAL "") + set(_palschema_xwin_dir "$ENV{HOME}/.cache/palschema/xwin") + else() + message(FATAL_ERROR "Set XWIN_DIR because neither XDG_CACHE_HOME nor HOME is available.") + endif() + + set(ENV{XWIN_DIR} "${_palschema_xwin_dir}") +endif() + +include( + "${CMAKE_CURRENT_LIST_DIR}/../../deps/RE-UE4SS/cmake/toolchains/xwin-clang-cl-toolchain.cmake" +) + +# PalSchema and its pinned RE-UE4SS revision do not use C++ modules. Disabling +# dependency scanning also keeps CMake's nested IPO probe from losing the +# clang-scan-deps path when it creates its temporary cross-build project. +set( + CMAKE_CXX_SCAN_FOR_MODULES + OFF + CACHE BOOL + "Disable C++ module dependency scanning for the PalSchema xwin build" + FORCE +) diff --git a/docs/linux/authoring-tools.md b/docs/linux/authoring-tools.md new file mode 100644 index 0000000..bf3e5f5 --- /dev/null +++ b/docs/linux/authoring-tools.md @@ -0,0 +1,173 @@ +# PalSchema authoring tools + +PalSchema includes a cross-platform authoring toolchain for Linux, Windows, +and macOS: + +- a JSON and JSONC validator with stable diagnostics and exit codes; +- a schema registry with checksums and explicit generated-schema status; +- a Language Server Protocol (LSP) server; +- an installable VS Code-compatible extension; +- portable workspace initialization; +- watch mode for editors and command-line workflows. + +The tools do not contain Palworld data, UE4SS, UEPseudo, or the +runtime-generated `enums.schema.json` and `raw.schema.json`. Those generated +schemas belong to the user's own game/runtime installation and can be copied +into a workspace when available. + +## Requirements + +- Node.js 20 or newer. +- npm 10 or newer is recommended. + +There are no distro-specific runtime dependencies. The same npm package and +VSIX work on Arch/CachyOS, Debian/Ubuntu, Fedora/RHEL-family distributions, +openSUSE, and other current Linux distributions for which Node.js and a +VS Code-compatible editor are available. + +## Build from this repository + +```bash +npm ci +npm run tools:typecheck +npm run tools:test +npm run tools:build +npm run editor:build +npm run editor:package +``` + +The resulting packages are written to `dist/`: + +- `palschema-tools-0.6.1.tgz` after running the npm pack command below; +- `palschema-vscode-0.6.1.vsix`. + +Build the npm tarball with: + +```bash +npm pack --workspace @palschema/tools --pack-destination dist +``` + +## Initialize a mod workspace + +After installing the package globally or invoking its built CLI, run: + +```bash +palschema init /path/to/MyPalSchemaMod +``` + +This creates: + +- `.palschema/schemas/` with the redistributable schema pack; +- `.vscode/settings.json` with JSON and JSONC mappings; +- `palschema.config.json` with a portable relative schema path. + +Existing `.vscode/settings.json` or `palschema.config.json` files are never +overwritten unless `--force` is explicitly supplied. Review existing editor +settings before using that option. + +The CLI searches parent directories for `palschema.config.json`, so commands +work from nested mod folders. `--schema-dir` overrides project configuration, +and `PALSCHEMA_SCHEMA_DIR` provides an environment-level override. + +## Validate and watch + +```bash +palschema validate . +palschema validate --format json . +palschema validate --watch . +palschema validate --strict-generated . +``` + +JSONC comments are supported. Trailing commas are rejected in JSON and JSONC so +authoring validation matches the PalSchema runtime parser. Exit codes are: + +- `0`: no errors; warnings may be present; +- `1`: at least one validation error; +- `2`: invalid arguments, configuration, or an internal failure. + +By default, a missing runtime-generated schema is an error. An initialized +workspace opts into warnings with `allowMissingGenerated: true`, which keeps +offline structural checks useful while clearly reporting that enum or raw +table constraints are incomplete. Use `--strict-generated` to override that +setting for release checks. + +Inspect the active schema pack with: + +```bash +palschema schemas list +palschema schemas verify +palschema doctor +palschema print-config +``` + +`schemas verify` checks every redistributable schema against the SHA-256 value +in `schema-index.json`. Missing generated schemas are reported separately and +do not make the command fail. + +## Add game-generated schemas + +PalSchema writes `enums.schema.json` and `raw.schema.json` from the current +game/runtime data. Copy those two files from your own PalSchema schema output +into: + +```text +.palschema/schemas/ +``` + +Then update their `sha256` values in a private/local copy of +`schema-index.json` if you want the local pack pinned to exact generated +content. Generated entries with a `null` checksum are accepted and reported as +`present-generated`; redistributable static schemas always require an exact +checksum match. Do not commit generated Palworld data to this repository. + +## VS Code and compatible editors + +Install the locally built extension: + +```bash +code --install-extension dist/palschema-vscode-0.6.1.vsix +``` + +VSCodium and many VS Code-derived editors accept the same VSIX through their +extension installation UI or equivalent CLI. + +The extension's language client attaches only to JSON/JSONC documents inside +known PalSchema loader folders. It provides: + +- JSON/JSONC diagnostics from the same validator as the CLI; +- property and value completion from the selected schema; +- schema hover documentation; +- document symbols; +- whole-document and range formatting. + +Settings: + +- `palschema.schemaDirectory`: an absolute path or a path relative to the first + workspace folder; empty uses the bundled redistributable schemas. +- `palschema.allowMissingGenerated`: downgrade absent runtime-generated + schemas to warnings. + +Run **PalSchema: Restart Language Server** after manually replacing a schema +pack. Configuration changes restart it automatically. + +## Generic LSP clients + +The npm package exposes `palschema-lsp`. Editors can start it with standard I/O: + +```bash +palschema-lsp --stdio +``` + +Pass these LSP initialization options when the client supports them: + +```json +{ + "schemaDirectory": "/absolute/path/to/schemas", + "allowMissingGenerated": true +} +``` + +The server implements incremental document synchronization, diagnostics, +completion, hover, symbols, and formatting. It uses only the selected local +schema directory and does not contact the game, Steam, UE4SS, or a network +service. diff --git a/docs/linux/building-win64.md b/docs/linux/building-win64.md new file mode 100644 index 0000000..986aef2 --- /dev/null +++ b/docs/linux/building-win64.md @@ -0,0 +1,188 @@ +# Building the Win64 PalSchema DLL on Linux + +Palworld's Steam client is a Windows executable, including when Steam launches +it through Proton. The PalSchema runtime loaded by that client must therefore +remain a Win64 DLL. These instructions build that DLL entirely from Linux. + +UE4SS is a separate runtime installation. PalSchema does not download, install, +or bundle UE4SS. + +## Supported build path + +The canonical toolchain is: + +- CMake 3.22 or newer; +- Ninja; +- LLVM/Clang with `clang-cl`, `lld-link`, `llvm-lib`, `llvm-rc`, and + `llvm-mt`; +- Rust and Cargo; +- [xwin](https://github.com/Jake-Shadle/xwin); +- the repository's pinned RE-UE4SS and UEPseudo submodules. + +The generated DLL is portable across Linux distributions because it targets +the Windows ABI. The build host may use Arch/CachyOS, Debian/Ubuntu, Fedora, +openSUSE, or another distribution that provides the listed tools. + +Typical host packages are: + +| Distribution family | Packages | +| --- | --- | +| Arch / CachyOS | `clang cmake curl git lld llvm ninja` | +| Debian | `clang clang-tools cmake curl git lld llvm ninja-build util-linux` | +| Ubuntu | `clang clang-tools cmake curl git lld llvm ninja-build util-linux` | +| Fedora / RHEL family | `clang clang-tools-extra cmake curl git lld llvm ninja-build util-linux` | +| openSUSE | `clang cmake curl git lld llvm ninja util-linux` | + +Package names can change between distribution releases. Run +`scripts/bootstrap-linux.sh` after installation; it checks the exact commands +the build uses, including `clang-cl`, `lld-link`, `llvm-lib`, `llvm-mt`, +`llvm-rc`, `llvm-ranlib`, `flock`, and Ninja. The check accepts the +distribution's version-suffixed LLVM tools, such as Ubuntu's `clang-cl-18`, +without creating system symlinks. Rust can remain project-isolated as +described below. + +The repository contains a clean-container matrix for Debian stable, Ubuntu +24.04, Fedora latest, Arch Linux, and openSUSE Tumbleweed: + +```bash +scripts/test-distro-matrix.sh +``` + +That default gate installs Node.js from its verified upstream SHA-256, +then runs the same shell, Python, JSON, schema, CLI/LSP, VS Code extension, +test, audit, and package checks as public CI. To also install each +distribution's build toolchain, cross-build the Win64 Shipping DLL, and verify +its PE contract using an already prepared local cache: + +```bash +scripts/test-distro-matrix.sh --build-shipping +``` + +The build mode mounts the existing PalSchema cache into each disposable +container. It never downloads the Microsoft SDK or accepts its license. +Prepare the SDK once through the explicit bootstrap gate below. + +## Epic and UEPseudo access + +The recursive RE-UE4SS dependency includes the private UEPseudo repository. +Your GitHub account must be linked to Epic Games and must have accepted the +EpicGames organization invitation. + +Initialize the exact commits pinned by PalSchema: + +```bash +git submodule update --init --recursive +``` + +Do not use `--remote`; it would move dependencies away from the reviewed +commits. + +## Bootstrap + +Run the read-only prerequisite check: + +```bash +scripts/bootstrap-linux.sh +``` + +If xwin is missing, it can be installed explicitly: + +```bash +scripts/bootstrap-linux.sh --install-xwin +``` + +RE-UE4SS also builds a Rust dependency for Windows. Some distributions ship +Rust without cross-target standard libraries. Install a project-isolated +toolchain and the required target without replacing the system Rust package: + +```bash +scripts/bootstrap-linux.sh --install-rust-toolchain +``` + +The installer is downloaded from `static.rust-lang.org` and verified against +its official SHA-256 before execution. It uses +`$XDG_CACHE_HOME/palschema/{rustup,cargo}` or +`$HOME/.cache/palschema/{rustup,cargo}` and does not modify shell profiles. + +xwin downloads Microsoft CRT and Windows SDK files. The project never accepts +that license silently. Review the applicable Microsoft terms and then prepare +the SDK with the explicit gate: + +```bash +scripts/bootstrap-linux.sh \ + --prepare-sdk \ + --accept-microsoft-license +``` + +The default SDK cache is: + +- `$XWIN_DIR` when set; +- otherwise `$XDG_CACHE_HOME/palschema/xwin` when set; +- otherwise `$HOME/.cache/palschema/xwin`. + +## Build + +Development build: + +```bash +scripts/build-linux.sh dev +``` + +Shipping build: + +```bash +scripts/build-linux.sh shipping +``` + +Verify the stable PE contract and emit machine-readable metadata: + +```bash +python scripts/verify-win64-artifact.py \ + build/win64-xwin-shipping/PalSchema.dll \ + --json-output build/win64-xwin-shipping/pe-contract.json +``` + +The scripts use `CMakePresets.json`. To configure once through the same +environment setup and then invoke CMake directly: + +```bash +scripts/build-linux.sh dev --configure-only +cmake --build --preset win64-xwin-dev --target PalSchema +``` + +Building the explicit target avoids compiling RE-UE4SS's unrelated example +programs and proxy DLL. The configure helper also selects the project-isolated +Rust toolchain when it is installed, so a distro's system Rust remains +untouched. Newer Cargo releases may normalize RE-UE4SS's pinned lock file for +the enabled feature subset. The helper routes Cargo through a serialized guard +that restores the exact pre-build lock file after every invocation, including +failed or interrupted builds. Build products remain under `build//`; +the development DLL is written to `build/win64-xwin-dev/PalSchema.dll` and the +shipping DLL to `build/win64-xwin-shipping/PalSchema.dll`. + +Shipping links use the MSVC-compatible reproducible-build flag and a +checkout-independent CodeView PDB name. Rebuilding identical inputs therefore +does not embed the build time or an absolute checkout path in the DLL. + +## Windows CI baseline + +Public pull requests run source-format checks without cloning UEPseudo. The +Windows/MSVC baseline is deliberately opt-in because recursive checkout +requires authorized access to Epic-licensed UEPseudo: + +- repository variable `PRIVATE_SUBMODULES_AVAILABLE=true`; +- repository secret `UEPSEUDO_TOKEN` containing a read-capable GitHub token. + +The private job never uploads source trees. Its artifact contains only +`PalSchema.dll` and the JSON PE contract. Pull requests from forks cannot run +the secret-bearing job. + +## What this does not provide + +This target produces a Win64 DLL for the Windows Palworld client under Proton +and the Win64 Dedicated Server under Wine or Proton. It is not a native ELF +plugin for `PalServer-Linux-Shipping`. Native server support requires a stable +native UE4SS C++ mod ABI plus Linux-specific signatures, layouts, hooks, and +integration testing. See +[Dedicated server development on Linux](dedicated-server.md) for the currently +verified server path and native-runtime status. diff --git a/docs/linux/dedicated-server.md b/docs/linux/dedicated-server.md new file mode 100644 index 0000000..44e638c --- /dev/null +++ b/docs/linux/dedicated-server.md @@ -0,0 +1,186 @@ +# Dedicated server development on Linux + +PalSchema's supported dedicated-server path on a Linux host is the **Win64 +Palworld Dedicated Server running through Wine or Proton**. It uses the same +PalSchema DLL and UE4SS C++ mod ABI as the Windows client. + +The native `PalServer-Linux-Shipping` binary is not currently a verified 1:1 +PalSchema target. PalSchema is presently built as a Win64 UE4SS C++ mod, while +the upstream native UE4SS port remains under development. The upstream +headless Linux draft explicitly leaves C++ mods outside its initial scope: +[UE4SS pull request 1347](https://github.com/UE4SS-RE/RE-UE4SS/pull/1347). +Downstream Linux experiments may accept a native `CppUserModBase`/`main.so` +mod at the loader boundary; that is narrower than proving PalSchema's +signatures, layouts, hooks, threading, teardown, and generated schemas against +the native Palworld server. No such downstream and PalSchema combination is +pinned or verified by this repository yet, so it must not be treated as a +drop-in replacement for this workflow. + +## Ownership boundary + +This repository builds and deploys only PalSchema. + +You must install and maintain UE4SS yourself. PalSchema does not download, +redistribute, package, or modify UE4SS. The same boundary applies to Palworld +and its Dedicated Server files. + +## Requirements + +- a recent 64-bit Wine or Proton installation; +- SteamCMD; +- a separately acquired Win64 Palworld Dedicated Server; +- a compatible UE4SS installation placed in that server; +- a completed PalSchema Shipping build. + +The workflow uses portable shell, CMake, Ninja, clang-cl, Node.js, and Wine +interfaces. It is not tied to CachyOS or Arch. Package names differ, but the +same repository commands apply on current Arch/CachyOS, Debian/Ubuntu, +Fedora/RHEL-family, openSUSE, and other major distributions. + +## Acquire an isolated Win64 server + +SteamCMD can download the Windows depot even when SteamCMD itself runs on +Linux. Choose a dedicated directory that does not overlap an existing native +server: + +```bash +steamcmd \ + +@sSteamCmdForcePlatformType windows \ + +force_install_dir /srv/palworld-win64 \ + +login anonymous \ + +app_update 2394010 validate \ + +quit +``` + +Valve documents `@sSteamCmdForcePlatformType` in the +[SteamCMD reference](https://developer.valvesoftware.com/wiki/SteamCMD). +Back up any existing server configuration and save data before updating. + +Install UE4SS into the resulting Win64 server according to the UE4SS and +Palworld-specific instructions you have chosen. Before continuing, the layout +must include: + +```text +/srv/palworld-win64/ +└── Pal/Binaries/Win64/ + ├── PalServer-Win64-Shipping-Cmd.exe + ├── dwmapi.dll + └── ue4ss/ + ├── UE4SS.dll + ├── UE4SS-settings.ini + ├── MemberVariableLayout.ini + └── Mods/ +``` + +These UE4SS files are examples of the required external installation. They +are never copied from or included in a PalSchema package. + +## Build and deploy PalSchema + +```bash +scripts/build-linux.sh shipping +scripts/deploy-proton.sh shipping \ + --target server \ + --game-dir /srv/palworld-win64 \ + --dry-run +``` + +Review the paths, remove `--dry-run`, and keep the printed rollback command. +Deployment uses a locked, journaled replacement transaction, creates a +timestamped backup, and refuses to proceed while the selected Win64 server is +active or process visibility is incomplete. A signal restores the previous +installation; after host or power interruption, the next deploy/rollback +recovers the recorded transaction before making another change. + +## Start under Wine + +Use a dedicated Wine prefix so server dependencies and settings remain +isolated: + +```bash +cd /srv/palworld-win64/Pal/Binaries/Win64 + +WINEPREFIX=/srv/palworld-win64/.compat/pfx \ +WINEDLLOVERRIDES='dwmapi=n,b' \ +WINEDEBUG=-all \ +SteamAppId=2394010 \ +wine ./PalServer-Win64-Shipping-Cmd.exe Pal \ + -port=8211 \ + -queryport=27015 \ + -players=32 \ + -useperfthreads \ + -NoAsyncLoadingThread \ + -UseMultithreadForDS +``` + +Adjust ports and normal Palworld server arguments for your environment. Do not +reuse ports already owned by another server. + +PalSchema detects Wine at runtime and selects UE4SS's synchronous signature +scanner path during startup. Native Windows keeps the normal parallel scanner. +This avoids a Wine `std::async` startup deadlock without changing signatures, +loader behavior, or the produced Win64 DLL ABI. + +When PalSchema auto-reload is enabled, Wine uses efsw's polling backend because +Wine does not reliably deliver the Win32 directory-change notifications used +by efsw's native Windows backend. Native Windows keeps the event-driven +backend. The selected fallback is reported in `UE4SS.log`. + +## Verification + +Check `Pal/Binaries/Win64/ue4ss/UE4SS.log` for: + +- `PalSchema v... loaded`; +- every expected `Found ...` signature line; +- `Event loop start`; +- initialization of `enums`, `raw`, `blueprints`, `resources`, `pals`, `npcs`, + `items`, `skins`, `appearance`, `buildings`, `helpguide`, `spawns`, and + `translations`. + +Also verify that the configured game and query ports are open. A process that +exists but never opens its ports is not a successful smoke test. + +The repeatable smoke harness performs those checks, stops only the selected +Wine process group and prefix between cycles, and writes one evidence directory +per run: + +```bash +scripts/test-win64-server.sh \ + --game-dir /srv/palworld-win64 \ + --cycles 3 +``` + +Auto-reload can be included by pointing at a harmless fixture that already +exists inside one PalSchema mod. The harness only updates that file's +modification time: + +```bash +scripts/test-win64-server.sh \ + --game-dir /srv/palworld-win64 \ + --cycles 3 \ + --hot-reload-file \ + /srv/palworld-win64/Pal/Binaries/Win64/ue4ss/Mods/PalSchema/mods/Smoke/translations/en/smoke.json +``` + +Automatic discovery is intentionally disabled: the server root must be +explicit so the harness cannot select another Palworld installation. + +The Linux-hosted workflow was exercised on 2026-07-25 against Win64 Dedicated +Server Steam build `24181105` under Wine 11.13. Five consecutive startup and +shutdown cycles found all 22 PalSchema signatures, initialized all 13 loaders, +reached UE4SS's event loop, opened both isolated UDP ports, and released both +ports after shutdown. Three additional cycles triggered and observed +PalSchema auto-reload through the Wine polling fallback. The same DLL was then +verified against the installed Palworld client under Proton. This is a +reproducibility record, not a promise that future Palworld or UE4SS updates +cannot change signatures or compatibility. + +## Updating and rollback + +Stop the Win64 server before every update or rollback. Rebuild, deploy, and +repeat the verification above after Palworld, UE4SS, Wine/Proton, or PalSchema +changes. + +Use the exact rollback command printed by `deploy-proton.sh`. It restores only +the previous `Mods/PalSchema` state and leaves UE4SS, other mods, the Wine +prefix, configuration, and saves untouched. diff --git a/docs/linux/proton-deployment.md b/docs/linux/proton-deployment.md new file mode 100644 index 0000000..2afecc2 --- /dev/null +++ b/docs/linux/proton-deployment.md @@ -0,0 +1,70 @@ +# Safe deployment to Palworld under Proton or Wine + +PalSchema is a UE4SS C++ mod. Install UE4SS separately before using these +commands; this repository neither downloads nor bundles it. + +Build and inspect the Shipping DLL first: + +```bash +scripts/build-linux.sh shipping +scripts/deploy-proton.sh shipping --target client --dry-run +``` + +The deploy helper discovers the default Steam library at +`$HOME/.local/share/Steam/steamapps/common/Palworld`. For another Steam library, +pass its Palworld directory explicitly: + +```bash +scripts/deploy-proton.sh shipping \ + --target client \ + --game-dir /mnt/games/SteamLibrary/steamapps/common/Palworld \ + --dry-run +``` + +Remove `--dry-run` to deploy. The helper: + +- verifies the Palworld Win64 executable and a separate `UE4SS.dll`; +- refuses to run while the Windows client is active; +- stages `Mods/PalSchema` before switching it into place; +- preserves the previous installation under + `Pal/Binaries/Win64/ue4ss/.palschema-backups/`; +- prints the exact rollback command. + +It does not modify UE4SS, other mods, the Proton prefix, Palworld saves, or the +native Dedicated Server. + +For a Windows dedicated-server installation managed on a Linux host, select +the server target explicitly: + +```bash +scripts/deploy-proton.sh shipping \ + --target server \ + --game-dir /srv/palworld-win64 +``` + +Auto-detection prefers the client when both executable layouts exist. Use an +explicit target in automation. The server target recognizes both +`PalServer-Win64-Shipping.exe` and +`PalServer-Win64-Shipping-Cmd.exe`, refuses to deploy while either is active, +and records the target in its rollback metadata. + +See [Dedicated server development on Linux](dedicated-server.md) for acquiring +and starting the Win64 server. The native Linux PalServer executable is a +different runtime and cannot load PalSchema's Win64 UE4SS C++ mod. + +Use the Dev flavor when schemas, examples, VS Code settings, and PDB symbols +are needed: + +```bash +scripts/deploy-proton.sh dev +``` + +Standalone release-compatible archives can be created with: + +```bash +scripts/package-linux.sh shipping +scripts/package-linux.sh dev +``` + +They are written under `dist/` and preserve the upstream +`PalSchema/dlls/main.dll` layout. diff --git a/include/Platform/Export.h b/include/Platform/Export.h new file mode 100644 index 0000000..a9688ba --- /dev/null +++ b/include/Platform/Export.h @@ -0,0 +1,13 @@ +#pragma once + +#if defined(_WIN32) + #if defined(PALSCHEMA_BUILDING_DLL) + #define PALSCHEMA_API __declspec(dllexport) + #else + #define PALSCHEMA_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) || defined(__clang__) + #define PALSCHEMA_API __attribute__((visibility("default"))) +#else + #define PALSCHEMA_API +#endif diff --git a/include/Platform/RuntimeEnvironment.h b/include/Platform/RuntimeEnvironment.h new file mode 100644 index 0000000..61ace7e --- /dev/null +++ b/include/Platform/RuntimeEnvironment.h @@ -0,0 +1,21 @@ +#pragma once + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace PS::Platform +{ + inline bool IsRunningUnderWine() + { +#if defined(_WIN32) + auto ntdll = GetModuleHandleW(L"ntdll.dll"); + return ntdll && GetProcAddress(ntdll, "wine_get_version"); +#else + return false; +#endif + } +} diff --git a/include/SDK/Classes/TSoftObjectPtr.h b/include/SDK/Classes/TSoftObjectPtr.h index 00ba1ce..9f96968 100644 --- a/include/SDK/Classes/TSoftObjectPtr.h +++ b/include/SDK/Classes/TSoftObjectPtr.h @@ -3,6 +3,8 @@ #include "SDK/Classes/TPersistentObjectPtr.h" #include "SDK/Structs/FSoftObjectPtr.h" +#include + namespace UECustom { template class TSoftObjectPtr @@ -15,10 +17,8 @@ namespace UECustom { { } - template < - class U - UE_REQUIRES(std::is_convertible_v) - > + template + requires std::is_convertible_v FORCEINLINE TSoftObjectPtr(const TSoftObjectPtr& Other) : SoftObjectPtr(Other.SoftObjectPtr) { @@ -31,4 +31,4 @@ namespace UECustom { private: FSoftObjectPtr SoftObjectPtr; }; -} \ No newline at end of file +} diff --git a/include/SDK/Helper/PropertyHelper.h b/include/SDK/Helper/PropertyHelper.h index f0a404a..9de0146 100644 --- a/include/SDK/Helper/PropertyHelper.h +++ b/include/SDK/Helper/PropertyHelper.h @@ -28,6 +28,9 @@ namespace Palworld { } namespace Palworld::PropertyHelper { + template + FFieldDerivedType* CastProperty(RC::Unreal::FField* Field); + void CopyJsonValueToContainer(void* Container, RC::Unreal::FProperty* Property, const nlohmann::json& Value); RC::Unreal::int64 ParseEnumFromJsonValue(RC::Unreal::FEnumProperty* Property, const nlohmann::json& Value); @@ -172,4 +175,4 @@ namespace Palworld::PropertyHelper { { return Field != nullptr && IsPropertyA(Field) ? static_cast(Field) : nullptr; } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d71c1b2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5027 @@ +{ + "name": "palschema-workspace", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "palschema-workspace", + "workspaces": [ + "tools/*" + ] + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.1.tgz", + "integrity": "sha512-zBhRGzABKSI7hfWh5EaZmril5ybZ7imBN1qEZl5sDTaelr+l8SnPjZO50Q4dnKnm347YPIlBMSnXKZyh3Yu5DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.11.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.2.tgz", + "integrity": "sha512-yDhtBOGDCdK9ipQ9g3+wmlMEPnZx2pXaDicDd9jYyR1L+7lEbvEohTDmF5qejZDutZY3m9pWPxeYxzNC701A2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.2.tgz", + "integrity": "sha512-fnqOpGYAV+i0RH4W5HB6Oy1IhqGZoCdnp7Y2Sa9k18FlT8aBkCA7L8Hv19hUHLDUK6kVjUO29AfnGX6wgAHyNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@palschema/tools": { + "resolved": "tools/palschema-tools", + "link": true + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/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/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/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/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.7.tgz", + "integrity": "sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "license": "MIT" + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "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/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/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/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/palschema-vscode": { + "resolved": "tools/vscode-palschema", + "link": true + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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/string-width/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/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.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "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/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/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/table/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/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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/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" + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.7.2.tgz", + "integrity": "sha512-WtKRDtJfFEmLrgtu+ODexOHm/6/krRF0k6t+uvkKIKW1Jh9ZIyxZQwJJwB3qhrEgvAxa37zbUg+vn+UyUK/U2w==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "jsonc-parser": "^3.3.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.1.0.tgz", + "integrity": "sha512-XXRx6lqVitQy/oOLr9MfNYRG+MbQkhXkDaxbQMiKxEm8zZNfheRFUKNb8UYNh2stn9btl2wQM5wZFJjJvoc+jA==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.2.5", + "semver": "^7.8.1", + "vscode-languageserver-protocol": "3.18.2", + "vscode-languageserver-textdocument": "1.0.13" + }, + "engines": { + "vscode": "^1.91.0" + } + }, + "node_modules/vscode-languageclient/node_modules/vscode-languageserver-textdocument": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.13.tgz", + "integrity": "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw==", + "license": "MIT" + }, + "node_modules/vscode-languageserver": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.1.0.tgz", + "integrity": "sha512-9gEWpXkYGXoqG7pBnE8O8hx/yP7+Aabn4+peQ3KDicQv6qunHSWyLTud3OF0w4S2+HfDD+5HqYKiXQW9HAU6mA==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.18.2" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.1", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "tools/palschema-tools": { + "name": "@palschema/tools", + "version": "0.6.1", + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "chokidar": "4.0.3", + "jsonc-parser": "3.3.1", + "vscode-json-languageservice": "5.7.2", + "vscode-languageserver": "10.1.0", + "vscode-languageserver-textdocument": "1.0.12" + }, + "bin": { + "palschema": "dist/cli.js", + "palschema-lsp": "dist/lsp-server.js" + }, + "devDependencies": { + "@types/node": "24.1.0", + "tsx": "4.20.3", + "typescript": "5.8.3" + }, + "engines": { + "node": ">=20" + } + }, + "tools/vscode-palschema": { + "name": "palschema-vscode", + "version": "0.6.1", + "license": "MIT", + "dependencies": { + "vscode-languageclient": "10.1.0" + }, + "devDependencies": { + "@types/vscode": "1.91.0", + "@vscode/vsce": "3.9.2", + "esbuild": "0.28.1", + "tsx": "4.20.3", + "typescript": "5.8.3" + }, + "engines": { + "vscode": "^1.91.0" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tools/vscode-palschema/node_modules/@types/vscode": { + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz", + "integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==", + "dev": true, + "license": "MIT" + }, + "tools/vscode-palschema/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..94ab884 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "palschema-workspace", + "private": true, + "workspaces": [ + "tools/*" + ], + "scripts": { + "tools:build": "npm run build --workspace @palschema/tools", + "tools:test": "npm run test --workspace @palschema/tools", + "tools:typecheck": "npm run typecheck --workspace @palschema/tools", + "editor:build": "npm run build --workspace palschema-vscode", + "editor:test": "npm run test --workspace palschema-vscode", + "editor:package": "npm run package:vsix --workspace palschema-vscode" + } +} diff --git a/scripts/bootstrap-linux.sh b/scripts/bootstrap-linux.sh new file mode 100755 index 0000000..8bbcfb9 --- /dev/null +++ b/scripts/bootstrap-linux.sh @@ -0,0 +1,373 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage: scripts/bootstrap-linux.sh [options]" \ + "" \ + "Checks the Linux-to-Win64 build prerequisites without changing the system." \ + "" \ + "Options:" \ + " --install-xwin Install xwin with cargo when it is missing." \ + " --install-rust-toolchain Install an isolated Rust toolchain in the PalSchema cache." \ + " --prepare-sdk Download and unpack the Microsoft CRT/SDK." \ + " --accept-microsoft-license Explicitly accept the xwin Microsoft license gate." \ + " -h, --help Show this help." +} + +install_xwin=false +install_rust_toolchain=false +prepare_sdk=false +accept_microsoft_license=false +readonly PALSCHEMA_XWIN_VERSION="0.9.0" + +while (($# > 0)); do + case "$1" in + --install-xwin) + install_xwin=true + ;; + --install-rust-toolchain) + install_rust_toolchain=true + ;; + --prepare-sdk) + prepare_sdk=true + ;; + --accept-microsoft-license) + accept_microsoft_license=true + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + esac + shift +done + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "$script_dir/lib/build-env.sh" +palschema_configure_build_environment + +if [[ "$(uname -s)" != "Linux" ]]; then + printf '%s\n' "This bootstrap targets Linux hosts." >&2 + exit 1 +fi + +required_commands=( + cmake + curl + flock + git + ninja + python3 + realpath + sha256sum + sync +) +missing_commands=() + +for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null 2>&1; then + missing_commands+=("$required_command") + fi +done + +for llvm_tool in clang-cl lld-link llvm-mt llvm-rc llvm-ranlib; do + if ! palschema_llvm_tool_available "$llvm_tool"; then + missing_commands+=("$llvm_tool") + fi +done +if ! palschema_llvm_tool_available llvm-lib && + ! palschema_llvm_tool_available llvm-ar; then + missing_commands+=("llvm-lib/llvm-ar") +fi + +if ((${#missing_commands[@]} > 0)); then + printf 'Missing required commands: %s\n' "${missing_commands[*]}" >&2 + printf '%s\n' \ + "Install the equivalent packages for your distribution, then rerun this script." >&2 + exit 1 +fi + +install_isolated_rust_toolchain() { + case "$(uname -m)" in + x86_64) + rustup_host="x86_64-unknown-linux-gnu" + ;; + aarch64|arm64) + rustup_host="aarch64-unknown-linux-gnu" + ;; + *) + printf 'Unsupported rustup host architecture: %s\n' "$(uname -m)" >&2 + exit 1 + ;; + esac + + rustup_base_url="https://static.rust-lang.org/rustup/dist/$rustup_host/rustup-init" + rustup_download_dir="$(mktemp -d "${TMPDIR:-/tmp}/palschema-rustup.XXXXXX")" + trap 'rm -rf -- "$rustup_download_dir"' EXIT + + curl --fail --location --silent --show-error \ + --output "$rustup_download_dir/rustup-init" \ + "$rustup_base_url" + curl --fail --location --silent --show-error \ + --output "$rustup_download_dir/rustup-init.sha256" \ + "$rustup_base_url.sha256" + + expected_sha256="$(awk '{print $1}' "$rustup_download_dir/rustup-init.sha256")" + actual_sha256="$(sha256sum "$rustup_download_dir/rustup-init" | awk '{print $1}')" + + if [[ "$actual_sha256" != "$expected_sha256" ]]; then + printf '%s\n' "rustup-init SHA-256 verification failed." >&2 + exit 1 + fi + + chmod +x "$rustup_download_dir/rustup-init" + mkdir -p "$PALSCHEMA_RUSTUP_HOME" "$PALSCHEMA_CARGO_HOME" + + RUSTUP_HOME="$PALSCHEMA_RUSTUP_HOME" \ + CARGO_HOME="$PALSCHEMA_CARGO_HOME" \ + RUSTUP_INIT_SKIP_PATH_CHECK=yes \ + "$rustup_download_dir/rustup-init" \ + --default-toolchain stable \ + --no-modify-path \ + --profile minimal \ + -y + + export RUSTUP_HOME="$PALSCHEMA_RUSTUP_HOME" + export CARGO_HOME="$PALSCHEMA_CARGO_HOME" + export PATH="$CARGO_HOME/bin:$PATH" +} + +if [[ "$install_rust_toolchain" == true ]]; then + if [[ ! -x "$PALSCHEMA_CARGO_HOME/bin/rustup" ]]; then + install_isolated_rust_toolchain + fi + export RUSTUP_HOME="$PALSCHEMA_RUSTUP_HOME" + export CARGO_HOME="$PALSCHEMA_CARGO_HOME" + export PATH="$CARGO_HOME/bin:$PATH" +fi + +if ! command -v cargo >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; then + printf '%s\n' \ + "Rust and Cargo are required. Install rustup, or use the isolated setup:" \ + " scripts/bootstrap-linux.sh --install-rust-toolchain" >&2 + exit 1 +fi + +rust_target="x86_64-pc-windows-msvc" +rust_target_libdir="$(rustc --print target-libdir --target "$rust_target")" + +if [[ ! -d "$rust_target_libdir" ]]; then + if [[ "$install_rust_toolchain" == true ]]; then + rustup target add "$rust_target" + else + printf '%s\n' \ + "Rust target $rust_target is missing." \ + "Keep the system Rust installation untouched and add an isolated toolchain with:" \ + " scripts/bootstrap-linux.sh --install-rust-toolchain" >&2 + exit 1 + fi +fi + +if [[ -n "${XWIN_DIR:-}" ]]; then + palschema_xwin_dir="$XWIN_DIR" +else + palschema_xwin_dir="$PALSCHEMA_CACHE_ROOT/xwin" +fi +palschema_xwin_dir="$(realpath -m -- "$palschema_xwin_dir")" +palschema_cache_root_real="$(realpath -m -- "$PALSCHEMA_CACHE_ROOT")" +project_root="$(cd -- "$script_dir/.." && pwd)" +user_home_real="$(realpath -m -- "${HOME:?HOME must be set}")" + +path_is_within() { + local candidate="$1" + local root="$2" + [[ "$candidate" == "$root" || "$candidate" == "$root/"* ]] +} + +if [[ "$palschema_xwin_dir" == "/" ]] || + path_is_within "$user_home_real" "$palschema_xwin_dir" || + path_is_within "$project_root" "$palschema_xwin_dir" || + path_is_within "$palschema_xwin_dir" "$project_root"; then + printf 'Unsafe XWIN_DIR overlaps a protected path: %s\n' \ + "$palschema_xwin_dir" >&2 + exit 1 +fi + +xwin_cache_payload_is_valid() { + local cache_dir="$1" + [[ -f "$cache_dir/crt/include/vcruntime.h" && + -f "$cache_dir/crt/lib/x86_64/msvcrt.lib" && + -f "$cache_dir/sdk/include/um/Windows.h" && + -f "$cache_dir/sdk/lib/ucrt/x86_64/ucrt.lib" && + -f "$cache_dir/sdk/lib/um/x86_64/kernel32.Lib" ]] +} + +xwin_cache_is_ready() { + local cache_dir="$1" + xwin_cache_payload_is_valid "$cache_dir" && + [[ -f "$cache_dir/.palschema-sdk-complete" ]] +} + +fsync_directory() { + python3 - "$1" <<'PY' +import os +import sys + +descriptor = os.open(sys.argv[1], os.O_RDONLY | os.O_DIRECTORY) +try: + os.fsync(descriptor) +finally: + os.close(descriptor) +PY +} + +xwin_parent="$(dirname -- "$palschema_xwin_dir")" +xwin_name="$(basename -- "$palschema_xwin_dir")" +xwin_previous="$xwin_parent/.${xwin_name}.previous" + +if [[ -e "$palschema_xwin_dir" ]] && + ! xwin_cache_is_ready "$palschema_xwin_dir" && + ! path_is_within "$palschema_xwin_dir" "$palschema_cache_root_real"; then + printf 'Refusing to replace an unowned XWIN_DIR outside %s: %s\n' \ + "$palschema_cache_root_real" "$palschema_xwin_dir" >&2 + exit 1 +fi +if [[ -e "$xwin_previous" ]] && + ! xwin_cache_is_ready "$xwin_previous"; then + printf 'Refusing to replace an unowned SDK recovery directory: %s\n' \ + "$xwin_previous" >&2 + exit 1 +fi + +mkdir -p "$xwin_parent" +exec {xwin_lock_fd}>> "$xwin_parent/.${xwin_name}.lock" +flock "$xwin_lock_fd" + +# Recover the last complete cache if a previous refresh was interrupted between +# the two directory renames. +if ! xwin_cache_is_ready "$palschema_xwin_dir" && + xwin_cache_is_ready "$xwin_previous"; then + if [[ -e "$palschema_xwin_dir" ]]; then + xwin_incomplete="$xwin_parent/.${xwin_name}.incomplete.$$" + mv -- "$palschema_xwin_dir" "$xwin_incomplete" + else + xwin_incomplete="" + fi + mv -- "$xwin_previous" "$palschema_xwin_dir" + fsync_directory "$xwin_parent" + if [[ -n "$xwin_incomplete" ]]; then + rm -rf -- "$xwin_incomplete" + fi +fi + +xwin_is_pinned() { + command -v xwin >/dev/null 2>&1 && + [[ "$(xwin --version 2>/dev/null)" == "xwin $PALSCHEMA_XWIN_VERSION" ]] +} + +# xwin is needed to create the SDK cache, but not to consume a complete cache +# during an offline or clean-container build. +if ! xwin_is_pinned && + { [[ "$install_xwin" == true ]] || + { [[ "$prepare_sdk" == true ]] && + ! xwin_cache_is_ready "$palschema_xwin_dir"; }; }; then + if [[ "$install_xwin" == true ]]; then + cargo install --locked --version "$PALSCHEMA_XWIN_VERSION" xwin + if ! xwin_is_pinned; then + printf 'Installed xwin does not report pinned version %s.\n' \ + "$PALSCHEMA_XWIN_VERSION" >&2 + exit 1 + fi + else + printf '%s\n' \ + "The pinned xwin version is required to prepare the Microsoft CRT/SDK cache." \ + "Rerun with --install-xwin or install it with:" \ + " cargo install --locked --version $PALSCHEMA_XWIN_VERSION xwin" >&2 + exit 1 + fi +fi + +if [[ "$accept_microsoft_license" == true && "$prepare_sdk" != true ]]; then + printf '%s\n' \ + "--accept-microsoft-license is only meaningful together with --prepare-sdk." >&2 + exit 2 +fi + +if [[ "$prepare_sdk" == true ]]; then + if [[ "$accept_microsoft_license" != true ]]; then + printf '%s\n' \ + "SDK preparation requires explicit license acceptance." \ + "Review Microsoft's terms, then rerun with both:" \ + " --prepare-sdk --accept-microsoft-license" >&2 + exit 2 + fi + + if xwin_cache_is_ready "$palschema_xwin_dir"; then + printf 'Reusing complete xwin SDK cache at %s.\n' "$palschema_xwin_dir" + else + xwin_stage="$(mktemp -d "$xwin_parent/.${xwin_name}.stage.XXXXXX")" + if ! xwin --accept-license splat --output "$xwin_stage"; then + rm -rf -- "$xwin_stage" + exit 1 + fi + if ! xwin_cache_payload_is_valid "$xwin_stage"; then + rm -rf -- "$xwin_stage" + printf '%s\n' "xwin produced an incomplete CRT/SDK cache." >&2 + exit 1 + fi + printf '%s\n' "xwin-splat-complete" \ + > "$xwin_stage/.palschema-sdk-complete" + # Flush the staged SDK and completion marker before publishing it. + sync -f "$xwin_stage" + fsync_directory "$xwin_stage" + fsync_directory "$xwin_parent" + + if [[ -e "$xwin_previous" ]]; then + rm -rf -- "$xwin_previous" + fsync_directory "$xwin_parent" + fi + if [[ -e "$palschema_xwin_dir" ]]; then + mv -- "$palschema_xwin_dir" "$xwin_previous" + fsync_directory "$xwin_parent" + fi + if ! mv -- "$xwin_stage" "$palschema_xwin_dir"; then + if [[ ! -e "$palschema_xwin_dir" && -e "$xwin_previous" ]]; then + mv -- "$xwin_previous" "$palschema_xwin_dir" + fi + exit 1 + fi + fsync_directory "$xwin_parent" + if [[ -e "$xwin_previous" ]]; then + rm -rf -- "$xwin_previous" + fsync_directory "$xwin_parent" + fi + fi +fi + +if ! xwin_cache_is_ready "$palschema_xwin_dir"; then + printf 'The xwin SDK is not prepared at %s.\n' "$palschema_xwin_dir" >&2 + printf '%s\n' \ + "After reviewing the Microsoft terms, prepare it explicitly with:" \ + " scripts/bootstrap-linux.sh --prepare-sdk --accept-microsoft-license" >&2 + exit 1 +fi + +if [[ ! -f deps/RE-UE4SS/deps/first/Unreal/CMakeLists.txt ]]; then + printf '%s\n' \ + "UEPseudo is missing. Initialize the authorized recursive submodules with:" \ + " git submodule update --init --recursive" >&2 + exit 1 +fi + +printf '%s\n' \ + "PalSchema Linux-to-Win64 prerequisites are ready." \ + "XWIN_DIR=$palschema_xwin_dir" \ + "Build Dev: scripts/build-linux.sh dev" \ + "Build Shipping: scripts/build-linux.sh shipping" diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100755 index 0000000..b02245b --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage: scripts/build-linux.sh [dev|shipping] [--configure-only]" \ + "" \ + "Cross-compiles the Win64 PalSchema DLL from a Linux host." +} + +build_flavor="${1:-dev}" +configure_only=false + +if (($# > 0)); then + shift +fi + +while (($# > 0)); do + case "$1" in + --configure-only) + configure_only=true + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + esac + shift +done + +case "$build_flavor" in + dev) + preset="win64-xwin-dev" + ;; + shipping) + preset="win64-xwin-shipping" + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown build flavor: %s\n\n' "$build_flavor" >&2 + show_usage >&2 + exit 2 + ;; +esac + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +project_root="$(cd -- "$script_dir/.." && pwd)" +cd "$project_root" + +source "$script_dir/lib/build-env.sh" +palschema_configure_build_environment + +"$script_dir/bootstrap-linux.sh" + +if command -v rustup >/dev/null 2>&1; then + rust_compiler="$(rustup which rustc)" + rust_cargo="$(rustup which cargo)" +else + rust_compiler="$(command -v rustc)" + rust_cargo="$(command -v cargo)" +fi + +export PALSCHEMA_REAL_CARGO="$rust_cargo" + +cmake \ + --preset "$preset" \ + -DRust_COMPILER="$rust_compiler" \ + -DRust_CARGO="$script_dir/cargo-preserve-lock.sh" + +if [[ "$configure_only" != true ]]; then + cmake --build --preset "$preset" --target PalSchema + + artifact="$project_root/build/$preset/PalSchema.dll" + if [[ ! -f "$artifact" ]]; then + printf 'Build completed without the expected artifact: %s\n' "$artifact" >&2 + exit 1 + fi + + printf 'Built %s\n' "$artifact" +fi diff --git a/scripts/cargo-preserve-lock.sh b/scripts/cargo-preserve-lock.sh new file mode 100755 index 0000000..85a2de0 --- /dev/null +++ b/scripts/cargo-preserve-lock.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +project_root="$(cd -- "$script_dir/.." && pwd)" + +source "$script_dir/lib/build-env.sh" +palschema_configure_build_environment + +real_cargo="${PALSCHEMA_REAL_CARGO:-}" +if [[ -z "$real_cargo" ]]; then + if command -v rustup >/dev/null 2>&1; then + real_cargo="$(rustup which cargo)" + else + real_cargo="$(command -v cargo)" + fi +fi + +if [[ "$real_cargo" == "$0" || "$real_cargo" == "$script_dir/cargo-preserve-lock.sh" ]]; then + printf '%s\n' "Cargo lockfile guard resolved itself instead of the real Cargo binary." >&2 + exit 1 +fi + +lock_file="$project_root/deps/RE-UE4SS/deps/first/patternsleuth_bind/Cargo.lock" +if [[ ! -f "$lock_file" ]]; then + exec "$real_cargo" "$@" +fi + +mkdir -p "$PALSCHEMA_CACHE_ROOT" +guard_file="$PALSCHEMA_CACHE_ROOT/patternsleuth-bind-cargo.lock.guard" +exec {guard_fd}>"$guard_file" +flock "$guard_fd" + +lock_backup="$(mktemp "$PALSCHEMA_CACHE_ROOT/patternsleuth-bind-cargo.lock.XXXXXX")" +cp -p -- "$lock_file" "$lock_backup" + +cleanup() { + local status=$? + + trap - EXIT + if ! cmp -s -- "$lock_backup" "$lock_file"; then + if ! cp -p -- "$lock_backup" "$lock_file"; then + printf 'Failed to restore %s after Cargo exited.\n' "$lock_file" >&2 + status=1 + fi + fi + if ! rm -f -- "$lock_backup"; then + printf 'Failed to remove temporary lockfile backup: %s\n' "$lock_backup" >&2 + status=1 + fi + + exit "$status" +} + +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +"$real_cargo" "$@" diff --git a/scripts/ci/run-public-source-checks.sh b/scripts/ci/run-public-source-checks.sh new file mode 100755 index 0000000..ce6d3fb --- /dev/null +++ b/scripts/ci/run-public-source-checks.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$project_root" + +required_commands=(bash git node npm python3) +missing_commands=() + +for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null 2>&1; then + missing_commands+=("$required_command") + fi +done + +if ((${#missing_commands[@]} > 0)); then + printf 'Missing public-check commands: %s\n' "${missing_commands[*]}" >&2 + exit 1 +fi + +node_major="$(node --version | sed -E 's/^v([0-9]+).*/\1/')" +if [[ ! "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 20)); then + printf 'Node.js 20 or newer is required; found %s.\n' "$(node --version)" >&2 + exit 1 +fi + +printf '%s\n' \ + "PalSchema public source checks" \ + "OS: $(. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-unknown}")" \ + "Kernel: $(uname -srmo)" \ + "Node: $(node --version)" \ + "npm: $(npm --version)" \ + "Python: $(python3 --version 2>&1)" + +bash -n \ + scripts/bootstrap-linux.sh \ + scripts/build-linux.sh \ + scripts/cargo-preserve-lock.sh \ + scripts/deploy-proton.sh \ + scripts/package-linux.sh \ + scripts/test-distro-matrix.sh \ + scripts/test-win64-server.sh \ + scripts/tests/test-deploy-transaction.sh \ + scripts/tests/test-bootstrap-cache.sh \ + scripts/tests/test-distro-matrix-output.sh \ + scripts/tests/test-process-scan.sh \ + scripts/tests/test-win64-server-process-guard.sh \ + scripts/ci/run-public-source-checks.sh \ + scripts/lib/build-env.sh \ + scripts/lib/process-scan.sh + +python3 -m py_compile scripts/verify-win64-artifact.py +python3 -m unittest discover -s scripts/tests -p 'test_*.py' +node --check scripts/copy-public-schemas.mjs +scripts/tests/test-deploy-transaction.sh +scripts/tests/test-bootstrap-cache.sh +scripts/tests/test-distro-matrix-output.sh +scripts/tests/test-process-scan.sh +scripts/tests/test-win64-server-process-guard.sh +python3 -m json.tool CMakePresets.json >/dev/null +python3 -m json.tool assets/.vscode/settings.json >/dev/null +python3 -m json.tool assets/schemas/schema-index.json >/dev/null + +npm ci +npm audit --audit-level=moderate +npm run tools:typecheck +npm run typecheck --workspace palschema-vscode +npm run editor:test +npm run tools:test +npm run tools:build +node tools/palschema-tools/dist/cli.js schemas verify +node tools/palschema-tools/dist/cli.js validate \ + --allow-missing-generated \ + assets/examples +npm run editor:build +npm run editor:package +npm pack \ + --workspace @palschema/tools \ + --pack-destination dist + +printf '%s\n' "PalSchema public source checks passed." diff --git a/scripts/copy-public-schemas.mjs b/scripts/copy-public-schemas.mjs new file mode 100755 index 0000000..d4ae985 --- /dev/null +++ b/scripts/copy-public-schemas.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import { + copyFile, + lstat, + mkdir, + readFile, + readdir, + realpath, + rm, +} from "node:fs/promises"; +import { + basename, + dirname, + isAbsolute, + relative, + resolve, + sep, +} from "node:path"; +import { fileURLToPath } from "node:url"; + +function assertSingleFileName(value, label) { + if ( + typeof value !== "string" || + !value || + value === "." || + value === ".." || + isAbsolute(value) || + basename(value) !== value || + value.includes("/") || + value.includes("\\") + ) { + throw new Error(`Invalid ${label}: ${String(value)}`); + } +} + +function containedPath(root, relativeFile) { + const candidate = resolve(root, relativeFile); + const fromRoot = relative(root, candidate); + if ( + !fromRoot || + fromRoot === ".." || + fromRoot.startsWith(`..${sep}`) || + isAbsolute(fromRoot) + ) { + throw new Error(`Schema path escapes its pack: ${relativeFile}`); + } + return candidate; +} + +function pathsOverlap(left, right) { + const fromLeft = relative(left, right); + const fromRight = relative(right, left); + return ( + fromLeft === "" || + (!fromLeft.startsWith(`..${sep}`) && fromLeft !== ".." && !isAbsolute(fromLeft)) || + (!fromRight.startsWith(`..${sep}`) && fromRight !== ".." && !isAbsolute(fromRight)) + ); +} + +async function canonicalPotentialPath(path) { + const missingSegments = []; + let candidate = path; + for (;;) { + try { + return resolve( + await realpath(candidate), + ...missingSegments.reverse(), + ); + } catch (error) { + if (error?.code !== "ENOENT") { + throw error; + } + const parent = dirname(candidate); + if (parent === candidate) { + throw error; + } + missingSegments.push(basename(candidate)); + candidate = parent; + } + } +} + +async function rejectGeneratedContent(source, entry) { + const generatedPath = containedPath(source, entry.file); + if (existsSync(generatedPath)) { + throw new Error( + `Refusing to package runtime-generated schema: ${entry.file}`, + ); + } + for (const pattern of entry.supportPatterns ?? []) { + const match = /^([^/]+)\/\*\.schema\.json$/.exec(pattern); + if (!match) { + throw new Error(`Unsupported schema support pattern: ${pattern}`); + } + const directory = containedPath(source, match[1]); + if (!existsSync(directory)) { + continue; + } + const generatedFiles = (await readdir(directory)).filter((file) => + file.endsWith(".schema.json"), + ); + if (generatedFiles.length > 0) { + throw new Error( + `Refusing to package runtime-generated schema support files below ${match[1]}/`, + ); + } + } +} + +export async function copyPublicSchemas(sourceDirectory, destinationDirectory) { + const source = resolve(sourceDirectory); + const destination = resolve(destinationDirectory); + if ( + pathsOverlap(source, destination) || + pathsOverlap( + await realpath(source), + await canonicalPotentialPath(destination), + ) + ) { + throw new Error("Schema source and destination must not overlap."); + } + + const indexPath = resolve(source, "schema-index.json"); + const index = JSON.parse(await readFile(indexPath, "utf8")); + if ( + index?.formatVersion !== 1 || + !Array.isArray(index.schemas) + ) { + throw new Error("Unsupported or malformed schema-index.json."); + } + + const staticEntries = []; + for (const entry of index.schemas) { + assertSingleFileName(entry.file, "schema file"); + if (entry.generated) { + await rejectGeneratedContent(source, entry); + continue; + } + if ( + typeof entry.sha256 !== "string" || + !/^[0-9a-f]{64}$/.test(entry.sha256) + ) { + throw new Error(`Static schema has no valid checksum: ${entry.file}`); + } + const schemaPath = containedPath(source, entry.file); + const metadata = await lstat(schemaPath); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error(`Static schema must be a regular file: ${entry.file}`); + } + const content = await readFile(schemaPath); + const digest = createHash("sha256").update(content).digest("hex"); + if (digest !== entry.sha256) { + throw new Error(`Static schema checksum mismatch: ${entry.file}`); + } + staticEntries.push({ entry, schemaPath }); + } + + await rm(destination, { recursive: true, force: true }); + await mkdir(destination, { recursive: true }); + await copyFile(indexPath, resolve(destination, "schema-index.json")); + for (const { entry, schemaPath } of staticEntries) { + const output = containedPath(destination, entry.file); + await mkdir(dirname(output), { recursive: true }); + await copyFile(schemaPath, output); + } +} + +const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ""; +if (invokedPath === fileURLToPath(import.meta.url)) { + const [source, destination, ...extra] = process.argv.slice(2); + if (!source || !destination || extra.length > 0) { + console.error( + "Usage: scripts/copy-public-schemas.mjs SOURCE_DIR DESTINATION_DIR", + ); + process.exitCode = 2; + } else { + try { + await copyPublicSchemas(source, destination); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } + } +} diff --git a/scripts/deploy-proton.sh b/scripts/deploy-proton.sh new file mode 100755 index 0000000..191806f --- /dev/null +++ b/scripts/deploy-proton.sh @@ -0,0 +1,660 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage:" \ + " scripts/deploy-proton.sh [dev|shipping] [--target auto|client|server]" \ + " [--game-dir PATH] [--dry-run]" \ + " scripts/deploy-proton.sh --rollback BACKUP_DIR [--target auto|client|server]" \ + " [--game-dir PATH] [--dry-run]" \ + "" \ + "Deploys only PalSchema into an existing, separately installed UE4SS." \ + "Refuses to run unless the selected Windows Palworld target can be" \ + "confirmed stopped through a complete /proc command-line scan." +} + +build_flavor="shipping" +game_dir="" +rollback_dir="" +dry_run=false +target_kind="auto" + +if (($# > 0)) && [[ "$1" != --* ]]; then + build_flavor="$1" + shift +fi + +while (($# > 0)); do + case "$1" in + --game-dir) + if (($# < 2)); then + printf '%s\n' "--game-dir requires a path." >&2 + exit 2 + fi + game_dir="$2" + shift + ;; + --rollback) + if (($# < 2)); then + printf '%s\n' "--rollback requires a backup directory." >&2 + exit 2 + fi + rollback_dir="$2" + shift + ;; + --target) + if (($# < 2)); then + printf '%s\n' "--target requires auto, client, or server." >&2 + exit 2 + fi + target_kind="$2" + shift + ;; + --dry-run) + dry_run=true + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + esac + shift +done + +case "$build_flavor" in + dev) + preset="win64-xwin-dev" + ;; + shipping) + preset="win64-xwin-shipping" + ;; + *) + printf 'Unknown build flavor: %s\n\n' "$build_flavor" >&2 + show_usage >&2 + exit 2 + ;; +esac + +case "$target_kind" in + auto|client|server) + ;; + *) + printf 'Unknown target: %s\n\n' "$target_kind" >&2 + show_usage >&2 + exit 2 + ;; +esac + +required_commands=( + awk + cp + flock + install + mktemp + mv + python3 + realpath + rm + sha256sum + sync +) +if [[ "$build_flavor" == "dev" ]]; then + required_commands+=(node) +fi +for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'Missing required deployment command: %s\n' "$required_command" >&2 + exit 1 + fi +done + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +project_root="$(cd -- "$script_dir/.." && pwd)" +source "$script_dir/lib/build-env.sh" +source "$script_dir/lib/process-scan.sh" + +if [[ -z "$game_dir" ]]; then + default_game_dir="$HOME/.local/share/Steam/steamapps/common/Palworld" + if [[ -d "$default_game_dir" ]]; then + game_dir="$default_game_dir" + else + printf '%s\n' \ + "Palworld was not found in Steam's default library." \ + "Pass its installation directory with --game-dir." >&2 + exit 1 + fi +fi + +game_dir="$(realpath -e -- "$game_dir")" +win64_dir="$game_dir/Pal/Binaries/Win64" +ue4ss_root="$win64_dir/ue4ss" +mods_dir="$ue4ss_root/Mods" +target_dir="$mods_dir/PalSchema" +backup_root="$ue4ss_root/.palschema-backups" +transaction_marker="$backup_root/.active-transaction" +stage_dir="" +transaction_active=false + +fsync_file() { + python3 - "$1" <<'PY' +import os +import sys + +descriptor = os.open(sys.argv[1], os.O_RDONLY) +try: + os.fsync(descriptor) +finally: + os.close(descriptor) +PY +} + +fsync_directory() { + python3 - "$1" <<'PY' +import os +import sys + +descriptor = os.open(sys.argv[1], os.O_RDONLY | os.O_DIRECTORY) +try: + os.fsync(descriptor) +finally: + os.close(descriptor) +PY +} + +assert_tree_has_no_symlinks() { + python3 - "$1" <<'PY' +import os +import sys + +root = sys.argv[1] +for directory, directories, files in os.walk(root, followlinks=False): + for name in [*directories, *files]: + path = os.path.join(directory, name) + if os.path.islink(path): + raise SystemExit(f"Backup tree contains a symlink: {path}") +PY +} + +rename_directory() { + python3 - "$1" "$2" <<'PY' +import os +import sys + +os.rename(sys.argv[1], sys.argv[2]) +PY +} + +exchange_directories() { + python3 - "$1" "$2" <<'PY' +import ctypes +import os +import sys + +libc = ctypes.CDLL(None, use_errno=True) +renameat2 = libc.renameat2 +renameat2.argtypes = [ + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_uint, +] +renameat2.restype = ctypes.c_int +at_fdcwd = -100 +rename_exchange = 2 +left = os.fsencode(sys.argv[1]) +right = os.fsencode(sys.argv[2]) +if renameat2(at_fdcwd, left, at_fdcwd, right, rename_exchange) != 0: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) +PY +} + +if [[ "$target_kind" == "auto" ]]; then + if [[ -f "$win64_dir/Palworld-Win64-Shipping.exe" ]]; then + target_kind="client" + elif [[ -f "$win64_dir/PalServer-Win64-Shipping.exe" || + -f "$win64_dir/PalServer-Win64-Shipping-Cmd.exe" ]]; then + target_kind="server" + else + printf 'Not a Palworld Win64 client or server installation: %s\n' "$game_dir" >&2 + exit 1 + fi +fi + +if [[ "$target_kind" == "client" && + ! -f "$win64_dir/Palworld-Win64-Shipping.exe" ]]; then + printf 'Not a Palworld Win64 client installation: %s\n' "$game_dir" >&2 + exit 1 +elif [[ "$target_kind" == "server" && + ! -f "$win64_dir/PalServer-Win64-Shipping.exe" && + ! -f "$win64_dir/PalServer-Win64-Shipping-Cmd.exe" ]]; then + printf 'Not a Palworld Win64 dedicated-server installation: %s\n' "$game_dir" >&2 + exit 1 +fi +if [[ ! -f "$ue4ss_root/UE4SS.dll" || ! -d "$mods_dir" ]]; then + printf 'A separate UE4SS installation was not found under: %s\n' "$win64_dir" >&2 + exit 1 +fi +if [[ -L "$target_dir" || -L "$backup_root" ]]; then + printf '%s\n' "Refusing to deploy through a symlinked PalSchema target or backup root." >&2 + exit 1 +fi + +ue4ss_root_real="$(realpath -e -- "$ue4ss_root")" +mods_dir_real="$(realpath -e -- "$mods_dir")" +case "$mods_dir_real/" in + "$ue4ss_root_real"/*/) + ;; + *) + printf 'UE4SS Mods directory escapes its installation: %s\n' "$mods_dir_real" >&2 + exit 1 + ;; +esac +mods_dir="$mods_dir_real" +target_dir="$mods_dir/PalSchema" +backup_root="$ue4ss_root_real/.palschema-backups" +transaction_marker="$backup_root/.active-transaction" + +assert_target_stopped() { + if target_is_stopped; then + return 0 + else + process_status=$? + fi + if ((process_status == 1)); then + printf 'Refusing to deploy while the Palworld Windows %s is active.\n' \ + "$target_kind" >&2 + exit 1 + fi + printf '%s\n' \ + "Refusing to deploy because one or more stable /proc process command lines are unreadable." \ + "Run as an account with complete process visibility after stopping the selected Win64 target." >&2 + exit 1 +} + +target_is_stopped() { + if palschema_target_process_status "$target_kind" /proc; then + return 1 + else + process_status=$? + if ((process_status == 1)); then + return 0 + fi + return 2 + fi +} + +assert_target_stopped + +validate_backup_id() { + [[ "$1" =~ ^(rollback-)?[0-9]{8}T[0-9]{6}Z-[0-9]+$ ]] +} + +validate_stage_name() { + [[ -z "$1" || "$1" =~ ^\.PalSchema\.stage\.[A-Za-z0-9]+$ ]] +} + +recover_incomplete_transaction() { + local marker_lines=() + local backup_id + local stage_name + local recovery_backup + local stale_stage + + if [[ ! -f "$transaction_marker" ]]; then + transaction_active=false + return 0 + fi + mapfile -t marker_lines < "$transaction_marker" + backup_id="${marker_lines[0]:-}" + stage_name="${marker_lines[1]:-}" + if ! validate_backup_id "$backup_id" || ! validate_stage_name "$stage_name"; then + printf 'Invalid deployment transaction marker: %s\n' "$transaction_marker" >&2 + return 1 + fi + recovery_backup="$backup_root/$backup_id" + if [[ -L "$recovery_backup" || -L "$recovery_backup/PalSchema" ]]; then + printf 'Deployment transaction backup contains a symlink: %s\n' \ + "$recovery_backup" >&2 + return 1 + fi + if [[ ! -d "$recovery_backup" ]]; then + printf 'Deployment transaction backup is missing: %s\n' "$recovery_backup" >&2 + return 1 + fi + recovery_backup="$(realpath -e -- "$recovery_backup")" + case "$recovery_backup/" in + "$backup_root"/*/) + ;; + *) + printf 'Deployment transaction backup escapes its root: %s\n' \ + "$recovery_backup" >&2 + return 1 + ;; + esac + if [[ -d "$recovery_backup/PalSchema" ]]; then + assert_tree_has_no_symlinks "$recovery_backup/PalSchema" + fi + + if [[ -d "$recovery_backup/PalSchema" ]]; then + if [[ -d "$target_dir" ]]; then + exchange_directories "$recovery_backup/PalSchema" "$target_dir" + rm -rf -- "$recovery_backup/PalSchema" + else + rename_directory "$recovery_backup/PalSchema" "$target_dir" + fi + fsync_directory "$mods_dir" + fsync_directory "$recovery_backup" + printf 'Recovered interrupted PalSchema transaction from %s\n' \ + "$recovery_backup" + elif [[ ! -e "$target_dir" && + ! -f "$recovery_backup/no-previous-install" ]]; then + printf '%s\n' "Interrupted transaction has neither a live mod nor a recoverable backup." >&2 + return 1 + fi + + if [[ -n "$stage_name" ]]; then + stale_stage="$mods_dir/$stage_name" + if [[ -d "$stale_stage" && ! -L "$stale_stage" ]]; then + assert_tree_has_no_symlinks "$stale_stage" + if [[ -d "$target_dir" && + ! -e "$recovery_backup/PalSchema" && + ! -f "$recovery_backup/no-previous-install" ]]; then + # The stage is the new candidate before the exchange and the + # previous live tree after it. Preserve it in either case: + # recovery must not guess which side of the atomic exchange + # reached disk. + rename_directory "$stale_stage" \ + "$recovery_backup/PalSchema" + fsync_directory "$mods_dir" + fsync_directory "$recovery_backup" + printf 'Preserved interrupted PalSchema stage at %s\n' \ + "$recovery_backup/PalSchema" + else + rm -rf -- "$stale_stage" + fsync_directory "$mods_dir" + fi + fi + fi + rm -f -- "$transaction_marker" + fsync_directory "$backup_root" + transaction_active=false +} + +begin_transaction() { + local backup_id="$1" + local stage_name="$2" + local temporary_marker="$backup_root/.active-transaction.tmp.$$" + + validate_backup_id "$backup_id" + validate_stage_name "$stage_name" + { + printf '%s\n' "$backup_id" + printf '%s\n' "$stage_name" + } > "$temporary_marker" + fsync_file "$temporary_marker" + mv -- "$temporary_marker" "$transaction_marker" + fsync_directory "$backup_root" + transaction_active=true +} + +commit_transaction() { + fsync_directory "$mods_dir" + fsync_directory "$backup_root" + rm -f -- "$transaction_marker" + fsync_directory "$backup_root" + transaction_active=false +} + +cleanup() { + local status=$? + if [[ "$transaction_active" == true ]]; then + recover_incomplete_transaction || true + fi + if [[ -n "$stage_dir" && -d "$stage_dir" && ! -L "$stage_dir" ]]; then + rm -rf -- "$stage_dir" + fi + return "$status" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +if [[ "$dry_run" != true ]]; then + mkdir -p "$backup_root" + if [[ -L "$backup_root" ]]; then + printf 'Refusing to use a symlinked backup root: %s\n' "$backup_root" >&2 + exit 1 + fi + backup_root="$(realpath -e -- "$backup_root")" + case "$backup_root/" in + "$ue4ss_root_real"/*/) + ;; + *) + printf 'PalSchema backup root escapes UE4SS: %s\n' "$backup_root" >&2 + exit 1 + ;; + esac + transaction_marker="$backup_root/.active-transaction" + exec {deployment_lock_fd}> "$backup_root/.deploy.lock" + if ! flock -n "$deployment_lock_fd"; then + printf '%s\n' "Another PalSchema deploy or rollback is active." >&2 + exit 1 + fi + recover_incomplete_transaction +fi + +if [[ -n "$rollback_dir" ]]; then + if [[ ! -d "$backup_root" ]]; then + printf 'No PalSchema backup root exists at: %s\n' "$backup_root" >&2 + exit 1 + fi + + backup_root_real="$(realpath -e -- "$backup_root")" + rollback_dir_real="$(realpath -e -- "$rollback_dir")" + case "$rollback_dir_real/" in + "$backup_root_real"/*/) + ;; + *) + printf 'Rollback directory must be below: %s\n' "$backup_root_real" >&2 + exit 1 + ;; + esac + if [[ -L "$rollback_dir_real/PalSchema" ]]; then + printf '%s\n' "Refusing to restore a symlinked PalSchema backup." >&2 + exit 1 + fi + if [[ ! -d "$rollback_dir_real/PalSchema" && + ! -f "$rollback_dir_real/no-previous-install" ]]; then + printf 'Not a valid PalSchema deployment backup: %s\n' "$rollback_dir_real" >&2 + exit 1 + fi + + if [[ "$dry_run" == true ]]; then + printf 'Would roll back %s using %s\n' "$target_dir" "$rollback_dir_real" + exit 0 + fi + + if [[ -d "$rollback_dir_real/PalSchema" ]]; then + if [[ ! -f "$rollback_dir_real/PalSchema/dlls/main.dll" || + -L "$rollback_dir_real/PalSchema/dlls/main.dll" ]]; then + printf 'Rollback backup has no regular dlls/main.dll: %s\n' \ + "$rollback_dir_real" >&2 + exit 1 + fi + assert_tree_has_no_symlinks "$rollback_dir_real/PalSchema" + stage_dir="$(mktemp -d "$mods_dir/.PalSchema.stage.XXXXXX")" + cp -a -- "$rollback_dir_real/PalSchema/." "$stage_dir/" + if [[ ! -f "$stage_dir/dlls/main.dll" ]]; then + printf '%s\n' "Rollback staging did not produce dlls/main.dll." >&2 + exit 1 + fi + assert_tree_has_no_symlinks "$stage_dir" + sync -f "$stage_dir" + fi + + rollback_id="rollback-$(date -u +%Y%m%dT%H%M%SZ)-$$" + rollback_safety="$backup_root/$rollback_id" + assert_target_stopped + mkdir -p "$rollback_safety" + had_live_target=false + if [[ ! -d "$target_dir" ]]; then + touch "$rollback_safety/no-previous-install" + else + had_live_target=true + fi + begin_transaction "$rollback_id" \ + "$([[ -n "$stage_dir" ]] && basename "$stage_dir" || true)" + if [[ -n "$stage_dir" ]]; then + if [[ "$had_live_target" == true ]]; then + exchange_directories "$stage_dir" "$target_dir" + else + rename_directory "$stage_dir" "$target_dir" + fi + fsync_directory "$mods_dir" + + if ! target_is_stopped; then + if [[ "$had_live_target" == true ]]; then + exchange_directories "$stage_dir" "$target_dir" + else + rename_directory "$target_dir" "$stage_dir" + fi + fsync_directory "$mods_dir" + commit_transaction + printf '%s\n' \ + "A Palworld process started during the atomic rollback; the previous live state was restored." >&2 + exit 1 + fi + + if [[ "$had_live_target" == true ]]; then + rename_directory "$stage_dir" "$rollback_safety/PalSchema" + fsync_directory "$rollback_safety" + fi + stage_dir="" + elif [[ "$had_live_target" == true ]]; then + rename_directory "$target_dir" "$rollback_safety/PalSchema" + fsync_directory "$mods_dir" + fsync_directory "$rollback_safety" + if ! target_is_stopped; then + rename_directory "$rollback_safety/PalSchema" "$target_dir" + fsync_directory "$mods_dir" + commit_transaction + printf '%s\n' \ + "A Palworld process started during the atomic rollback; the previous live state was restored." >&2 + exit 1 + fi + fi + commit_transaction + + printf 'Rolled back PalSchema from %s\n' "$rollback_dir_real" + printf 'Previous deployed state preserved at %s\n' "$rollback_safety" + exit 0 +fi + +artifact="$project_root/build/$preset/PalSchema.dll" +if [[ ! -f "$artifact" ]]; then + printf 'Missing build artifact: %s\n' "$artifact" >&2 + printf 'Build it first with: scripts/build-linux.sh %s\n' "$build_flavor" >&2 + exit 1 +fi +llvm_readobj="$(palschema_find_llvm_tool llvm-readobj)" +python3 "$script_dir/verify-win64-artifact.py" "$artifact" \ + --llvm-readobj "$llvm_readobj" >/dev/null + +if [[ "$build_flavor" == "dev" && + ! -f "$project_root/build/$preset/PalSchema.pdb" ]]; then + printf 'Missing Dev debug symbols: %s\n' \ + "$project_root/build/$preset/PalSchema.pdb" >&2 + exit 1 +fi + +if [[ "$dry_run" == true ]]; then + printf 'Would deploy %s to %s/dlls/main.dll\n' "$artifact" "$target_dir" + if [[ -d "$target_dir" ]]; then + printf 'Would preserve the existing mod below %s\n' "$backup_root" + else + printf '%s\n' "No existing PalSchema installation would be replaced." + fi + exit 0 +fi + +deploy_id="$(date -u +%Y%m%dT%H%M%SZ)-$$" +backup_dir="$backup_root/$deploy_id" + +stage_dir="$(mktemp -d "$mods_dir/.PalSchema.stage.XXXXXX")" +install -d "$stage_dir/dlls" "$stage_dir/mods" +install -m 0644 "$artifact" "$stage_dir/dlls/main.dll" +touch "$stage_dir/enabled.txt" +if [[ "$build_flavor" == "dev" ]]; then + install -m 0644 "$project_root/build/$preset/PalSchema.pdb" \ + "$stage_dir/dlls/main.pdb" + cp -a "$project_root/assets/.vscode" "$stage_dir/.vscode" + cp -a "$project_root/assets/examples" "$stage_dir/examples" + node "$script_dir/copy-public-schemas.mjs" \ + "$project_root/assets/schemas" "$stage_dir/schemas" +fi + +if [[ ! -f "$stage_dir/dlls/main.dll" ]]; then + printf '%s\n' "Deployment staging did not produce dlls/main.dll." >&2 + exit 1 +fi +assert_tree_has_no_symlinks "$stage_dir" +sync -f "$stage_dir" +assert_target_stopped +mkdir -p "$backup_dir" +had_live_target=false +if [[ ! -d "$target_dir" ]]; then + touch "$backup_dir/no-previous-install" +else + had_live_target=true +fi +begin_transaction "$deploy_id" "$(basename "$stage_dir")" +if [[ "$had_live_target" == true ]]; then + exchange_directories "$stage_dir" "$target_dir" +else + rename_directory "$stage_dir" "$target_dir" +fi +fsync_directory "$mods_dir" + +if ! target_is_stopped; then + if [[ "$had_live_target" == true ]]; then + exchange_directories "$stage_dir" "$target_dir" + else + rename_directory "$target_dir" "$stage_dir" + fi + fsync_directory "$mods_dir" + commit_transaction + printf '%s\n' \ + "A Palworld process started during the atomic deployment; the previous live state was restored." >&2 + exit 1 +fi + +if [[ "$had_live_target" == true ]]; then + rename_directory "$stage_dir" "$backup_dir/PalSchema" + fsync_directory "$backup_dir" +fi +stage_dir="" +if [[ "${PALSCHEMA_TEST_INTERRUPT_AFTER_BACKUP:-0}" == "1" ]]; then + kill -TERM "$$" +fi +commit_transaction + +{ + printf 'flavor=%s\n' "$build_flavor" + printf 'target=%s\n' "$target_kind" + printf 'artifact=%s\n' "$artifact" + printf 'sha256=%s\n' "$(sha256sum "$artifact" | awk '{print $1}')" + printf 'deployed_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +} > "$backup_dir/deployment.txt" + +printf 'Deployed PalSchema to %s\n' "$target_dir" +printf 'Rollback with: scripts/deploy-proton.sh --rollback %s --target %s --game-dir %s\n' \ + "$backup_dir" "$target_kind" "$game_dir" diff --git a/scripts/lib/build-env.sh b/scripts/lib/build-env.sh new file mode 100644 index 0000000..ee5b886 --- /dev/null +++ b/scripts/lib/build-env.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Shared environment setup for PalSchema's Linux-hosted cross-build scripts. +# This file is sourced; it intentionally does not enable shell options. + +PALSCHEMA_LLVM_VERSION_SUFFIXES=(22 21 20 19 18 17 16 15) + +palschema_configure_build_environment() { + if [[ -n "${XDG_CACHE_HOME:-}" ]]; then + palschema_cache_root="$XDG_CACHE_HOME/palschema" + else + user_home_dir="${HOME:?HOME must be set when XDG_CACHE_HOME is unset}" + palschema_cache_root="$user_home_dir/.cache/palschema" + fi + + export PALSCHEMA_CACHE_ROOT="${PALSCHEMA_CACHE_ROOT:-$palschema_cache_root}" + export PALSCHEMA_RUSTUP_HOME="${PALSCHEMA_RUSTUP_HOME:-$PALSCHEMA_CACHE_ROOT/rustup}" + export PALSCHEMA_CARGO_HOME="${PALSCHEMA_CARGO_HOME:-$PALSCHEMA_CACHE_ROOT/cargo}" + + if [[ -x "$PALSCHEMA_CARGO_HOME/bin/rustup" ]]; then + export RUSTUP_HOME="$PALSCHEMA_RUSTUP_HOME" + export CARGO_HOME="$PALSCHEMA_CARGO_HOME" + export PATH="$CARGO_HOME/bin:$PATH" + fi +} + +palschema_find_llvm_tool() { + local tool_name="$1" + local candidate + local suffix + + if command -v "$tool_name" >/dev/null 2>&1; then + command -v "$tool_name" + return 0 + fi + for suffix in "${PALSCHEMA_LLVM_VERSION_SUFFIXES[@]}"; do + candidate="$tool_name-$suffix" + if command -v "$candidate" >/dev/null 2>&1; then + command -v "$candidate" + return 0 + fi + done + + printf 'Unable to find %s (including supported version-suffixed names).\n' \ + "$tool_name" >&2 + return 1 +} + +palschema_llvm_tool_available() { + palschema_find_llvm_tool "$1" >/dev/null 2>&1 +} diff --git a/scripts/lib/process-scan.sh b/scripts/lib/process-scan.sh new file mode 100644 index 0000000..4c5877d --- /dev/null +++ b/scripts/lib/process-scan.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# Return 0 when a matching Win64 Palworld process exists, 1 after a complete +# negative scan, and 2 when any stable process command line cannot be read. +palschema_target_process_status() { + local target_kind="$1" + local proc_root="$2" + local cmdline + local cmdline_fd + local argument + local incomplete=false + local cmdlines=("$proc_root"/[0-9]*/cmdline) + + if [[ ! -d "$proc_root" || ! -r "$proc_root" || + "${cmdlines[0]}" == "$proc_root/[0-9]*/cmdline" ]]; then + return 2 + fi + + for cmdline in "${cmdlines[@]}"; do + if ! { exec {cmdline_fd}<"$cmdline"; } 2>/dev/null; then + # A process that vanished between glob expansion and open is benign. + if [[ -e "$cmdline" ]]; then + incomplete=true + fi + continue + fi + + while IFS= read -r -d '' argument <&"$cmdline_fd"; do + if [[ "$target_kind" == "client" ]]; then + case "$argument" in + Palworld-Win64-Shipping.exe|*/Palworld-Win64-Shipping.exe|*\\Palworld-Win64-Shipping.exe) + exec {cmdline_fd}<&- + return 0 + ;; + esac + else + case "$argument" in + PalServer.exe|*/PalServer.exe|*\\PalServer.exe|\ + PalServer-Win64-Shipping.exe|*/PalServer-Win64-Shipping.exe|*\\PalServer-Win64-Shipping.exe|\ + PalServer-Win64-Shipping-Cmd.exe|*/PalServer-Win64-Shipping-Cmd.exe|*\\PalServer-Win64-Shipping-Cmd.exe) + exec {cmdline_fd}<&- + return 0 + ;; + esac + fi + done + exec {cmdline_fd}<&- + done + + if [[ "$incomplete" == true ]]; then + return 2 + fi + return 1 +} diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh new file mode 100755 index 0000000..fe91d69 --- /dev/null +++ b/scripts/package-linux.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage: scripts/package-linux.sh [dev|shipping] [--output-dir PATH]" \ + "" \ + "Packages a Linux-built Win64 DLL in the standard UE4SS mod layout." +} + +build_flavor="${1:-shipping}" +if (($# > 0)); then + shift +fi + +output_dir="" +while (($# > 0)); do + case "$1" in + --output-dir) + if (($# < 2)); then + printf '%s\n' "--output-dir requires a path." >&2 + exit 2 + fi + output_dir="$2" + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + esac + shift +done + +case "$build_flavor" in + dev) + preset="win64-xwin-dev" + archive_suffix="_Dev" + ;; + shipping) + preset="win64-xwin-shipping" + archive_suffix="" + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown build flavor: %s\n\n' "$build_flavor" >&2 + show_usage >&2 + exit 2 + ;; +esac + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +project_root="$(cd -- "$script_dir/.." && pwd)" +source "$script_dir/lib/build-env.sh" +artifact="$project_root/build/$preset/PalSchema.dll" +pdb="$project_root/build/$preset/PalSchema.pdb" + +if [[ ! -f "$artifact" ]]; then + printf 'Missing build artifact: %s\n' "$artifact" >&2 + printf 'Build it first with: scripts/build-linux.sh %s\n' "$build_flavor" >&2 + exit 1 +fi + +required_commands=(install mktemp python3 zip) +if [[ "$build_flavor" == "dev" ]]; then + required_commands+=(node) +fi +for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'Missing required packaging command: %s\n' "$required_command" >&2 + exit 1 + fi +done + +llvm_readobj="$(palschema_find_llvm_tool llvm-readobj)" +python3 "$script_dir/verify-win64-artifact.py" "$artifact" \ + --llvm-readobj "$llvm_readobj" >/dev/null + +version="$( + awk ' + /VERSION_MAJOR[[:space:]]+[0-9]+/ { major = $3 } + /VERSION_MINOR[[:space:]]+[0-9]+/ { minor = $3 } + /VERSION_REVISION[[:space:]]+[0-9]+/ { revision = $3 } + END { printf "%s.%s.%s", major, minor, revision } + ' "$project_root/version.h" +)" + +if [[ -z "$output_dir" ]]; then + output_dir="$project_root/dist" +fi +mkdir -p "$output_dir" +output_dir="$(cd -- "$output_dir" && pwd)" + +package_dir="$(mktemp -d "${TMPDIR:-/tmp}/palschema-package.XXXXXX")" +trap 'rm -rf -- "$package_dir"' EXIT + +mod_root="$package_dir/PalSchema" +install -d "$mod_root/dlls" "$mod_root/mods" +install -m 0644 "$artifact" "$mod_root/dlls/main.dll" +touch "$mod_root/enabled.txt" + +if [[ "$build_flavor" == dev ]]; then + if [[ ! -f "$pdb" ]]; then + printf 'Missing Dev debug symbols: %s\n' "$pdb" >&2 + exit 1 + fi + + install -m 0644 "$pdb" "$mod_root/dlls/main.pdb" + cp -a "$project_root/assets/.vscode" "$mod_root/.vscode" + cp -a "$project_root/assets/examples" "$mod_root/examples" + node "$script_dir/copy-public-schemas.mjs" \ + "$project_root/assets/schemas" "$mod_root/schemas" +fi + +python3 "$script_dir/verify-win64-artifact.py" \ + "$mod_root/dlls/main.dll" --llvm-readobj "$llvm_readobj" >/dev/null + +archive_name="PalSchema_${version}_Win64${archive_suffix}.zip" +temporary_archive="$package_dir/$archive_name" +( + cd "$package_dir" + zip -q -r "$temporary_archive" PalSchema +) +install -m 0644 "$temporary_archive" "$output_dir/$archive_name" + +printf 'Packaged %s\n' "$output_dir/$archive_name" diff --git a/scripts/test-distro-matrix.sh b/scripts/test-distro-matrix.sh new file mode 100755 index 0000000..6f3441b --- /dev/null +++ b/scripts/test-distro-matrix.sh @@ -0,0 +1,326 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage: scripts/test-distro-matrix.sh [options] [distro ...]" \ + "" \ + "Runs PalSchema's public source checks in clean Linux containers." \ + "" \ + "Distros:" \ + " debian ubuntu fedora opensuse arch" \ + "" \ + "Options:" \ + " --list Print the supported distro/image mapping." \ + " --no-pull Use an already available local image." \ + " --build-shipping Also cross-build and verify the Win64 Shipping DLL." \ + " --node-major N Test with the latest verified Node.js N.x (default: 22)." \ + " --timeout-minutes N Stop one distro after N minutes (default: 10)." \ + " -h, --help Show this help." \ + "" \ + "With no distro arguments, all supported distros are tested." \ + "--build-shipping reuses an already prepared PalSchema cache and never" \ + "downloads or silently accepts the Microsoft SDK license." +} + +project_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +node_major=22 +timeout_minutes=10 +pull_images=true +build_shipping=false +selected_distros=() + +declare -A distro_images=( + [debian]="debian:stable-slim" + [ubuntu]="ubuntu:24.04" + [fedora]="fedora:latest" + [opensuse]="opensuse/tumbleweed:latest" + [arch]="archlinux:latest" +) + +all_distros=(debian ubuntu fedora opensuse arch) + +list_distros() { + local distro + for distro in "${all_distros[@]}"; do + printf '%-10s %s\n' "$distro" "${distro_images[$distro]}" + done +} + +while (($# > 0)); do + case "$1" in + --list) + list_distros + exit 0 + ;; + --no-pull) + pull_images=false + ;; + --build-shipping) + build_shipping=true + ;; + --node-major) + if (($# < 2)); then + printf '%s\n' "--node-major requires a value." >&2 + exit 2 + fi + node_major="$2" + shift + ;; + --timeout-minutes) + if (($# < 2)); then + printf '%s\n' "--timeout-minutes requires a value." >&2 + exit 2 + fi + timeout_minutes="$2" + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + -*) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + *) + selected_distros+=("$1") + ;; + esac + shift +done + +if [[ ! "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 20)); then + printf '%s\n' "--node-major must be an integer greater than or equal to 20." >&2 + exit 2 +fi +if [[ ! "$timeout_minutes" =~ ^[0-9]+$ ]] || ((timeout_minutes < 1)); then + printf '%s\n' "--timeout-minutes must be a positive integer." >&2 + exit 2 +fi +matrix_timeout_seconds="$((timeout_minutes * 60))" +if [[ -n "${PALSCHEMA_MATRIX_TIMEOUT_SECONDS:-}" ]]; then + if [[ ! "$PALSCHEMA_MATRIX_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || + ((PALSCHEMA_MATRIX_TIMEOUT_SECONDS < 1)); then + printf '%s\n' "PALSCHEMA_MATRIX_TIMEOUT_SECONDS must be positive." >&2 + exit 2 + fi + matrix_timeout_seconds="$PALSCHEMA_MATRIX_TIMEOUT_SECONDS" +fi + +if ((${#selected_distros[@]} == 0)); then + selected_distros=("${all_distros[@]}") +fi + +for required_command in docker tail timeout; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf '%s is required to run the distro matrix.\n' \ + "$required_command" >&2 + exit 1 + fi +done + +if [[ -n "${PALSCHEMA_CACHE_ROOT:-}" ]]; then + palschema_cache_root="$PALSCHEMA_CACHE_ROOT" +elif [[ -n "${XDG_CACHE_HOME:-}" ]]; then + palschema_cache_root="$XDG_CACHE_HOME/palschema" +else + user_home_dir="${HOME:?HOME must be set when XDG_CACHE_HOME is unset}" + palschema_cache_root="$user_home_dir/.cache/palschema" +fi + +if [[ "$build_shipping" == true ]]; then + required_cache_paths=( + "$palschema_cache_root/cargo/bin/rustup" + "$palschema_cache_root/rustup" + "$palschema_cache_root/xwin/crt" + "$palschema_cache_root/xwin/sdk" + ) + for required_cache_path in "${required_cache_paths[@]}"; do + if [[ ! -e "$required_cache_path" ]]; then + printf 'Prepared build cache is missing: %s\n' "$required_cache_path" >&2 + printf '%s\n' \ + "Run scripts/bootstrap-linux.sh with the explicit install/SDK options first." >&2 + exit 1 + fi + done +fi + +for distro in "${selected_distros[@]}"; do + if [[ -z "${distro_images[$distro]:-}" ]]; then + printf 'Unsupported distro: %s\n\n' "$distro" >&2 + list_distros >&2 + exit 2 + fi +done + +matrix_tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/palschema-distro-matrix.XXXXXX")" +archive_path="$matrix_tmp_dir/source.tar" +active_container="" + +cleanup() { + if [[ -n "$active_container" ]]; then + docker rm --force "$active_container" >/dev/null 2>&1 || true + fi + rm -rf -- "$matrix_tmp_dir" +} +trap cleanup EXIT INT TERM + +tar \ + --exclude=.git \ + --exclude=build \ + --exclude=dist \ + --exclude=graphify-out \ + --exclude=node_modules \ + -cf "$archive_path" \ + -C "$project_root" \ + . + +container_script=' +set -euo pipefail + +case "$PALSCHEMA_DISTRO" in + debian|ubuntu) + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends \ + bash ca-certificates coreutils curl git gzip python3 tar xz-utils + if [[ "$PALSCHEMA_BUILD_SHIPPING" == 1 ]]; then + apt-get install -y --no-install-recommends \ + clang clang-tools cmake lld llvm ninja-build util-linux + fi + rm -rf /var/lib/apt/lists/* + ;; + fedora) + dnf install -y \ + bash ca-certificates coreutils curl findutils git gzip python3 tar xz + if [[ "$PALSCHEMA_BUILD_SHIPPING" == 1 ]]; then + dnf install -y \ + clang clang-tools-extra cmake lld llvm ninja-build util-linux + fi + dnf clean all + ;; + opensuse) + zypper --non-interactive refresh + zypper --non-interactive install -y \ + bash ca-certificates coreutils curl findutils git gzip python3 tar xz + if [[ "$PALSCHEMA_BUILD_SHIPPING" == 1 ]]; then + zypper --non-interactive install -y \ + clang cmake lld llvm ninja util-linux + fi + zypper clean --all + ;; + arch) + pacman -Syu --noconfirm --needed \ + bash ca-certificates coreutils curl findutils git gzip python tar xz + if [[ "$PALSCHEMA_BUILD_SHIPPING" == 1 ]]; then + pacman -S --noconfirm --needed \ + clang cmake lld llvm ninja util-linux + fi + ;; + *) + printf "Unsupported container distro: %s\n" "$PALSCHEMA_DISTRO" >&2 + exit 2 + ;; +esac + +node_checksums_url="https://nodejs.org/dist/latest-v${PALSCHEMA_NODE_MAJOR}.x/SHASUMS256.txt" +curl --fail --location --silent --show-error \ + --output /tmp/node-shasums.txt \ + "$node_checksums_url" +node_archive_line="$( + grep -E " node-v${PALSCHEMA_NODE_MAJOR}[^ ]*-linux-x64.tar.xz\$" \ + /tmp/node-shasums.txt | + head -n 1 +)" +node_archive="${node_archive_line##* }" +if [[ -z "$node_archive" ]]; then + printf "Could not resolve a Node.js %s.x Linux x64 archive.\n" "$PALSCHEMA_NODE_MAJOR" >&2 + exit 1 +fi +node_version="${node_archive#node-}" +node_version="${node_version%-linux-x64.tar.xz}" +curl --fail --location --silent --show-error \ + --output "/tmp/$node_archive" \ + "https://nodejs.org/dist/$node_version/$node_archive" +( + cd /tmp + grep " $node_archive\$" node-shasums.txt | sha256sum --check - +) +tar -xJf "/tmp/$node_archive" -C /usr/local --strip-components=1 + +mkdir -p /workspace +tar -xf /input/source.tar -C /workspace +cd /workspace +scripts/ci/run-public-source-checks.sh + +if [[ "$PALSCHEMA_BUILD_SHIPPING" == 1 ]]; then + mkdir -p /palschema-cache + cp -a /palschema-cache-source/. /palschema-cache/ + export PALSCHEMA_CACHE_ROOT=/palschema-cache + export XWIN_DIR=/palschema-cache/xwin + scripts/build-linux.sh shipping + python3 scripts/verify-win64-artifact.py \ + build/win64-xwin-shipping/PalSchema.dll \ + --json-output build/win64-xwin-shipping/pe-contract.json + sha256sum build/win64-xwin-shipping/PalSchema.dll +fi +' + +failures=() +for distro in "${selected_distros[@]}"; do + image="${distro_images[$distro]}" + if [[ "$pull_images" == true ]]; then + docker pull "$image" + fi + + active_container="palschema-matrix-${distro}-$$" + distro_log="$matrix_tmp_dir/$distro.log" + distro_started="$SECONDS" + printf 'Testing %-10s (%s, timeout %ss)... ' \ + "$distro" "$image" "$matrix_timeout_seconds" + docker_args=( + --name "$active_container" + --rm + --volume "$matrix_tmp_dir:/input:ro" + --env "PALSCHEMA_DISTRO=$distro" + --env "PALSCHEMA_NODE_MAJOR=$node_major" + --env "PALSCHEMA_BUILD_SHIPPING=$([[ "$build_shipping" == true ]] && printf 1 || printf 0)" + ) + if [[ "$build_shipping" == true ]]; then + docker_args+=( + --volume "$palschema_cache_root:/palschema-cache-source:ro" + ) + fi + set +e + timeout --signal=TERM --kill-after=2s \ + "${matrix_timeout_seconds}s" \ + docker run "${docker_args[@]}" "$image" bash -c "$container_script" \ + 2>&1 | + tail -c 8388608 > "$distro_log" + run_status="${PIPESTATUS[0]}" + set -e + distro_elapsed="$((SECONDS - distro_started))" + if ((run_status == 0)); then + printf 'PASS (%ss)\n' "$distro_elapsed" + else + printf 'FAIL (%ss, exit %s)\n' "$distro_elapsed" "$run_status" + if ((run_status == 124)); then + printf 'Timed out after %s seconds.\n' "$matrix_timeout_seconds" >&2 + fi + printf '%s\n' "--- last 120 log lines for $distro ---" >&2 + tail -n 120 "$distro_log" >&2 + docker rm --force "$active_container" >/dev/null 2>&1 || true + failures+=("$distro") + fi + active_container="" +done + +if ((${#failures[@]} > 0)); then + printf '\nDistro matrix failures: %s\n' "${failures[*]}" >&2 + exit 1 +fi + +printf '\nPalSchema distro matrix passed: %s\n' "${selected_distros[*]}" diff --git a/scripts/test-win64-server.sh b/scripts/test-win64-server.sh new file mode 100755 index 0000000..f72491e --- /dev/null +++ b/scripts/test-win64-server.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash + +set -euo pipefail + +show_usage() { + printf '%s\n' \ + "Usage: scripts/test-win64-server.sh --game-dir PATH [options]" \ + "" \ + "Runs repeatable PalSchema startup smoke tests against an isolated" \ + "Win64 Palworld Dedicated Server under Wine." \ + "" \ + "Options:" \ + " --game-dir PATH Explicit isolated server root (required)." \ + " --cycles N Number of start/stop cycles (default: 3)." \ + " --game-port PORT Isolated UDP game port (default: 18211)." \ + " --query-port PORT Isolated UDP query port (default: 37015)." \ + " --startup-timeout S Seconds allowed for each startup (default: 120)." \ + " --hot-reload-file P Touch this safe fixture after each startup." \ + " --reload-timeout S Seconds allowed for hot-reload (default: 30)." \ + " --evidence-dir PATH Output directory for sanitized logs." \ + " -h, --help Show this help." +} + +project_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +source "$project_root/scripts/lib/process-scan.sh" +game_dir="" +cycles=3 +game_port=18211 +query_port=37015 +startup_timeout=120 +reload_timeout=30 +hot_reload_file="" +evidence_dir="" + +while (($# > 0)); do + case "$1" in + --game-dir|--cycles|--game-port|--query-port|--startup-timeout|--hot-reload-file|--reload-timeout|--evidence-dir) + if (($# < 2)); then + printf '%s requires a value.\n' "$1" >&2 + exit 2 + fi + case "$1" in + --game-dir) game_dir="$2" ;; + --cycles) cycles="$2" ;; + --game-port) game_port="$2" ;; + --query-port) query_port="$2" ;; + --startup-timeout) startup_timeout="$2" ;; + --hot-reload-file) hot_reload_file="$2" ;; + --reload-timeout) reload_timeout="$2" ;; + --evidence-dir) evidence_dir="$2" ;; + esac + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + show_usage >&2 + exit 2 + ;; + esac + shift +done + +if [[ -z "$game_dir" ]]; then + printf '%s\n' "--game-dir is required; automatic server discovery is intentionally disabled." >&2 + exit 2 +fi + +for numeric_value in "$cycles" "$game_port" "$query_port" "$startup_timeout" "$reload_timeout"; do + if [[ ! "$numeric_value" =~ ^[0-9]+$ ]] || ((numeric_value < 1)); then + printf 'Expected a positive integer, found: %s\n' "$numeric_value" >&2 + exit 2 + fi +done + +if ((game_port > 65535 || query_port > 65535)); then + printf '%s\n' "Ports must be between 1 and 65535." >&2 + exit 2 +fi +if ((game_port == query_port)); then + printf '%s\n' "Game and query ports must be different." >&2 + exit 2 +fi + +game_dir="$(cd -- "$game_dir" && pwd)" +win64_dir="$game_dir/Pal/Binaries/Win64" +server_exe="$win64_dir/PalServer-Win64-Shipping-Cmd.exe" +ue4ss_dir="$win64_dir/ue4ss" +ue4ss_log="$ue4ss_dir/UE4SS.log" +palschema_dll="$ue4ss_dir/Mods/PalSchema/dlls/main.dll" +mods_dir="$ue4ss_dir/Mods/PalSchema/mods" +wine_prefix="$game_dir/.compat/pfx" + +required_files=( + "$server_exe" + "$win64_dir/dwmapi.dll" + "$ue4ss_dir/UE4SS.dll" + "$ue4ss_dir/UE4SS-settings.ini" + "$ue4ss_dir/MemberVariableLayout.ini" + "$palschema_dll" +) +for required_file in "${required_files[@]}"; do + if [[ ! -f "$required_file" ]]; then + printf 'Required isolated-server file is missing: %s\n' "$required_file" >&2 + exit 1 + fi +done + +hot_reload_mod_name="" +if [[ -n "$hot_reload_file" ]]; then + if [[ ! -f "$hot_reload_file" ]]; then + printf 'Hot-reload fixture is missing: %s\n' "$hot_reload_file" >&2 + exit 1 + fi + hot_reload_file="$( + cd -- "$(dirname -- "$hot_reload_file")" + printf '%s/%s' "$PWD" "$(basename -- "$hot_reload_file")" + )" + case "$hot_reload_file" in + "$mods_dir"/*) + hot_reload_relative="${hot_reload_file#"$mods_dir"/}" + hot_reload_mod_name="${hot_reload_relative%%/*}" + ;; + *) + printf 'Hot-reload fixture must be inside %s\n' "$mods_dir" >&2 + exit 1 + ;; + esac +fi + +required_commands=(grep kill setsid ss timeout wine wineserver) +for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'Required command is missing: %s\n' "$required_command" >&2 + exit 1 + fi +done + +if palschema_target_process_status server /proc; then + printf 'The selected Win64 server is already running: %s\n' "$server_exe" >&2 + exit 1 +else + process_status=$? + if ((process_status == 2)); then + printf '%s\n' \ + "Cannot confirm that the isolated Win64 server is stopped because /proc visibility is incomplete." >&2 + exit 1 + fi +fi + +port_is_open() { + local port="$1" + ss -H -lun "sport = :$port" | grep -q . +} + +for port in "$game_port" "$query_port"; do + if port_is_open "$port"; then + printf 'UDP port %s is already in use; choose an isolated port.\n' "$port" >&2 + exit 1 + fi +done + +if [[ -z "$evidence_dir" ]]; then + timestamp="$(date -u +%Y%m%dT%H%M%SZ)" + evidence_dir="$project_root/build/runtime-evidence/$timestamp" +fi +mkdir -p "$evidence_dir" "$game_dir/.compat" +evidence_dir="$(cd -- "$evidence_dir" && pwd)" + +expected_loaders=( + enums + raw + blueprints + resources + pals + npcs + items + skins + appearance + buildings + helpguide + spawns + translations +) +expected_signature_count=22 +launcher_pid="" + +stop_selected_server() { + local wait_iteration + + if [[ -n "$launcher_pid" ]] && kill -0 "$launcher_pid" >/dev/null 2>&1; then + kill -INT -- "-$launcher_pid" >/dev/null 2>&1 || true + for wait_iteration in {1..20}; do + if ! kill -0 "$launcher_pid" >/dev/null 2>&1; then + break + fi + sleep 1 + done + fi + + if [[ -n "$launcher_pid" ]] && kill -0 "$launcher_pid" >/dev/null 2>&1; then + kill -TERM -- "-$launcher_pid" >/dev/null 2>&1 || true + sleep 2 + fi + if [[ -n "$launcher_pid" ]] && kill -0 "$launcher_pid" >/dev/null 2>&1; then + kill -KILL -- "-$launcher_pid" >/dev/null 2>&1 || true + fi + + WINEPREFIX="$wine_prefix" wineserver -k >/dev/null 2>&1 || true + launcher_pid="" +} +trap stop_selected_server EXIT INT TERM + +printf '%s\n' \ + "PalSchema Win64 server smoke suite" \ + "Server: $game_dir" \ + "Cycles: $cycles" \ + "Ports: $game_port/$query_port" \ + "Hot reload: ${hot_reload_file:-not requested}" \ + "Evidence: $evidence_dir" + +printf 'cycle\tstartup_seconds\tsignatures\tloaders\tgame_port\tquery_port\thot_reload\n' \ + > "$evidence_dir/summary.tsv" + +for ((cycle = 1; cycle <= cycles; cycle++)); do + cycle_dir="$evidence_dir/cycle-$cycle" + console_log="$cycle_dir/wine-console.log" + mkdir -p "$cycle_dir" + + if [[ -f "$ue4ss_log" ]]; then + mv -- "$ue4ss_log" "$cycle_dir/ue4ss-before-start.log" + fi + + cycle_started_at="$(date +%s)" + ( + cd "$win64_dir" + exec setsid env \ + WINEPREFIX="$wine_prefix" \ + WINEDLLOVERRIDES="dwmapi=n,b" \ + WINEDEBUG="-all" \ + SteamAppId="2394010" \ + wine "./$(basename -- "$server_exe")" \ + Pal \ + "-port=$game_port" \ + "-queryport=$query_port" \ + -players=4 \ + -useperfthreads \ + -NoAsyncLoadingThread \ + -UseMultithreadForDS + ) >"$console_log" 2>&1 & + launcher_pid=$! + + cycle_deadline=$((cycle_started_at + startup_timeout)) + cycle_ready=false + while (( $(date +%s) <= cycle_deadline )); do + if ! kill -0 "$launcher_pid" >/dev/null 2>&1; then + printf 'Cycle %s exited before reaching readiness.\n' "$cycle" >&2 + break + fi + + if [[ -f "$ue4ss_log" ]] && + grep -Fq "Event loop start" "$ue4ss_log" && + port_is_open "$game_port" && + port_is_open "$query_port"; then + all_loaders_ready=true + for loader in "${expected_loaders[@]}"; do + if ! grep -Fq "Loader '$loader' initialized." "$ue4ss_log"; then + all_loaders_ready=false + break + fi + done + if [[ "$all_loaders_ready" == true ]]; then + cycle_ready=true + break + fi + fi + sleep 1 + done + + if [[ -f "$ue4ss_log" ]]; then + cp -- "$ue4ss_log" "$cycle_dir/UE4SS.log" + fi + + if [[ "$cycle_ready" != true ]]; then + printf 'Cycle %s did not become ready within %s seconds.\n' \ + "$cycle" "$startup_timeout" >&2 + exit 1 + fi + + if grep -Eiq '\[PalSchema\].*(\[error\]|\[fatal\])' "$ue4ss_log"; then + printf 'Cycle %s contains a PalSchema error or fatal log entry.\n' "$cycle" >&2 + exit 1 + fi + + signature_count="$(grep -Fc "[PalSchema] Found " "$ue4ss_log")" + if ((signature_count != expected_signature_count)); then + printf 'Cycle %s found %s signatures; expected %s.\n' \ + "$cycle" "$signature_count" "$expected_signature_count" >&2 + exit 1 + fi + + loader_count=0 + for loader in "${expected_loaders[@]}"; do + if grep -Fq "Loader '$loader' initialized." "$ue4ss_log"; then + loader_count=$((loader_count + 1)) + fi + done + + hot_reload_result="not-run" + if [[ -n "$hot_reload_file" ]]; then + if ! grep -Fq "Auto-reload is enabled." "$ue4ss_log"; then + printf '%s\n' \ + "Hot-reload was requested, but PalSchema auto-reload is disabled." >&2 + exit 1 + fi + + reload_marker="Auto-reloaded mod $hot_reload_mod_name" + reload_count_before="$(grep -Fc "$reload_marker" "$ue4ss_log" || true)" + touch -- "$hot_reload_file" + reload_deadline=$(($(date +%s) + reload_timeout)) + while (( $(date +%s) <= reload_deadline )); do + reload_count_after="$(grep -Fc "$reload_marker" "$ue4ss_log" || true)" + if ((reload_count_after > reload_count_before)); then + hot_reload_result="passed" + break + fi + sleep 1 + done + if [[ "$hot_reload_result" != "passed" ]]; then + printf 'Cycle %s did not auto-reload %s within %s seconds.\n' \ + "$cycle" "$hot_reload_mod_name" "$reload_timeout" >&2 + exit 1 + fi + cp -- "$ue4ss_log" "$cycle_dir/UE4SS.log" + fi + + startup_seconds=$(($(date +%s) - cycle_started_at)) + printf '%s\t%s\t%s\t%s\topen\topen\t%s\n' \ + "$cycle" \ + "$startup_seconds" \ + "$signature_count" \ + "$loader_count" \ + "$hot_reload_result" \ + >> "$evidence_dir/summary.tsv" + + stop_selected_server + + for port in "$game_port" "$query_port"; do + if port_is_open "$port"; then + printf 'Cycle %s left UDP port %s open after shutdown.\n' "$cycle" "$port" >&2 + exit 1 + fi + done + + printf 'Cycle %s/%s passed in %ss: %s signatures, %s loaders.\n' \ + "$cycle" "$cycles" "$startup_seconds" "$signature_count" "$loader_count" +done + +trap - EXIT INT TERM +printf 'All %s cycle(s) passed. Evidence: %s\n' "$cycles" "$evidence_dir" diff --git a/scripts/tests/test-bootstrap-cache.sh b/scripts/tests/test-bootstrap-cache.sh new file mode 100755 index 0000000..0288a7f --- /dev/null +++ b/scripts/tests/test-bootstrap-cache.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/palschema-bootstrap-test.XXXXXX")" +trap 'rm -rf -- "$test_root"' EXIT + +project_root="$test_root/project" +fake_bin="$test_root/bin" +mkdir -p \ + "$project_root/scripts/lib" \ + "$project_root/deps/RE-UE4SS/deps/first/Unreal" \ + "$fake_bin" \ + "$test_root/rust-target" +cp -- "$repository_root/scripts/bootstrap-linux.sh" "$project_root/scripts/" +cp -- "$repository_root/scripts/lib/build-env.sh" "$project_root/scripts/lib/" +touch "$project_root/deps/RE-UE4SS/deps/first/Unreal/CMakeLists.txt" + +for tool in clang-cl cmake lld-link llvm-mt llvm-rc llvm-ranlib llvm-lib ninja; do + printf '%s\n' '#!/usr/bin/env sh' 'exit 0' > "$fake_bin/$tool" + chmod +x "$fake_bin/$tool" +done +cat > "$fake_bin/rustc" <<'EOF' +#!/usr/bin/env sh +if [ "$1" = "--print" ] && [ "$2" = "target-libdir" ]; then + printf '%s\n' "$FAKE_RUST_LIBDIR" + exit 0 +fi +exit 0 +EOF +cat > "$fake_bin/cargo" <<'EOF' +#!/usr/bin/env sh +exit 0 +EOF +cat > "$fake_bin/xwin" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "--version" ]]; then + printf '%s\n' "xwin 0.9.0" + exit 0 +fi +output="" +while (($# > 0)); do + if [[ "$1" == "--output" ]]; then + output="$2" + shift + fi + shift +done +{ + flock 9 + count=0 + if [[ -f "$FAKE_XWIN_COUNTER" ]]; then + count="$(cat "$FAKE_XWIN_COUNTER")" + fi + printf '%s\n' "$((count + 1))" > "$FAKE_XWIN_COUNTER" +} 9>"$FAKE_XWIN_COUNTER.lock" +mkdir -p \ + "$output/crt/include" \ + "$output/crt/lib/x86_64" \ + "$output/sdk/include/um" \ + "$output/sdk/lib/ucrt/x86_64" \ + "$output/sdk/lib/um/x86_64" +touch \ + "$output/crt/include/vcruntime.h" \ + "$output/crt/lib/x86_64/msvcrt.lib" +if [[ "${FAKE_XWIN_FAIL:-0}" == "1" ]]; then + exit 1 +fi +touch \ + "$output/sdk/include/um/Windows.h" \ + "$output/sdk/lib/ucrt/x86_64/ucrt.lib" \ + "$output/sdk/lib/um/x86_64/kernel32.Lib" +sleep 0.2 +EOF +chmod +x "$fake_bin/rustc" "$fake_bin/cargo" "$fake_bin/xwin" + +export PATH="$fake_bin:$PATH" +export FAKE_RUST_LIBDIR="$test_root/rust-target" +export FAKE_XWIN_COUNTER="$test_root/xwin-count" +export PALSCHEMA_CACHE_ROOT="$test_root/cache" +export XWIN_DIR="$PALSCHEMA_CACHE_ROOT/xwin" + +cd "$project_root" +"$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license >"$test_root/first.out" & +first_pid=$! +"$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license >"$test_root/second.out" & +second_pid=$! +wait "$first_pid" +wait "$second_pid" + +if [[ "$(cat "$FAKE_XWIN_COUNTER")" != "1" ]]; then + printf '%s\n' "Concurrent bootstrap runs performed more than one xwin splat." >&2 + exit 1 +fi +if [[ ! -f "$XWIN_DIR/.palschema-sdk-complete" ]]; then + printf '%s\n' "Concurrent bootstrap did not publish a completed cache." >&2 + exit 1 +fi + +export PALSCHEMA_CACHE_ROOT="$test_root/failure-cache" +export XWIN_DIR="$PALSCHEMA_CACHE_ROOT/xwin" +export FAKE_XWIN_COUNTER="$test_root/failure-xwin-count" +set +e +FAKE_XWIN_FAIL=1 "$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license \ + >"$test_root/failure.out" 2>"$test_root/failure.err" +failure_status=$? +set -e +if ((failure_status == 0)); then + printf '%s\n' "Interrupted xwin splat unexpectedly succeeded." >&2 + exit 1 +fi +if [[ -e "$XWIN_DIR" ]]; then + printf '%s\n' "Interrupted xwin splat published a partial cache." >&2 + exit 1 +fi +"$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license >"$test_root/retry.out" +if [[ ! -f "$XWIN_DIR/.palschema-sdk-complete" ]]; then + printf '%s\n' "Bootstrap retry did not publish a completed cache." >&2 + exit 1 +fi + +# A markerless legacy cache must be regenerated even when it contains every +# former representative sentinel. +export PALSCHEMA_CACHE_ROOT="$test_root/legacy-cache" +export XWIN_DIR="$PALSCHEMA_CACHE_ROOT/xwin" +export FAKE_XWIN_COUNTER="$test_root/legacy-xwin-count" +mkdir -p \ + "$XWIN_DIR/crt/include" \ + "$XWIN_DIR/crt/lib/x86_64" \ + "$XWIN_DIR/sdk/include/um" \ + "$XWIN_DIR/sdk/lib/ucrt/x86_64" \ + "$XWIN_DIR/sdk/lib/um/x86_64" +touch \ + "$XWIN_DIR/crt/include/vcruntime.h" \ + "$XWIN_DIR/crt/lib/x86_64/msvcrt.lib" \ + "$XWIN_DIR/sdk/include/um/Windows.h" \ + "$XWIN_DIR/sdk/lib/ucrt/x86_64/ucrt.lib" \ + "$XWIN_DIR/sdk/lib/um/x86_64/kernel32.Lib" +"$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license >"$test_root/legacy.out" +if [[ "$(cat "$FAKE_XWIN_COUNTER")" != "1" || + ! -f "$XWIN_DIR/.palschema-sdk-complete" ]]; then + printf '%s\n' "Markerless legacy cache was incorrectly certified." >&2 + exit 1 +fi + +# A custom XWIN_DIR outside the PalSchema cache must never replace an +# unrelated existing directory. +export PALSCHEMA_CACHE_ROOT="$test_root/owned-cache" +export XWIN_DIR="$test_root/unowned-sdk" +export FAKE_XWIN_COUNTER="$test_root/unowned-xwin-count" +mkdir -p "$XWIN_DIR" +printf '%s\n' "keep-me" > "$XWIN_DIR/sentinel" +set +e +"$project_root/scripts/bootstrap-linux.sh" \ + --prepare-sdk --accept-microsoft-license \ + >"$test_root/unowned.out" 2>"$test_root/unowned.err" +unowned_status=$? +set -e +if ((unowned_status == 0)) || + ! grep -q "Refusing to replace an unowned XWIN_DIR" \ + "$test_root/unowned.err" || + [[ "$(cat "$XWIN_DIR/sentinel")" != "keep-me" ]]; then + printf '%s\n' "Bootstrap did not preserve an unowned XWIN_DIR." >&2 + exit 1 +fi +export PALSCHEMA_CACHE_ROOT="$test_root/legacy-cache" +export XWIN_DIR="$PALSCHEMA_CACHE_ROOT/xwin" +export FAKE_XWIN_COUNTER="$test_root/legacy-xwin-count" + +# --install-rust-toolchain must select the PalSchema-local rustup even when +# system cargo and rustc are already available. +isolated_cargo="$test_root/isolated-cargo" +isolated_rustup="$test_root/isolated-rustup" +mkdir -p "$isolated_cargo/bin" "$isolated_rustup" +cp -- "$fake_bin/cargo" "$isolated_cargo/bin/cargo" +cp -- "$fake_bin/rustc" "$isolated_cargo/bin/rustc" +cat > "$isolated_cargo/bin/rustup" <<'EOF' +#!/usr/bin/env sh +printf '%s\n' "$*" >> "$FAKE_ISOLATED_RUSTUP_LOG" +exit 0 +EOF +chmod +x \ + "$isolated_cargo/bin/cargo" \ + "$isolated_cargo/bin/rustc" \ + "$isolated_cargo/bin/rustup" +export PALSCHEMA_CARGO_HOME="$isolated_cargo" +export PALSCHEMA_RUSTUP_HOME="$isolated_rustup" +export FAKE_ISOLATED_RUSTUP_LOG="$test_root/isolated-rustup.log" +export FAKE_RUST_LIBDIR="$test_root/missing-rust-target" +"$project_root/scripts/bootstrap-linux.sh" \ + --install-rust-toolchain >"$test_root/isolated-rust.out" +if ! grep -Fxq "target add x86_64-pc-windows-msvc" \ + "$FAKE_ISOLATED_RUSTUP_LOG"; then + printf '%s\n' "Bootstrap modified or used the wrong Rust toolchain." >&2 + exit 1 +fi + +printf '%s\n' "PalSchema bootstrap cache tests passed." diff --git a/scripts/tests/test-deploy-transaction.sh b/scripts/tests/test-deploy-transaction.sh new file mode 100755 index 0000000..02ca4b6 --- /dev/null +++ b/scripts/tests/test-deploy-transaction.sh @@ -0,0 +1,365 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/palschema-deploy-test.XXXXXX")" +trap 'rm -rf -- "$test_root"' EXIT + +project_root="$test_root/project" +game_root="$test_root/game" +fake_bin="$test_root/bin" +mkdir -p \ + "$project_root/scripts" \ + "$project_root/scripts/lib" \ + "$project_root/build/win64-xwin-shipping" \ + "$game_root/Pal/Binaries/Win64/ue4ss/Mods/PalSchema/dlls" \ + "$fake_bin" +cp -- "$repository_root/scripts/deploy-proton.sh" "$project_root/scripts/" +cp -- "$repository_root/scripts/verify-win64-artifact.py" "$project_root/scripts/" +cp -- "$repository_root/scripts/lib/build-env.sh" "$project_root/scripts/lib/" +cp -- "$repository_root/scripts/lib/process-scan.sh" "$project_root/scripts/lib/" +cp -- "$repository_root/version.h" "$project_root/version.h" +touch "$game_root/Pal/Binaries/Win64/PalServer-Win64-Shipping-Cmd.exe" +touch "$game_root/Pal/Binaries/Win64/ue4ss/UE4SS.dll" +printf '%s\n' "old-install" \ + > "$game_root/Pal/Binaries/Win64/ue4ss/Mods/PalSchema/dlls/main.dll" + +python3 - "$project_root/build/win64-xwin-shipping/PalSchema.dll" <<'PY' +from pathlib import Path +import sys + +Path(sys.argv[1]).write_bytes( + "PalSchema\0".encode("utf-16-le") + "0.6.1.0\0".encode("utf-16-le") +) +PY + +cat > "$fake_bin/llvm-readobj" <<'EOF' +#!/usr/bin/env python3 +import struct + + +def align4(content): + return content + b"\0" * (-len(content) % 4) + + +def node(key, value=b"", value_type=0, children=()): + encoded_key = f"{key}\0".encode("utf-16-le") + if isinstance(value, str): + encoded_value = f"{value}\0".encode("utf-16-le") + value_length = len(encoded_value) // 2 + else: + encoded_value = value + value_length = len(encoded_value) + content = align4(b"\0" * 6 + encoded_key) + content += encoded_value + content = align4(content) + content += b"".join(align4(child) for child in children) + return struct.pack(""$test_root/interrupted.out" 2>"$test_root/interrupted.err" +interrupt_status=$? +set -e +if ((interrupt_status != 143)); then + printf 'Expected interrupted deploy to exit 143, got %s.\n' \ + "$interrupt_status" >&2 + exit 1 +fi +if [[ "$(cat "$game_root/Pal/Binaries/Win64/ue4ss/Mods/PalSchema/dlls/main.dll")" \ + != "old-install" ]]; then + printf '%s\n' "Interrupted deploy did not restore the previous installation." >&2 + exit 1 +fi +if [[ -e "$game_root/Pal/Binaries/Win64/ue4ss/.palschema-backups/.active-transaction" ]]; then + printf '%s\n' "Interrupted deploy left an active transaction marker." >&2 + exit 1 +fi + +backup_root="$game_root/Pal/Binaries/Win64/ue4ss/.palschema-backups" +mods_root="$game_root/Pal/Binaries/Win64/ue4ss/Mods" +target_root="$mods_root/PalSchema" +crash_id="20000101T000000Z-2" +stale_stage_name=".PalSchema.stage.CRASHED" +mkdir -p \ + "$backup_root/$crash_id/PalSchema/dlls" \ + "$mods_root/$stale_stage_name/dlls" +printf '%s\n' "power-loss-old-install" \ + > "$backup_root/$crash_id/PalSchema/dlls/main.dll" +printf '%s\n' "abandoned-stage" \ + > "$mods_root/$stale_stage_name/dlls/main.dll" +rm -rf -- "$target_root" +printf '%s\n%s\n' "$crash_id" "$stale_stage_name" \ + > "$backup_root/.active-transaction" + +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/success.out" +if ! grep -q "Recovered interrupted PalSchema transaction" \ + "$test_root/success.out"; then + printf '%s\n' "A persisted power-loss transaction was not recovered." >&2 + exit 1 +fi +if ! grep -q "Deployed PalSchema" "$test_root/success.out"; then + printf '%s\n' "Recovered deployment did not complete." >&2 + exit 1 +fi +if [[ -e "$mods_root/$stale_stage_name" || + -e "$backup_root/.active-transaction" ]]; then + printf '%s\n' "Power-loss recovery left stale transaction state." >&2 + exit 1 +fi +if ! grep -R -q "power-loss-old-install" "$backup_root"/*/PalSchema/dlls/main.dll; then + printf '%s\n' "Recovered installation was not preserved by the next deploy." >&2 + exit 1 +fi + +# If power is lost after the atomic exchange but before the old tree reaches +# its backup, recovery must preserve the ambiguous stage instead of deleting it. +post_swap_id="20000101T000000Z-6" +post_swap_stage=".PalSchema.stage.POSTSWAP" +mkdir -p \ + "$backup_root/$post_swap_id" \ + "$mods_root/$post_swap_stage/dlls" +printf '%s\n' "post-swap-previous-install" \ + > "$mods_root/$post_swap_stage/dlls/main.dll" +printf '%s\n%s\n' "$post_swap_id" "$post_swap_stage" \ + > "$backup_root/.active-transaction" +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/post-swap.out" +if ! grep -q "Preserved interrupted PalSchema stage" \ + "$test_root/post-swap.out" || + ! grep -R -q "post-swap-previous-install" \ + "$backup_root"/*/PalSchema/dlls/main.dll; then + printf '%s\n' "Post-exchange recovery discarded the previous installation." >&2 + exit 1 +fi + +live_hash="$(sha256sum "$target_root/dlls/main.dll" | awk '{print $1}')" +missing_id="20000101T000000Z-3" +printf '%s\n\n' "$missing_id" > "$backup_root/.active-transaction" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/missing-backup.out" 2>"$test_root/missing-backup.err" +missing_backup_status=$? +set -e +if ((missing_backup_status == 0)) || + ! grep -q "transaction backup is missing" "$test_root/missing-backup.err"; then + printf '%s\n' "Missing transaction backup did not fail closed." >&2 + exit 1 +fi +if [[ "$(sha256sum "$target_root/dlls/main.dll" | awk '{print $1}')" != "$live_hash" ]]; then + printf '%s\n' "Missing-backup recovery modified the live installation." >&2 + exit 1 +fi +rm -f -- "$backup_root/.active-transaction" + +symlink_id="20000101T000000Z-4" +external_recovery="$test_root/external-recovery" +mkdir -p "$external_recovery/PalSchema/dlls" +printf '%s\n' "external-sentinel" \ + > "$external_recovery/PalSchema/dlls/main.dll" +ln -s "$external_recovery" "$backup_root/$symlink_id" +printf '%s\n\n' "$symlink_id" > "$backup_root/.active-transaction" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/symlink-recovery.out" 2>"$test_root/symlink-recovery.err" +symlink_recovery_status=$? +set -e +if ((symlink_recovery_status == 0)) || + ! grep -q "backup contains a symlink" "$test_root/symlink-recovery.err"; then + printf '%s\n' "Symlinked transaction backup did not fail closed." >&2 + exit 1 +fi +if [[ "$(cat "$external_recovery/PalSchema/dlls/main.dll")" != "external-sentinel" ]]; then + printf '%s\n' "Symlinked recovery moved or modified the external directory." >&2 + exit 1 +fi +rm -f -- "$backup_root/.active-transaction" "$backup_root/$symlink_id" + +printf '%s\n' "not-a-valid-transaction" > "$backup_root/.active-transaction" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/malformed.out" 2>"$test_root/malformed.err" +malformed_status=$? +set -e +if ((malformed_status == 0)) || + ! grep -q "Invalid deployment transaction marker" "$test_root/malformed.err"; then + printf '%s\n' "Malformed transaction marker did not fail closed." >&2 + exit 1 +fi +rm -f -- "$backup_root/.active-transaction" + +bad_backup="$game_root/Pal/Binaries/Win64/ue4ss/.palschema-backups/20000101T000000Z-1" +mkdir -p "$bad_backup/PalSchema/dlls" +ln -s /definitely-missing "$bad_backup/PalSchema/dlls/main.dll" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" \ + --rollback "$bad_backup" \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/rollback.out" 2>"$test_root/rollback.err" +rollback_status=$? +set -e +if ((rollback_status == 0)); then + printf '%s\n' "Rollback accepted an invalid staged main.dll." >&2 + exit 1 +fi +if [[ ! -f "$game_root/Pal/Binaries/Win64/ue4ss/Mods/PalSchema/dlls/main.dll" ]]; then + printf '%s\n' "Rejected rollback modified the live installation." >&2 + exit 1 +fi + +nested_link_backup="$backup_root/20000101T000000Z-5" +external_mods="$test_root/external-mods" +mkdir -p "$nested_link_backup/PalSchema/dlls" "$external_mods" +printf '%s\n' "regular-backup" \ + > "$nested_link_backup/PalSchema/dlls/main.dll" +printf '%s\n' "external-mod-sentinel" > "$external_mods/sentinel.txt" +ln -s "$external_mods" "$nested_link_backup/PalSchema/mods" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" \ + --rollback "$nested_link_backup" \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/nested-link.out" 2>"$test_root/nested-link.err" +nested_link_status=$? +set -e +if ((nested_link_status == 0)) || + ! grep -q "Backup tree contains a symlink" "$test_root/nested-link.err"; then + printf '%s\n' "Rollback accepted a nested backup symlink." >&2 + exit 1 +fi +if [[ "$(cat "$external_mods/sentinel.txt")" != "external-mod-sentinel" ]]; then + printf '%s\n' "Rejected nested symlink rollback modified external data." >&2 + exit 1 +fi + +# Starting the server from a marker-rename wrapper reproduces the old +# check-then-mv race. The atomic swap plus post-swap scan must restore the live +# installation and fail closed. +real_mv="$(command -v mv)" +cat > "$fake_bin/mv" < "$test_root/race-process.pid" +fi +exec "$real_mv" "\$@" +EOF +chmod +x "$fake_bin/mv" +pre_race_hash="$(sha256sum "$target_root/dlls/main.dll" | awk '{print $1}')" +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/deploy-proton.sh" shipping \ + --target server \ + --game-dir "$game_root" \ + >"$test_root/race.out" 2>"$test_root/race.err" +race_status=$? +set -e +if [[ -f "$test_root/race-process.pid" ]]; then + kill "$(cat "$test_root/race-process.pid")" >/dev/null 2>&1 || true +fi +if ((race_status == 0)) || + ! grep -q "started during the atomic deployment" "$test_root/race.err"; then + printf '%s\n' "Process-start race was not detected after the atomic swap." >&2 + exit 1 +fi +if [[ "$(sha256sum "$target_root/dlls/main.dll" | awk '{print $1}')" \ + != "$pre_race_hash" ]]; then + printf '%s\n' "Process-start race did not restore the previous live mod." >&2 + exit 1 +fi + +printf '%s\n' "PalSchema deploy transaction tests passed." diff --git a/scripts/tests/test-distro-matrix-output.sh b/scripts/tests/test-distro-matrix-output.sh new file mode 100755 index 0000000..4e79bd4 --- /dev/null +++ b/scripts/tests/test-distro-matrix-output.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/palschema-matrix-output-test.XXXXXX")" +trap 'rm -rf -- "$test_root"' EXIT + +fake_bin="$test_root/bin" +mkdir -p "$fake_bin" +cat > "$fake_bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + run) + if [[ "${FAKE_DOCKER_SLEEP:-0}" == "1" ]]; then + sleep 30 + fi + for index in $(seq 1 5000); do + printf 'very-noisy-container-line-%s\n' "$index" + done + exit "${FAKE_DOCKER_STATUS:-0}" + ;; + rm|pull) + exit 0 + ;; + *) + exit 0 + ;; +esac +EOF +chmod +x "$fake_bin/docker" + +PATH="$fake_bin:$PATH" \ + "$repository_root/scripts/test-distro-matrix.sh" \ + --no-pull debian >"$test_root/success.out" 2>&1 +if grep -q "very-noisy-container-line" "$test_root/success.out"; then + printf '%s\n' "Successful matrix output leaked container logs." >&2 + exit 1 +fi +if ! grep -q "PASS" "$test_root/success.out"; then + printf '%s\n' "Successful matrix output did not report a concise PASS." >&2 + exit 1 +fi + +set +e +PATH="$fake_bin:$PATH" \ +PALSCHEMA_MATRIX_TIMEOUT_SECONDS=1 \ +FAKE_DOCKER_SLEEP=1 \ + "$repository_root/scripts/test-distro-matrix.sh" \ + --no-pull debian >"$test_root/timeout.out" 2>&1 +timeout_status=$? +set -e +if ((timeout_status == 0)); then + printf '%s\n' "Timed-out matrix unexpectedly succeeded." >&2 + exit 1 +fi +if ! grep -q "Timed out after 1 seconds" "$test_root/timeout.out"; then + printf '%s\n' "Timed-out matrix did not report its hard limit." >&2 + exit 1 +fi +if (($(wc -c < "$test_root/timeout.out") > 32768)); then + printf '%s\n' "Timed-out matrix emitted excessive terminal output." >&2 + exit 1 +fi + +printf '%s\n' "PalSchema distro matrix output tests passed." diff --git a/scripts/tests/test-process-scan.sh b/scripts/tests/test-process-scan.sh new file mode 100755 index 0000000..056b357 --- /dev/null +++ b/scripts/tests/test-process-scan.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/palschema-process-scan.XXXXXX")" +trap 'rm -rf -- "$test_root"' EXIT +source "$repository_root/scripts/lib/process-scan.sh" + +mkdir -p "$test_root/100" +printf 'wine\0C:\\PalServer-Win64-Shipping-Cmd.exe\0' \ + > "$test_root/100/cmdline" +if ! palschema_target_process_status server "$test_root"; then + printf '%s\n' "Process scan did not detect a matching Win64 server." >&2 + exit 1 +fi + +rm -rf -- "$test_root/100" +mkdir -p "$test_root/200" +printf 'unrelated\0process\0' > "$test_root/200/cmdline" +set +e +palschema_target_process_status server "$test_root" +negative_status=$? +set -e +if ((negative_status != 1)); then + printf 'Expected a complete negative scan, got status %s.\n' \ + "$negative_status" >&2 + exit 1 +fi + +rm -rf -- "$test_root/200" +set +e +palschema_target_process_status server "$test_root" +empty_status=$? +palschema_target_process_status server "$test_root/missing" +missing_status=$? +set -e +if ((empty_status != 2 || missing_status != 2)); then + printf 'Expected empty and missing proc roots to fail closed, got %s and %s.\n' \ + "$empty_status" "$missing_status" >&2 + exit 1 +fi + +mkdir -p "$test_root/300" +python3 - "$test_root/300/cmdline" <<'PY' +import socket +import sys + +sock = socket.socket(socket.AF_UNIX) +sock.bind(sys.argv[1]) +sock.close() +PY +set +e +palschema_target_process_status server "$test_root" +incomplete_status=$? +set -e +if ((incomplete_status != 2)); then + printf 'Expected an incomplete scan to fail closed with status 2, got %s.\n' \ + "$incomplete_status" >&2 + exit 1 +fi + +printf '%s\n' "PalSchema process scan tests passed." diff --git a/scripts/tests/test-win64-server-process-guard.sh b/scripts/tests/test-win64-server-process-guard.sh new file mode 100755 index 0000000..d6e6024 --- /dev/null +++ b/scripts/tests/test-win64-server-process-guard.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/palschema-server-guard.XXXXXX")" +guard_pid="" +cleanup() { + if [[ -n "$guard_pid" ]]; then + kill "$guard_pid" >/dev/null 2>&1 || true + fi + rm -rf -- "$test_root" +} +trap cleanup EXIT + +project_root="$test_root/project" +game_root="$test_root/game" +fake_bin="$test_root/bin" +win64_root="$game_root/Pal/Binaries/Win64" +ue4ss_root="$win64_root/ue4ss" +mkdir -p \ + "$project_root/scripts/lib" \ + "$ue4ss_root/Mods/PalSchema/dlls" \ + "$fake_bin" +cp -- "$repository_root/scripts/test-win64-server.sh" "$project_root/scripts/" +cp -- "$repository_root/scripts/lib/process-scan.sh" "$project_root/scripts/lib/" +touch \ + "$win64_root/PalServer-Win64-Shipping-Cmd.exe" \ + "$win64_root/dwmapi.dll" \ + "$ue4ss_root/UE4SS.dll" \ + "$ue4ss_root/UE4SS-settings.ini" \ + "$ue4ss_root/MemberVariableLayout.ini" \ + "$ue4ss_root/Mods/PalSchema/dlls/main.dll" + +for command_name in ss wine wineserver; do + printf '%s\n' '#!/usr/bin/env sh' 'exit 0' > "$fake_bin/$command_name" + chmod +x "$fake_bin/$command_name" +done + +bash -c 'exec -a ./PalServer-Win64-Shipping-Cmd.exe sleep 30' & +guard_pid=$! +sleep 0.1 + +set +e +PATH="$fake_bin:$PATH" \ + "$project_root/scripts/test-win64-server.sh" \ + --game-dir "$game_root" \ + --cycles 1 \ + >"$test_root/guard.out" 2>"$test_root/guard.err" +status=$? +set -e + +if ((status == 0)) || + ! grep -q "already running" "$test_root/guard.err"; then + printf '%s\n' \ + "Server smoke harness did not reject a relative executable command line." >&2 + exit 1 +fi + +printf '%s\n' "PalSchema Win64 server process guard tests passed." diff --git a/scripts/tests/test_verify_win64_artifact.py b/scripts/tests/test_verify_win64_artifact.py new file mode 100644 index 0000000..0183251 --- /dev/null +++ b/scripts/tests/test_verify_win64_artifact.py @@ -0,0 +1,287 @@ +from __future__ import annotations + +import subprocess +import struct +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +VERIFIER = REPOSITORY_ROOT / "scripts" / "verify-win64-artifact.py" + + +def align4(content: bytes) -> bytes: + return content + b"\0" * (-len(content) % 4) + + +def version_node( + key: str, + *, + value: bytes | str = b"", + value_type: int = 0, + children: tuple[bytes, ...] = (), +) -> bytes: + encoded_key = f"{key}\0".encode("utf-16-le") + if isinstance(value, str): + encoded_value = f"{value}\0".encode("utf-16-le") + value_length = len(encoded_value) // 2 + else: + encoded_value = value + value_length = len(encoded_value) + content = align4(b"\0" * 6 + encoded_key) + content += encoded_value + content = align4(content) + content += b"".join(align4(child) for child in children) + return struct.pack(" bytes: + components = [ + int(value) + for value in (fixed_version or product_version).split(".") + ] + product_ms = (components[0] << 16) | components[1] + product_ls = (components[2] << 16) | components[3] + fixed = struct.pack( + "<13I", + 0xFEEF04BD, + 0x00010000, + product_ms, + product_ls, + product_ms, + product_ls, + 0x3F, + 0, + 0x40004, + 1, + 0, + 0, + 0, + ) + strings = [ + version_node("ProductName", value=product_name, value_type=1), + version_node("ProductVersion", value=product_version, value_type=1), + ] + if comments is not None: + strings.append(version_node("Comments", value=comments, value_type=1)) + table = version_node("040904B0", children=tuple(strings)) + string_file_info = version_node("StringFileInfo", children=(table,)) + return version_node( + "VS_VERSION_INFO", + value=fixed, + children=(string_file_info,), + ) + + +def resource_dump( + product_name: str, + product_version: str, + *, + fixed_version: str | None = None, + comments: str | None = None, +) -> str: + content = version_resource( + product_name, + product_version, + fixed_version=fixed_version, + comments=comments, + ) + content += b"\0" * (-len(content) % 4) + groups = [ + content[index : index + 4].hex().upper() + for index in range(0, len(content), 4) + ] + rows = [] + for index in range(0, len(groups), 4): + rows.append( + f" {index * 4:04X}: {' '.join(groups[index:index + 4])} |data|" + ) + return "\n".join(rows) + + +def readobj_output( + product_name: str = "PalSchema", + product_version: str = "0.6.1.0", + *, + fixed_version: str | None = None, + comments: str | None = None, +) -> str: + return f"""\ +File: fixture.dll +Format: COFF-x86-64 +ImageFileHeader {{ + Characteristics [ (0x2022) + IMAGE_FILE_DLL + ] +}} +ImageOptionalHeader {{ + Characteristics [ (0x8160) + IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE + IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA + IMAGE_DLL_CHARACTERISTICS_NX_COMPAT + ] +}} +Export {{ + Name: start_mod +}} +Export {{ + Name: uninstall_mod +}} +Import {{ + Name: UE4SS.dll +}} +Resources [ + Type: VERSIONINFO (ID 16) [ + Data ( +{resource_dump(product_name, product_version, fixed_version=fixed_version, comments=comments)} + ) + ] +] +""" + + +class VerifyWin64ArtifactTest(unittest.TestCase): + def run_verifier( + self, + output: str | None = None, + ) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory(prefix="palschema-pe-test-") as directory: + root = Path(directory) + artifact = root / "fixture.dll" + artifact.write_bytes(b"fixture") + readobj = root / "llvm-readobj" + readobj.write_text( + "#!/usr/bin/env sh\n" + "cat <<'EOF'\n" + f"{output or readobj_output()}" + "EOF\n", + encoding="utf-8", + ) + readobj.chmod(0o755) + return subprocess.run( + [ + sys.executable, + str(VERIFIER), + str(artifact), + "--llvm-readobj", + str(readobj), + ], + capture_output=True, + text=True, + check=False, + ) + + def test_accepts_project_identity_and_version(self) -> None: + completed = self.run_verifier() + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertIn('"product_name": "PalSchema"', completed.stdout) + + def test_rejects_an_unrelated_generic_ue4ss_mod(self) -> None: + completed = self.run_verifier( + readobj_output(product_name="OtherMod!", comments="PalSchema"), + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("product identity", completed.stderr) + + def test_rejects_wrong_fixed_product_version_despite_decoy_string(self) -> None: + completed = self.run_verifier( + readobj_output(fixed_version="9.9.9.9"), + ) + self.assertEqual(completed.returncode, 1) + self.assertIn("VS_FIXEDFILEINFO product version differs", completed.stderr) + + def test_rejects_each_broken_pe_contract_field(self) -> None: + valid = readobj_output() + mutations = { + "architecture": ( + valid.replace("Format: COFF-x86-64", "Format: COFF-i386"), + "not COFF x86-64", + ), + "dll flag": ( + valid.replace(" IMAGE_FILE_DLL\n", "") + + "Import {\n Name: IMAGE_FILE_DLL\n}\n", + "not marked as a DLL", + ), + "export": ( + valid.replace("Export {\n Name: uninstall_mod\n}\n", ""), + "exports differ", + ), + "UE4SS import": ( + valid.replace("Import {\n Name: UE4SS.dll\n}\n", ""), + "UE4SS.dll import is missing", + ), + "VERSIONINFO": ( + valid.replace("Type: VERSIONINFO", "Type: OTHER"), + "VERSIONINFO resource is missing", + ), + "product version": ( + readobj_output(product_version="9.9.9.9"), + "product version", + ), + } + for characteristic in ( + "IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE", + "IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA", + "IMAGE_DLL_CHARACTERISTICS_NX_COMPAT", + ): + mutations[characteristic] = ( + valid.replace(f" {characteristic}\n", "") + + f"Import {{\n Name: {characteristic}\n}}\n", + "missing DLL security characteristics", + ) + + for name, (output, expected_error) in mutations.items(): + with self.subTest(name=name): + completed = self.run_verifier(output) + self.assertEqual(completed.returncode, 1) + self.assertIn(expected_error, completed.stderr) + + def test_json_output_cannot_replace_artifact_or_follow_symlink(self) -> None: + with tempfile.TemporaryDirectory(prefix="palschema-pe-output-") as directory: + root = Path(directory) + artifact = root / "fixture.dll" + artifact.write_bytes(b"fixture") + protected = root / "protected.json" + protected.write_text("sentinel\n", encoding="utf-8") + linked_output = root / "contract.json" + linked_output.symlink_to(protected) + readobj = root / "llvm-readobj" + readobj.write_text( + "#!/usr/bin/env sh\ncat <<'EOF'\n" + f"{readobj_output()}" + "EOF\n", + encoding="utf-8", + ) + readobj.chmod(0o755) + + for output in (artifact, linked_output): + with self.subTest(output=output.name): + completed = subprocess.run( + [ + sys.executable, + str(VERIFIER), + str(artifact), + "--llvm-readobj", + str(readobj), + "--json-output", + str(output), + ], + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(completed.returncode, 1) + self.assertEqual(artifact.read_bytes(), b"fixture") + self.assertEqual(protected.read_text(encoding="utf-8"), "sentinel\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_version_consistency.py b/scripts/tests/test_version_consistency.py new file mode 100644 index 0000000..e98a8a9 --- /dev/null +++ b/scripts/tests/test_version_consistency.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + + +def palschema_version() -> str: + header = (REPOSITORY_ROOT / "version.h").read_text(encoding="utf-8") + components = [] + for name in ("MAJOR", "MINOR", "REVISION"): + match = re.search( + rf"^\s*#define\s+VERSION_{name}\s+([0-9]+)\s*$", + header, + re.MULTILINE, + ) + if match is None: + raise AssertionError(f"VERSION_{name} is missing from version.h") + components.append(match.group(1)) + return ".".join(components) + + +class VersionConsistencyTests(unittest.TestCase): + def test_packages_and_schema_ids_match_version_header(self) -> None: + version = palschema_version() + for manifest in ( + "tools/palschema-tools/package.json", + "tools/vscode-palschema/package.json", + ): + data = json.loads( + (REPOSITORY_ROOT / manifest).read_text(encoding="utf-8") + ) + self.assertEqual(data["version"], version, manifest) + + schema_root = REPOSITORY_ROOT / "assets" / "schemas" + index = json.loads( + (schema_root / "schema-index.json").read_text(encoding="utf-8") + ) + self.assertEqual(index["palSchemaVersion"], version) + for entry in index["schemas"]: + expected_id = ( + "https://okaetsu.github.io/PalSchema/" + f"schemas/{version}/{entry['file']}" + ) + self.assertEqual(entry["id"], expected_id, entry["file"]) + schema_file = schema_root / entry["file"] + if schema_file.is_file(): + schema = json.loads(schema_file.read_text(encoding="utf-8")) + self.assertEqual(schema.get("$id"), expected_id, entry["file"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/verify-win64-artifact.py b/scripts/verify-win64-artifact.py new file mode 100755 index 0000000..cb45bbb --- /dev/null +++ b/scripts/verify-win64-artifact.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 + +"""Verify and describe the stable PE contract of a PalSchema Win64 DLL.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + + +EXPECTED_EXPORTS = {"start_mod", "uninstall_mod"} +EXPECTED_PRODUCT_NAME = "PalSchema" +REQUIRED_DLL_CHARACTERISTICS = { + "IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE", + "IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA", + "IMAGE_DLL_CHARACTERISTICS_NX_COMPAT", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("artifact", type=Path, help="PalSchema.dll or main.dll") + parser.add_argument( + "--json-output", + type=Path, + help="Write the verified contract to this JSON file.", + ) + parser.add_argument( + "--llvm-readobj", + default="llvm-readobj", + help="llvm-readobj executable name or path.", + ) + parser.add_argument( + "--expected-version", + help="Expected four-component PalSchema product version (defaults to version.h).", + ) + return parser.parse_args() + + +def version_from_header() -> str: + header = Path(__file__).resolve().parent.parent / "version.h" + text = header.read_text(encoding="utf-8") + components = [] + for name in ("MAJOR", "MINOR", "REVISION", "BUILD"): + match = re.search( + rf"^\s*#define\s+VERSION_{name}\s+([0-9]+)\s*$", + text, + re.MULTILINE, + ) + if match is None: + raise RuntimeError(f"Unable to resolve VERSION_{name} from {header}.") + components.append(match.group(1)) + return ".".join(components) + + +def version_resource_bytes(output: str) -> bytes: + in_version_resource = False + in_data = False + chunks: list[bytes] = [] + for raw_line in output.splitlines(): + line = raw_line.strip() + if line.startswith("Type: VERSIONINFO"): + in_version_resource = True + continue + if in_version_resource and line == "Data (": + in_data = True + continue + if in_data and line == ")": + break + if not in_data: + continue + match = re.match( + r"^[0-9A-Fa-f]+:\s+((?:[0-9A-Fa-f]{8}\s+)+)\|", + line, + ) + if match is not None: + chunks.extend( + bytes.fromhex(group) + for group in match.group(1).split() + ) + return b"".join(chunks) + + +def align4(offset: int) -> int: + return (offset + 3) & ~3 + + +def read_utf16_z(data: bytes, offset: int, end: int) -> tuple[str, int]: + cursor = offset + while cursor + 2 <= end: + if data[cursor : cursor + 2] == b"\0\0": + return data[offset:cursor].decode("utf-16-le"), cursor + 2 + cursor += 2 + raise RuntimeError("VERSIONINFO contains an unterminated UTF-16 key.") + + +def parse_version_node( + data: bytes, + offset: int, + limit: int, +) -> tuple[dict[str, object], int]: + if offset + 6 > limit: + raise RuntimeError("VERSIONINFO node header is truncated.") + length, value_length, value_type = struct.unpack_from(" limit: + raise RuntimeError("VERSIONINFO node length is invalid.") + end = offset + length + key, key_end = read_utf16_z(data, offset + 6, end) + value_offset = align4(key_end) + value_size = value_length * 2 if value_type == 1 else value_length + value_end = value_offset + value_size + if value_end > end: + raise RuntimeError(f"VERSIONINFO value for {key!r} is truncated.") + if value_type == 1: + value: bytes | str = data[value_offset:value_end].decode( + "utf-16-le", + ).rstrip("\0") + else: + value = data[value_offset:value_end] + + children: list[dict[str, object]] = [] + cursor = align4(value_end) + while cursor + 2 <= end: + if data[cursor:end].strip(b"\0") == b"": + break + child, child_end = parse_version_node(data, cursor, end) + children.append(child) + cursor = align4(child_end) + return { + "key": key, + "value": value, + "children": children, + }, end + + +def decode_version_info( + resource: bytes, +) -> tuple[tuple[int, int, int, int], dict[str, list[str]]]: + root, _ = parse_version_node(resource, 0, len(resource)) + if root["key"] != "VS_VERSION_INFO": + raise RuntimeError("VERSIONINFO root key is invalid.") + fixed = root["value"] + if not isinstance(fixed, bytes) or len(fixed) < 52: + raise RuntimeError("VS_FIXEDFILEINFO is missing or truncated.") + fields = struct.unpack_from("<13I", fixed) + if fields[0] != 0xFEEF04BD: + raise RuntimeError("VS_FIXEDFILEINFO signature is invalid.") + product_version = ( + fields[4] >> 16, + fields[4] & 0xFFFF, + fields[5] >> 16, + fields[5] & 0xFFFF, + ) + + strings: dict[str, list[str]] = {} + + def collect(node: dict[str, object], under_string_table: bool = False) -> None: + key = node["key"] + children = node["children"] + if not isinstance(key, str) or not isinstance(children, list): + raise RuntimeError("VERSIONINFO contains an invalid node.") + is_string_table = under_string_table or key == "StringFileInfo" + value = node["value"] + if is_string_table and isinstance(value, str) and key not in { + "StringFileInfo", + }: + strings.setdefault(key, []).append(value) + for child in children: + if not isinstance(child, dict): + raise RuntimeError("VERSIONINFO contains an invalid child.") + collect(child, is_string_table) + + collect(root) + return product_version, strings + + +def parse_flag_block(output: str, parent: str, heading: str) -> set[str]: + flags: set[str] = set() + in_parent = False + in_block = False + for raw_line in output.splitlines(): + line = raw_line.strip() + if line == f"{parent} {{": + in_parent = True + continue + if in_parent and line.startswith(f"{heading} ["): + in_block = True + continue + if in_block and line == "]": + break + if in_block: + match = re.match(r"^(IMAGE_[A-Z0-9_]+)(?:\s|\(|$)", line) + if match: + flags.add(match.group(1)) + return flags + + +def inspect_artifact( + artifact: Path, + llvm_readobj: str, + expected_version: str, +) -> dict[str, object]: + executable = shutil.which(llvm_readobj) + if executable is None: + raise RuntimeError(f"Unable to find {llvm_readobj!r} on PATH.") + + completed = subprocess.run( + [ + executable, + "--file-headers", + "--coff-exports", + "--coff-imports", + "--coff-resources", + str(artifact), + ], + check=True, + capture_output=True, + text=True, + ) + output = completed.stdout + + exports: set[str] = set() + imports: set[str] = set() + block: str | None = None + for raw_line in output.splitlines(): + line = raw_line.strip() + if line == "Export {": + block = "export" + continue + if line == "Import {": + block = "import" + continue + if line == "}": + block = None + continue + if not line.startswith("Name: "): + continue + + name = line.removeprefix("Name: ").strip() + if block == "export": + exports.add(name) + elif block == "import": + imports.add(name) + + file_characteristics = parse_flag_block( + output, + "ImageFileHeader", + "Characteristics", + ) + dll_characteristics = parse_flag_block( + output, + "ImageOptionalHeader", + "Characteristics", + ) + missing_characteristics = sorted( + characteristic + for characteristic in REQUIRED_DLL_CHARACTERISTICS + if characteristic not in dll_characteristics + ) + errors: list[str] = [] + if "Format: COFF-x86-64" not in output: + errors.append("artifact is not COFF x86-64") + if "IMAGE_FILE_DLL" not in file_characteristics: + errors.append("artifact is not marked as a DLL") + if exports != EXPECTED_EXPORTS: + errors.append( + "exports differ: " + f"expected {sorted(EXPECTED_EXPORTS)}, found {sorted(exports)}" + ) + if "UE4SS.dll" not in imports: + errors.append("dynamic UE4SS.dll import is missing") + resource_bytes = version_resource_bytes(output) + if "Type: VERSIONINFO" not in output: + errors.append("VERSIONINFO resource is missing") + elif not resource_bytes: + errors.append("VERSIONINFO resource data is missing") + if resource_bytes: + try: + fixed_version, version_strings = decode_version_info(resource_bytes) + expected_components = tuple(int(value) for value in expected_version.split(".")) + if fixed_version != expected_components: + errors.append( + "VS_FIXEDFILEINFO product version differs: " + f"expected {expected_components}, found {fixed_version}" + ) + if version_strings.get("ProductName") != [EXPECTED_PRODUCT_NAME]: + errors.append( + f"VERSIONINFO product identity {EXPECTED_PRODUCT_NAME!r} is missing" + ) + if version_strings.get("ProductVersion") != [expected_version]: + errors.append( + f"VERSIONINFO product version {expected_version!r} is missing" + ) + except RuntimeError as error: + errors.append(str(error)) + if missing_characteristics: + errors.append( + "missing DLL security characteristics: " + + ", ".join(missing_characteristics) + ) + if errors: + raise RuntimeError("; ".join(errors)) + + digest = hashlib.sha256(artifact.read_bytes()).hexdigest() + return { + "schema_version": 1, + "artifact": artifact.name, + "sha256": digest, + "format": "COFF-x86-64", + "product_name": EXPECTED_PRODUCT_NAME, + "product_version": expected_version, + "exports": sorted(exports), + "imported_dlls": sorted(imports, key=str.casefold), + "dll_characteristics": sorted(REQUIRED_DLL_CHARACTERISTICS), + } + + +def main() -> int: + args = parse_args() + artifact = args.artifact.resolve() + if not artifact.is_file(): + print(f"Artifact does not exist: {artifact}", file=sys.stderr) + return 1 + + try: + expected_version = args.expected_version or version_from_header() + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+", expected_version): + raise RuntimeError( + f"Expected version must have four numeric components: {expected_version}" + ) + contract = inspect_artifact( + artifact, + args.llvm_readobj, + expected_version, + ) + except (OSError, RuntimeError, subprocess.CalledProcessError) as error: + print(f"Win64 artifact verification failed: {error}", file=sys.stderr) + return 1 + + serialized = json.dumps(contract, indent=2) + "\n" + if args.json_output: + output_path = args.json_output.absolute() + try: + if output_path.is_symlink(): + raise RuntimeError( + f"JSON output must not be a symlink: {output_path}", + ) + if output_path.exists() and os.path.samefile(output_path, artifact): + raise RuntimeError( + "JSON output must not replace the verified artifact.", + ) + if output_path.resolve(strict=False) == artifact: + raise RuntimeError( + "JSON output must not replace the verified artifact.", + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=output_path.parent, + prefix=f".{output_path.name}.", + delete=False, + ) as temporary: + temporary.write(serialized) + temporary.flush() + os.fsync(temporary.fileno()) + temporary_path = Path(temporary.name) + os.replace(temporary_path, output_path) + except (OSError, RuntimeError) as error: + print(f"Unable to write verifier JSON: {error}", file=sys.stderr) + return 1 + print(f"Verified {artifact}; wrote {output_path}") + else: + print(serialized, end="") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/Loader/PalMainLoader.cpp b/src/Loader/PalMainLoader.cpp index 293dcc4..4139b06 100644 --- a/src/Loader/PalMainLoader.cpp +++ b/src/Loader/PalMainLoader.cpp @@ -42,6 +42,8 @@ namespace Palworld { PalMainLoader::~PalMainLoader() { + m_fileWatcher.reset(); + auto expected1 = DatatableSerialize_Hook.disable(); DatatableSerialize_Hook = {}; diff --git a/src/Misc/FileWatchWrapper.cpp b/src/Misc/FileWatchWrapper.cpp index fe1a006..d3c650a 100644 --- a/src/Misc/FileWatchWrapper.cpp +++ b/src/Misc/FileWatchWrapper.cpp @@ -1,4 +1,7 @@ #include "Misc/FileWatchWrapper.h" +#include "Platform/RuntimeEnvironment.h" +#include "Utility/Logging.h" +#include "Helpers/String.hpp" #include "UE4SSProgram.hpp" namespace fs = std::filesystem; @@ -6,16 +9,37 @@ namespace fs = std::filesystem; namespace PS { FileWatchWrapper::FileWatchWrapper(const std::filesystem::path& path, const FilesystemUpdateCallback& callback) { - m_fileWatcher = std::make_unique(); + const auto useGenericWatcher = Platform::IsRunningUnderWine(); + m_fileWatcher = std::make_unique(useGenericWatcher); m_updateListener = std::make_unique(); m_updateListener->registerCallback(callback); m_fileWatchId = m_fileWatcher->addWatch(path.string(), m_updateListener.get(), true); + if (m_fileWatchId <= efsw::Errors::NoError) + { + auto error = efsw::Errors::Log::getLastErrorLog(); + PS::Log( + STR("Unable to watch PalSchema mods for changes: {}\n"), + RC::to_generic_string(error) + ); + m_fileWatcher.reset(); + return; + } + + if (useGenericWatcher) + { + PS::Log( + STR("Using the polling file watcher for auto-reload under Wine.\n") + ); + } } FileWatchWrapper::~FileWatchWrapper() { - m_fileWatcher->removeWatch(m_fileWatchId); + if (m_fileWatcher) + { + m_fileWatcher->removeWatch(m_fileWatchId); + } } void FileWatchWrapper::Watch() @@ -27,4 +51,4 @@ namespace PS { m_fileWatcher->watch(); } -} \ No newline at end of file +} diff --git a/src/SDK/PalSignatures.cpp b/src/SDK/PalSignatures.cpp index b46b4d8..b468305 100644 --- a/src/SDK/PalSignatures.cpp +++ b/src/SDK/PalSignatures.cpp @@ -1,3 +1,6 @@ +#include + +#include "Platform/RuntimeEnvironment.h" #include "SDK/PalSignatures.h" #include "Signatures.hpp" #include "SigScanner/SinglePassSigScanner.hpp" @@ -68,7 +71,26 @@ namespace Palworld { } SigContainerMap.emplace(ScanTarget::MainExe, SigContainerBox); - SinglePassScanner::start_scan(SigContainerMap); + const auto PreviousModuleSizeThreshold = SinglePassScanner::m_multithreading_module_size_threshold; + if (PS::Platform::IsRunningUnderWine()) + { + // SinglePassScanner uses std::async whenever the executable is + // larger than this threshold. Wine can block while creating that + // worker during UE4SS mod startup, before Unreal can initialize. + // Force the scanner's existing synchronous path under Wine while + // preserving parallel scanning on native Windows. + SinglePassScanner::m_multithreading_module_size_threshold = std::numeric_limits::max(); + } + try + { + SinglePassScanner::start_scan(SigContainerMap); + } + catch (...) + { + SinglePassScanner::m_multithreading_module_size_threshold = PreviousModuleSizeThreshold; + throw; + } + SinglePassScanner::m_multithreading_module_size_threshold = PreviousModuleSizeThreshold; } void* SignatureManager::GetSignature(const std::string& ClassAndFunction) @@ -81,4 +103,4 @@ namespace Palworld { return nullptr; } -} \ No newline at end of file +} diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 8fa0561..a4b0e70 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -7,6 +7,7 @@ #include "SDK/PalSignatures.h" #include "SDK/Classes/Async.h" #include "SDK/UnrealOffsets.h" +#include "Platform/Export.h" #include "../version.h" using namespace RC; @@ -42,6 +43,7 @@ class PalSchema : public RC::CppUserModBase PS::Log(STR("Preparing to pre-initialize PalSchema...\n")); MainLoader.PreInitialize(); + m_isReady = true; PS::Log(STR("{} v{} by {} loaded.\n"), ModName, ModVersion, ModAuthors); } @@ -79,6 +81,11 @@ class PalSchema : public RC::CppUserModBase auto on_ui_init() -> void override { + if (!m_isReady) + { + return; + } + register_tab(STR("Pal Schema"), [](CppUserModBase* instance) { UE4SS_ENABLE_IMGUI() @@ -105,14 +112,17 @@ class PalSchema : public RC::CppUserModBase auto on_unreal_init() -> void override { - MainLoader.Initialize(); + if (m_isReady) + { + MainLoader.Initialize(); + } } private: Palworld::PalMainLoader MainLoader; + bool m_isReady = false; }; -#define PALSCHEMA_API __declspec(dllexport) extern "C" { PALSCHEMA_API RC::CppUserModBase* start_mod() diff --git a/tools/palschema-tools/LICENSE b/tools/palschema-tools/LICENSE new file mode 100644 index 0000000..d152b5e --- /dev/null +++ b/tools/palschema-tools/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Okaetsu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/palschema-tools/README.md b/tools/palschema-tools/README.md new file mode 100644 index 0000000..1e5c8ef --- /dev/null +++ b/tools/palschema-tools/README.md @@ -0,0 +1,30 @@ +# PalSchema authoring tools + +Cross-platform JSON/JSONC validation and Language Server Protocol support for +PalSchema mod projects. + +Install a repository-built tarball: + +```bash +npm install --global ./palschema-tools-0.6.1.tgz +``` + +Initialize a workspace and validate it: + +```bash +palschema init /path/to/MyPalSchemaMod +palschema validate /path/to/MyPalSchemaMod +``` + +Start the language server for a generic editor: + +```bash +palschema-lsp --stdio +``` + +Run `palschema --help` for all commands. Full build, configuration, generated +schema, and editor instructions are in +`docs/linux/authoring-tools.md` in the PalSchema repository. + +The package contains redistributable static schemas only. It does not bundle +Palworld data, UE4SS, UEPseudo, or runtime-generated enum/raw schemas. diff --git a/tools/palschema-tools/package.json b/tools/palschema-tools/package.json new file mode 100644 index 0000000..1ca94f1 --- /dev/null +++ b/tools/palschema-tools/package.json @@ -0,0 +1,39 @@ +{ + "name": "@palschema/tools", + "version": "0.6.1", + "description": "Cross-platform PalSchema JSON/JSONC validation and editor core", + "license": "MIT", + "type": "module", + "bin": { + "palschema": "./dist/cli.js", + "palschema-lsp": "./dist/lsp-server.js" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsc -p tsconfig.json && node scripts/copy-schemas.mjs", + "schemas:index": "node scripts/update-schema-index.mjs", + "test": "tsx --test test/*.test.ts && node --test test/*.test.mjs", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "ajv": "8.20.0", + "chokidar": "4.0.3", + "jsonc-parser": "3.3.1", + "vscode-json-languageservice": "5.7.2", + "vscode-languageserver": "10.1.0", + "vscode-languageserver-textdocument": "1.0.12" + }, + "devDependencies": { + "@types/node": "24.1.0", + "tsx": "4.20.3", + "typescript": "5.8.3" + } +} diff --git a/tools/palschema-tools/scripts/copy-schemas.mjs b/tools/palschema-tools/scripts/copy-schemas.mjs new file mode 100644 index 0000000..abef423 --- /dev/null +++ b/tools/palschema-tools/scripts/copy-schemas.mjs @@ -0,0 +1,12 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { copyPublicSchemas } from "../../../scripts/copy-public-schemas.mjs"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(scriptDirectory, ".."); +const repositoryRoot = resolve(packageRoot, "../.."); +const source = resolve(repositoryRoot, "assets/schemas"); +const destination = resolve(packageRoot, "dist/schemas"); + +await copyPublicSchemas(source, destination); diff --git a/tools/palschema-tools/scripts/update-schema-index.mjs b/tools/palschema-tools/scripts/update-schema-index.mjs new file mode 100644 index 0000000..a6ba968 --- /dev/null +++ b/tools/palschema-tools/scripts/update-schema-index.mjs @@ -0,0 +1,23 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const schemaDirectory = resolve(scriptDirectory, "../../../assets/schemas"); +const indexPath = resolve(schemaDirectory, "schema-index.json"); +const index = JSON.parse(await readFile(indexPath, "utf8")); + +for (const entry of index.schemas) { + try { + const content = await readFile(resolve(schemaDirectory, entry.file)); + entry.sha256 = createHash("sha256").update(content).digest("hex"); + } catch (error) { + if (error.code !== "ENOENT" || !entry.generated) { + throw error; + } + entry.sha256 = null; + } +} + +await writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8"); diff --git a/tools/palschema-tools/src/cli.ts b/tools/palschema-tools/src/cli.ts new file mode 100644 index 0000000..9369bd8 --- /dev/null +++ b/tools/palschema-tools/src/cli.ts @@ -0,0 +1,773 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { + access, + constants, + copyFile, + lstat, + mkdtemp, + mkdir, + readdir, + realpath, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import { + dirname, + resolve, + extname, + join, + relative, + sep, +} from "node:path"; + +import chokidar from "chokidar"; + +import { findProjectConfig } from "./configuration.js"; +import { + isPathContained, + resolveContainedPath, +} from "./path-security.js"; +import { SchemaRegistry } from "./schema-registry.js"; +import type { PalSchemaDiagnostic, ValidationResult } from "./types.js"; +import { PalSchemaValidator } from "./validator.js"; + +const IGNORED_DIRECTORIES = new Set([ + ".git", + ".palschema", + ".vscode", + "build", + "dist", + "node_modules", +]); +const IGNORED_FILES = new Set(["palschema.config.json"]); + +interface CommonOptions { + schemaDirectory?: string; + format: "human" | "json"; +} + +interface ValidateOptions extends CommonOptions { + paths: string[]; + watch: boolean; + allowMissingGenerated: boolean | undefined; +} + +interface InitOptions extends CommonOptions { + destination: string; + force: boolean; +} + +function showUsage(): void { + console.log(`PalSchema authoring tools + +Usage: + palschema validate [--watch] [--allow-missing-generated|--strict-generated] + [--format human|json] + [--schema-dir PATH] [PATH...] + palschema schemas list [--format human|json] [--schema-dir PATH] + palschema schemas verify [--format human|json] [--schema-dir PATH] + palschema doctor [--format human|json] [--schema-dir PATH] + palschema print-config [--schema-dir PATH] + palschema init [--force] [--schema-dir PATH] [PATH] + +Exit codes: + 0 validation passed (warnings are allowed) + 1 one or more validation errors + 2 invalid arguments, configuration, or internal failure`); +} + +async function pathExists(path: string): Promise { + try { + await access(path, constants.F_OK); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw error; + } +} + +function containedDestination(root: string, file: string): string { + return resolveContainedPath( + root, + file, + "Schema destination escapes its workspace", + ); +} + +function parseCommonOptions(arguments_: string[]): { + options: CommonOptions; + remaining: string[]; +} { + const options: CommonOptions = { format: "human" }; + const remaining: string[] = []; + + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index]; + if (argument === "--schema-dir") { + const value = arguments_[index + 1]; + if (!value) { + throw new Error("--schema-dir requires a path."); + } + options.schemaDirectory = value; + index += 1; + } else if (argument === "--format") { + const value = arguments_[index + 1]; + if (value !== "human" && value !== "json") { + throw new Error("--format must be human or json."); + } + options.format = value; + index += 1; + } else { + remaining.push(argument ?? ""); + } + } + + return { options, remaining }; +} + +function parseValidateOptions(arguments_: string[]): ValidateOptions { + const watch = arguments_.includes("--watch"); + const allowsMissing = arguments_.includes("--allow-missing-generated"); + const requiresGenerated = arguments_.includes("--strict-generated"); + if (allowsMissing && requiresGenerated) { + throw new Error( + "--allow-missing-generated and --strict-generated are mutually exclusive.", + ); + } + const allowMissingGenerated = allowsMissing + ? true + : requiresGenerated + ? false + : undefined; + const filtered = arguments_.filter( + (argument) => + argument !== "--watch" && + argument !== "--allow-missing-generated" && + argument !== "--strict-generated", + ); + const { options, remaining } = parseCommonOptions(filtered); + return { + ...options, + paths: remaining.length > 0 ? remaining : ["."], + watch, + allowMissingGenerated, + }; +} + +function parseInitOptions(arguments_: string[]): InitOptions { + const force = arguments_.includes("--force"); + const filtered = arguments_.filter((argument) => argument !== "--force"); + const { options, remaining } = parseCommonOptions(filtered); + if (remaining.length > 1) { + throw new Error("init accepts at most one destination path."); + } + return { + ...options, + destination: resolve(remaining[0] ?? "."), + force, + }; +} + +function isJsonDocument(path: string): boolean { + const extension = extname(path).toLowerCase(); + return extension === ".json" || extension === ".jsonc"; +} + +async function discoverFiles(paths: string[]): Promise { + const discovered = new Set(); + const visitedDirectories = new Set(); + + async function visit(path: string): Promise { + const absolutePath = resolve(path); + const metadata = await lstat(absolutePath); + if (metadata.isSymbolicLink()) { + return; + } + if (metadata.isFile()) { + if ( + !IGNORED_FILES.has(absolutePath.split(/[\\/]/).at(-1) ?? "") && + isJsonDocument(absolutePath) + ) { + discovered.add(absolutePath); + } + return; + } + if (!metadata.isDirectory()) { + return; + } + + const canonicalDirectory = await realpath(absolutePath); + if (visitedDirectories.has(canonicalDirectory)) { + return; + } + visitedDirectories.add(canonicalDirectory); + + const entries = await readdir(absolutePath, { withFileTypes: true }); + await Promise.all( + entries.map(async (entry) => { + if (IGNORED_DIRECTORIES.has(entry.name)) { + return; + } + await visit(join(absolutePath, entry.name)); + }), + ); + } + + await Promise.all(paths.map(visit)); + return [...discovered].sort(); +} + +function displayFile(file: string): string { + const relativePath = relative(process.cwd(), file); + return relativePath && !relativePath.startsWith("..") ? relativePath : file; +} + +function printDiagnostic(diagnostic: PalSchemaDiagnostic): void { + console.log( + `${displayFile(diagnostic.file)}:${diagnostic.line}:${diagnostic.column} ` + + `[${diagnostic.code}] ${diagnostic.severity}: ${diagnostic.message}`, + ); +} + +function printValidationResults( + results: ValidationResult[], + format: CommonOptions["format"], + ndjson = false, +): number { + const diagnostics = results.flatMap((result) => result.diagnostics); + if (format === "json") { + const report = { + ...(ndjson ? { event: "validation" } : {}), + files: results.length, + errors: diagnostics.filter((item) => item.severity === "error").length, + warnings: diagnostics.filter((item) => item.severity === "warning") + .length, + diagnostics, + }; + console.log(JSON.stringify(report, null, ndjson ? undefined : 2)); + } else { + diagnostics.forEach(printDiagnostic); + const errors = diagnostics.filter((item) => item.severity === "error").length; + const warnings = diagnostics.length - errors; + console.log( + `Validated ${results.length} file(s): ${errors} error(s), ${warnings} warning(s).`, + ); + } + return diagnostics.some((item) => item.severity === "error") ? 1 : 0; +} + +async function validateFiles( + options: ValidateOptions, + files: string[], + validators: Map>, +): Promise { + return Promise.all( + files.map(async (file) => { + const projectConfig = await findProjectConfig(dirname(file)); + const schemaDirectory = + options.schemaDirectory ?? + process.env.PALSCHEMA_SCHEMA_DIR ?? + projectConfig?.schemaDirectory; + const allowMissingGenerated = + options.allowMissingGenerated ?? + projectConfig?.allowMissingGenerated ?? + false; + const key = JSON.stringify([ + schemaDirectory ? resolve(schemaDirectory) : null, + allowMissingGenerated, + ]); + let validator = validators.get(key); + if (!validator) { + validator = PalSchemaValidator.create(schemaDirectory, { + allowMissingGenerated, + }); + validators.set(key, validator); + } + return (await validator).validateFile(file); + }), + ); +} + +async function validateOnce( + options: ValidateOptions, + validators: Map>, +): Promise { + return validateFiles( + options, + await discoverFiles(options.paths), + validators, + ); +} + +async function validateCommand(arguments_: string[]): Promise { + const options = parseValidateOptions(arguments_); + const validators = new Map>(); + const resultCache = new Map( + (await validateOnce(options, validators)).map((result) => [ + result.file, + result, + ]), + ); + const ndjson = options.watch && options.format === "json"; + let exitCode = printValidationResults( + [...resultCache.values()], + options.format, + ndjson, + ); + if (!options.watch) { + return exitCode; + } + + const watcher = chokidar.watch(options.paths, { + followSymlinks: false, + ignored: (path, metadata) => + metadata?.isDirectory() === true && + IGNORED_DIRECTORIES.has(path.split(/[\\/]/).at(-1) ?? ""), + ignoreInitial: true, + }); + let pending: NodeJS.Timeout | undefined; + let validationActive = false; + let validationRequested = false; + let activeValidation: Promise | undefined; + let fullValidationRequested = false; + const pendingFiles = new Map(); + + const runQueuedValidations = async (): Promise => { + if (validationActive) { + validationRequested = true; + return; + } + validationActive = true; + try { + do { + validationRequested = false; + const validateEverything = fullValidationRequested; + fullValidationRequested = false; + const changes = new Map(pendingFiles); + pendingFiles.clear(); + try { + if (validateEverything) { + resultCache.clear(); + for (const result of await validateOnce(options, validators)) { + resultCache.set(result.file, result); + } + } else { + const filesToValidate = [...changes] + .filter(([, action]) => action === "validate") + .map(([file]) => file); + for (const [file, action] of changes) { + if (action === "delete") { + resultCache.delete(file); + } + } + for (const result of await validateFiles( + options, + filesToValidate, + validators, + )) { + resultCache.set(result.file, result); + } + } + exitCode = printValidationResults( + [...resultCache.values()].sort((left, right) => + left.file.localeCompare(right.file), + ), + options.format, + options.format === "json", + ); + process.exitCode = exitCode; + } catch (error) { + if (options.format === "json") { + console.log( + JSON.stringify({ + event: "error", + message: error instanceof Error ? error.message : String(error), + }), + ); + } else { + console.error(error); + } + exitCode = 2; + process.exitCode = 2; + } + } while (validationRequested); + } finally { + validationActive = false; + } + }; + + const rerun = (path: string, action: "validate" | "delete"): void => { + const absolutePath = resolve(path); + const name = absolutePath.split(/[\\/]/).at(-1) ?? ""; + if (name === "palschema.config.json" || !isJsonDocument(absolutePath)) { + fullValidationRequested = true; + } else if (!IGNORED_FILES.has(name)) { + pendingFiles.set(absolutePath, action); + } + if (pending) { + clearTimeout(pending); + } + pending = setTimeout(() => { + validationRequested = true; + if (!validationActive) { + activeValidation = runQueuedValidations(); + } + }, 100); + }; + watcher + .on("add", (path) => rerun(path, "validate")) + .on("change", (path) => rerun(path, "validate")) + .on("unlink", (path) => rerun(path, "delete")); + await new Promise((resolveReady, rejectReady) => { + watcher.once("ready", resolveReady); + watcher.once("error", rejectReady); + }); + if (options.format === "json") { + console.log(JSON.stringify({ event: "watch-started", paths: options.paths })); + } else { + console.log("Watching for PalSchema JSON/JSONC changes. Press Ctrl+C to stop."); + } + await new Promise((resolvePromise) => { + process.once("SIGINT", () => resolvePromise()); + process.once("SIGTERM", () => resolvePromise()); + }); + if (pending) { + clearTimeout(pending); + } + if (activeValidation) { + await activeValidation; + } + await watcher.close(); + return exitCode; +} + +async function schemasCommand(arguments_: string[]): Promise { + const action = arguments_[0]; + const { options, remaining } = parseCommonOptions(arguments_.slice(1)); + if (remaining.length > 0) { + throw new Error(`Unknown schemas option: ${remaining[0]}`); + } + const projectConfig = await findProjectConfig(); + const registry = await SchemaRegistry.load( + options.schemaDirectory ?? + process.env.PALSCHEMA_SCHEMA_DIR ?? + projectConfig?.schemaDirectory, + { verifyStatic: action !== "verify" }, + ); + + if (action === "list") { + if (options.format === "json") { + console.log(JSON.stringify(registry.index, null, 2)); + } else { + for (const entry of registry.index.schemas) { + console.log( + `${entry.file}\t${entry.generated ? "generated" : "static"}\t` + + `${entry.folder ?? "dependency-only"}`, + ); + } + } + return 0; + } + + if (action === "verify") { + const results = await registry.verify(); + if (options.format === "json") { + console.log(JSON.stringify(results, null, 2)); + } else { + for (const result of results) { + console.log(`${result.status}\t${result.entry.file}`); + } + } + return results.some( + (result) => + result.status === "missing" || result.status === "checksum-mismatch", + ) + ? 1 + : 0; + } + + throw new Error("schemas requires either list or verify."); +} + +async function doctorCommand(arguments_: string[]): Promise { + const { options, remaining } = parseCommonOptions(arguments_); + if (remaining.length > 0) { + throw new Error(`Unknown doctor option: ${remaining[0]}`); + } + const projectConfig = await findProjectConfig(); + const registry = await SchemaRegistry.load( + options.schemaDirectory ?? + process.env.PALSCHEMA_SCHEMA_DIR ?? + projectConfig?.schemaDirectory, + { verifyStatic: false }, + ); + const verification = await registry.verify(); + const report = { + node: process.version, + platform: process.platform, + architecture: process.arch, + schemaDirectory: registry.schemaDirectory, + palSchemaVersion: registry.index.palSchemaVersion, + schemas: verification.map((result) => ({ + file: result.entry.file, + generated: result.entry.generated, + status: result.status, + })), + }; + if (options.format === "json") { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(`Node: ${report.node} (${report.platform}/${report.architecture})`); + console.log(`Schema directory: ${report.schemaDirectory}`); + report.schemas.forEach((schema) => + console.log(`${schema.status}\t${schema.file}`), + ); + } + return verification.some( + (result) => + result.status === "missing" || result.status === "checksum-mismatch", + ) + ? 1 + : 0; +} + +async function printConfigCommand(arguments_: string[]): Promise { + const { options, remaining } = parseCommonOptions(arguments_); + if (remaining.length > 0) { + throw new Error(`Unknown print-config option: ${remaining[0]}`); + } + const projectConfig = await findProjectConfig(); + const registry = await SchemaRegistry.load( + options.schemaDirectory ?? + process.env.PALSCHEMA_SCHEMA_DIR ?? + projectConfig?.schemaDirectory, + ); + console.log( + JSON.stringify( + { + schemaDirectory: registry.schemaDirectory, + palSchemaVersion: registry.index.palSchemaVersion, + formatVersion: registry.index.formatVersion, + projectConfig: projectConfig?.file ?? null, + }, + null, + 2, + ), + ); + return 0; +} + +function editorSchemaMappings(registry: SchemaRegistry): Array<{ + fileMatch: string[]; + url: string; +}> { + return registry.index.schemas + .filter( + (entry) => + entry.folder !== null && existsSync(registry.pathFor(entry)), + ) + .map((entry) => ({ + fileMatch: entry.patterns, + url: `./.palschema/schemas/${entry.file}`, + })); +} + +async function initCommand(arguments_: string[]): Promise { + const options = parseInitOptions(arguments_); + const registry = await SchemaRegistry.load(options.schemaDirectory); + await mkdir(options.destination, { recursive: true }); + options.destination = await realpath(options.destination); + const schemaDestination = join( + options.destination, + ".palschema", + "schemas", + ); + const schemaParent = dirname(schemaDestination); + const settingsPath = join(options.destination, ".vscode", "settings.json"); + const configPath = join(options.destination, "palschema.config.json"); + const managedTargets = [settingsPath, configPath]; + + const assertNoManagedSymlink = async (path: string): Promise => { + if (!isPathContained(options.destination, path)) { + throw new Error(`Managed init path escapes its workspace: ${path}`); + } + const relativePath = relative(options.destination, path); + let candidate = options.destination; + for (const segment of relativePath.split(sep).filter(Boolean)) { + candidate = join(candidate, segment); + try { + if ((await lstat(candidate)).isSymbolicLink()) { + throw new Error( + `Refusing to initialize through a symlink: ${candidate}`, + ); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return; + } + throw error; + } + } + }; + await assertNoManagedSymlink(join(options.destination, ".palschema")); + await assertNoManagedSymlink(schemaDestination); + await assertNoManagedSymlink(join(options.destination, ".vscode")); + for (const target of managedTargets) { + await assertNoManagedSymlink(target); + } + + if (!options.force) { + const conflicts = []; + for (const target of [...managedTargets, schemaDestination]) { + if (await pathExists(target)) { + conflicts.push(target); + } + } + if (conflicts.length > 0) { + throw new Error( + `Refusing to overwrite existing workspace files:\n${conflicts.join("\n")}\n` + + "Re-run with --force after reviewing them.", + ); + } + } + + await mkdir(schemaParent, { recursive: true }); + await mkdir(join(options.destination, ".vscode"), { recursive: true }); + + const assertRegularSchemaSource = async ( + source: string, + label: string, + ): Promise => { + const metadata = await lstat(source); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error(`${label} must be a regular non-symlink file: ${source}`); + } + const canonical = await realpath(source); + if (!isPathContained(registry.schemaDirectory, canonical)) { + throw new Error(`${label} escapes its schema pack: ${source}`); + } + }; + + const stagedSchemas = await mkdtemp(join(schemaParent, ".schemas-stage-")); + const copiedSchemas: string[] = ["schema-index.json"]; + const missingGenerated: string[] = []; + try { + const sourceIndex = join(registry.schemaDirectory, "schema-index.json"); + await assertRegularSchemaSource(sourceIndex, "Schema index"); + await copyFile(sourceIndex, join(stagedSchemas, "schema-index.json")); + + for (const entry of registry.index.schemas) { + const source = registry.pathFor(entry); + const destination = containedDestination(stagedSchemas, entry.file); + if (!(await pathExists(source))) { + if (entry.generated) { + missingGenerated.push(entry.file); + continue; + } + throw new Error(`Required schema is missing: ${source}`); + } + await assertRegularSchemaSource(source, `Schema ${entry.file}`); + await copyFile(source, destination); + copiedSchemas.push(entry.file); + for (const supportFile of await registry.supportFilesFor(entry)) { + const supportSource = registry.pathForRelative(supportFile); + const supportDestination = containedDestination( + stagedSchemas, + supportFile, + ); + await mkdir(dirname(supportDestination), { recursive: true }); + await copyFile(supportSource, supportDestination); + copiedSchemas.push(supportFile); + } + } + + const backupSchemas = `${schemaDestination}.backup-${process.pid}-${Date.now()}`; + const hadExistingSchemas = await pathExists(schemaDestination); + if (hadExistingSchemas) { + await rename(schemaDestination, backupSchemas); + } + try { + await rename(stagedSchemas, schemaDestination); + } catch (error) { + if (hadExistingSchemas && !(await pathExists(schemaDestination))) { + await rename(backupSchemas, schemaDestination); + } + throw error; + } + if (hadExistingSchemas) { + await rm(backupSchemas, { recursive: true, force: true }); + } + } catch (error) { + await rm(stagedSchemas, { recursive: true, force: true }); + throw error; + } + + const settings = { + "json.schemas": editorSchemaMappings(registry), + "palschema.schemaDirectory": ".palschema/schemas", + "palschema.allowMissingGenerated": true, + }; + const config = { + schemaDirectory: ".palschema/schemas", + allowMissingGenerated: true, + }; + await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); + await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); + + const report = { + destination: options.destination, + schemaDirectory: schemaDestination, + copiedSchemas, + missingGenerated, + settings: settingsPath, + config: configPath, + }; + if (options.format === "json") { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(`Initialized PalSchema authoring in ${options.destination}`); + console.log(`Copied ${copiedSchemas.length} schema file(s).`); + if (missingGenerated.length > 0) { + console.log( + `Runtime-generated schemas not present yet: ${missingGenerated.join(", ")}`, + ); + } + } + return 0; +} + +async function main(): Promise { + const [command, ...arguments_] = process.argv.slice(2); + if (!command || command === "-h" || command === "--help" || command === "help") { + showUsage(); + return 0; + } + if (command === "validate") { + return validateCommand(arguments_); + } + if (command === "schemas") { + return schemasCommand(arguments_); + } + if (command === "doctor") { + return doctorCommand(arguments_); + } + if (command === "print-config") { + return printConfigCommand(arguments_); + } + if (command === "init") { + return initCommand(arguments_); + } + throw new Error(`Unknown command: ${command}`); +} + +try { + process.exitCode = await main(); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 2; +} diff --git a/tools/palschema-tools/src/configuration.ts b/tools/palschema-tools/src/configuration.ts new file mode 100644 index 0000000..569e668 --- /dev/null +++ b/tools/palschema-tools/src/configuration.ts @@ -0,0 +1,76 @@ +import { readFile } from "node:fs/promises"; +import { parse as parsePath, resolve } from "node:path"; + +import { parse, type ParseError, printParseErrorCode } from "jsonc-parser"; + +export interface PalSchemaProjectConfig { + schemaDirectory?: string; + allowMissingGenerated?: boolean; +} + +export interface ResolvedProjectConfig extends PalSchemaProjectConfig { + file: string; +} + +function assertProjectConfig( + value: unknown, + file: string, +): asserts value is PalSchemaProjectConfig { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`${file} must contain a JSON object.`); + } + if ( + "schemaDirectory" in value && + typeof value.schemaDirectory !== "string" + ) { + throw new Error(`${file}: schemaDirectory must be a string.`); + } + if ( + "allowMissingGenerated" in value && + typeof value.allowMissingGenerated !== "boolean" + ) { + throw new Error(`${file}: allowMissingGenerated must be a boolean.`); + } +} + +export async function findProjectConfig( + startDirectory = process.cwd(), +): Promise { + let directory = resolve(startDirectory); + for (;;) { + const file = resolve(directory, "palschema.config.json"); + try { + const text = await readFile(file, "utf8"); + const errors: ParseError[] = []; + const value = parse(text, errors, { + allowTrailingComma: true, + disallowComments: false, + }) as unknown; + const firstError = errors[0]; + if (firstError) { + throw new Error( + `${file}: ${printParseErrorCode(firstError.error)}`, + ); + } + assertProjectConfig(value, file); + const result: ResolvedProjectConfig = { file }; + if (value.schemaDirectory !== undefined) { + result.schemaDirectory = resolve(directory, value.schemaDirectory); + } + if (value.allowMissingGenerated !== undefined) { + result.allowMissingGenerated = value.allowMissingGenerated; + } + return result; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + + const parent = parsePath(directory).dir; + if (parent === directory) { + return null; + } + directory = parent; + } +} diff --git a/tools/palschema-tools/src/index.ts b/tools/palschema-tools/src/index.ts new file mode 100644 index 0000000..f2e0a52 --- /dev/null +++ b/tools/palschema-tools/src/index.ts @@ -0,0 +1,16 @@ +export { findProjectConfig } from "./configuration.js"; +export { parseJsoncDocument } from "./jsonc-document.js"; +export { SchemaRegistry } from "./schema-registry.js"; +export { PalSchemaValidator } from "./validator.js"; +export type { + DiagnosticSeverity, + PalSchemaDiagnostic, + SchemaIndex, + SchemaIndexEntry, + SchemaVerification, + ValidationResult, +} from "./types.js"; +export type { + PalSchemaProjectConfig, + ResolvedProjectConfig, +} from "./configuration.js"; diff --git a/tools/palschema-tools/src/jsonc-document.ts b/tools/palschema-tools/src/jsonc-document.ts new file mode 100644 index 0000000..c6d6535 --- /dev/null +++ b/tools/palschema-tools/src/jsonc-document.ts @@ -0,0 +1,33 @@ +import { + parse, + parseTree, + printParseErrorCode, + type ParseError, +} from "jsonc-parser"; +import { extname } from "node:path"; + +import { createPositionMapper } from "./text-position.js"; +import type { PalSchemaDiagnostic, ParsedDocument } from "./types.js"; + +export function parseJsoncDocument(text: string, file: string): ParsedDocument { + const errors: ParseError[] = []; + const isJsonc = extname(file).toLowerCase() === ".jsonc"; + const options = { + allowEmptyContent: false, + allowTrailingComma: false, + disallowComments: !isJsonc, + }; + const data = parse(text, errors, options) as unknown; + const root = parseTree(text, [], options); + const positionAt = createPositionMapper(text); + const diagnostics: PalSchemaDiagnostic[] = errors.map((error) => ({ + ...positionAt(error.offset, error.length), + code: `PS_JSON_${printParseErrorCode(error.error).toUpperCase()}`, + severity: "error", + message: printParseErrorCode(error.error), + file, + source: "palschema", + })); + + return { data, root, diagnostics }; +} diff --git a/tools/palschema-tools/src/lsp-options.ts b/tools/palschema-tools/src/lsp-options.ts new file mode 100644 index 0000000..d8de62b --- /dev/null +++ b/tools/palschema-tools/src/lsp-options.ts @@ -0,0 +1,38 @@ +export interface PalSchemaInitializationOptions { + schemaDirectory?: string; + allowMissingGenerated?: boolean; +} + +export function parseInitializationOptions( + value: unknown, +): PalSchemaInitializationOptions { + if (value === undefined || value === null) { + return {}; + } + if (typeof value !== "object" || Array.isArray(value)) { + throw new Error("PalSchema initializationOptions must be an object."); + } + const candidate = value as Record; + if ( + candidate.schemaDirectory !== undefined && + typeof candidate.schemaDirectory !== "string" + ) { + throw new Error("initializationOptions.schemaDirectory must be a string."); + } + if ( + candidate.allowMissingGenerated !== undefined && + typeof candidate.allowMissingGenerated !== "boolean" + ) { + throw new Error( + "initializationOptions.allowMissingGenerated must be a boolean.", + ); + } + return { + ...(candidate.schemaDirectory !== undefined + ? { schemaDirectory: candidate.schemaDirectory } + : {}), + ...(candidate.allowMissingGenerated !== undefined + ? { allowMissingGenerated: candidate.allowMissingGenerated } + : {}), + }; +} diff --git a/tools/palschema-tools/src/lsp-server.ts b/tools/palschema-tools/src/lsp-server.ts new file mode 100644 index 0000000..02a0b39 --- /dev/null +++ b/tools/palschema-tools/src/lsp-server.ts @@ -0,0 +1,246 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { + getLanguageService, + type LanguageService, +} from "vscode-json-languageservice"; +import { + createConnection, + DiagnosticSeverity, + ProposedFeatures, + TextDocumentSyncKind, + type InitializeParams, + type InitializeResult, +} from "vscode-languageserver/node"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { TextDocuments } from "vscode-languageserver/node"; + +import { SchemaRegistry } from "./schema-registry.js"; +import { + parseInitializationOptions, + type PalSchemaInitializationOptions, +} from "./lsp-options.js"; +import { isPathContained } from "./path-security.js"; +import type { PalSchemaDiagnostic } from "./types.js"; +import { PalSchemaValidator } from "./validator.js"; + +const connection = createConnection(ProposedFeatures.all); +const documents = new TextDocuments(TextDocument); +const validationTimers = new Map(); +const MAX_PUBLISHED_DIAGNOSTICS = 500; + +let registry: SchemaRegistry; +let validator: PalSchemaValidator; +let jsonLanguageService: LanguageService; +let initializationOptions: PalSchemaInitializationOptions = {}; + +function fileForUri(uri: string): string { + try { + return fileURLToPath(uri); + } catch { + return uri; + } +} + +function toLspDiagnostic( + document: TextDocument, + diagnostic: PalSchemaDiagnostic, +) { + const start = document.positionAt(diagnostic.offset); + const end = document.positionAt( + Math.min(document.getText().length, diagnostic.offset + diagnostic.length), + ); + return { + range: { start, end }, + severity: + diagnostic.severity === "error" + ? DiagnosticSeverity.Error + : DiagnosticSeverity.Warning, + code: diagnostic.code, + source: diagnostic.source, + message: diagnostic.message, + }; +} + +async function configureServices(): Promise { + registry = await SchemaRegistry.load(initializationOptions.schemaDirectory); + validator = await PalSchemaValidator.create( + initializationOptions.schemaDirectory, + { + allowMissingGenerated: + initializationOptions.allowMissingGenerated ?? true, + }, + ); + jsonLanguageService = getLanguageService({ + workspaceContext: { + resolveRelativePath: (relativePath, resource) => + new URL(relativePath, resource).toString(), + }, + schemaRequestService: async (uri) => { + if (uri.startsWith("file:")) { + const path = fileURLToPath(uri); + if (!isPathContained(registry.schemaDirectory, path)) { + throw new Error(`Schema URI escapes the configured pack: ${uri}`); + } + return readFile(path, "utf8"); + } + const localPath = await registry.pathForId(uri); + if (!localPath) { + throw new Error(`Schema URI is not in the configured pack: ${uri}`); + } + return readFile(localPath, "utf8"); + }, + }); + jsonLanguageService.configure({ + allowComments: true, + schemas: registry.index.schemas + .filter((entry) => existsSync(registry.pathFor(entry))) + .map((entry) => ({ + uri: entry.id, + fileMatch: entry.patterns, + })), + }); +} + +async function validateDocument(document: TextDocument): Promise { + const version = document.version; + try { + const result = await validator.validateText( + document.getText(), + fileForUri(document.uri), + ); + const current = documents.get(document.uri); + if (!current || current.version !== version) { + return; + } + connection.sendDiagnostics({ + uri: document.uri, + diagnostics: result.diagnostics + .slice(0, MAX_PUBLISHED_DIAGNOSTICS) + .map((diagnostic) => toLspDiagnostic(document, diagnostic)), + }); + } catch (error) { + const current = documents.get(document.uri); + if (current && current.version === version) { + connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + } + connection.console.error( + error instanceof Error ? error.stack ?? error.message : String(error), + ); + } +} + +function scheduleValidation(document: TextDocument): void { + const existing = validationTimers.get(document.uri); + if (existing) { + clearTimeout(existing); + } + validationTimers.set( + document.uri, + setTimeout(() => { + validationTimers.delete(document.uri); + void validateDocument(document); + }, 100), + ); +} + +connection.onInitialize(async (params: InitializeParams): Promise => { + initializationOptions = parseInitializationOptions( + params.initializationOptions, + ); + await configureServices(); + return { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: { + resolveProvider: false, + triggerCharacters: ['"', ":"], + }, + hoverProvider: true, + documentSymbolProvider: true, + documentFormattingProvider: true, + documentRangeFormattingProvider: true, + }, + serverInfo: { + name: "PalSchema Language Server", + version: registry.index.palSchemaVersion, + }, + }; +}); + +connection.onCompletion((params) => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return null; + } + return jsonLanguageService.doComplete( + document, + params.position, + jsonLanguageService.parseJSONDocument(document), + ); +}); + +connection.onHover((params) => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return null; + } + return jsonLanguageService.doHover( + document, + params.position, + jsonLanguageService.parseJSONDocument(document), + ); +}); + +connection.onDocumentSymbol((params) => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return []; + } + return jsonLanguageService.findDocumentSymbols2( + document, + jsonLanguageService.parseJSONDocument(document), + ); +}); + +connection.onDocumentFormatting((params) => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return []; + } + return jsonLanguageService.format( + document, + undefined, + params.options, + ); +}); + +connection.onDocumentRangeFormatting((params) => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return []; + } + return jsonLanguageService.format( + document, + params.range, + params.options, + ); +}); + +documents.onDidOpen(({ document }) => void validateDocument(document)); +documents.onDidChangeContent(({ document }) => scheduleValidation(document)); +documents.onDidClose(({ document }) => { + const pending = validationTimers.get(document.uri); + if (pending) { + clearTimeout(pending); + validationTimers.delete(document.uri); + } + connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); +}); + +documents.listen(connection); +connection.listen(); diff --git a/tools/palschema-tools/src/path-security.ts b/tools/palschema-tools/src/path-security.ts new file mode 100644 index 0000000..3033872 --- /dev/null +++ b/tools/palschema-tools/src/path-security.ts @@ -0,0 +1,23 @@ +import { isAbsolute, relative, resolve, sep } from "node:path"; + +export function isPathContained(root: string, candidate: string): boolean { + const pathFromRoot = relative(root, candidate); + return ( + pathFromRoot !== "" && + pathFromRoot !== ".." && + !pathFromRoot.startsWith(`..${sep}`) && + !isAbsolute(pathFromRoot) + ); +} + +export function resolveContainedPath( + root: string, + file: string, + label: string, +): string { + const candidate = resolve(root, file); + if (!isPathContained(root, candidate)) { + throw new Error(`${label}: ${file}`); + } + return candidate; +} diff --git a/tools/palschema-tools/src/schema-registry.ts b/tools/palschema-tools/src/schema-registry.ts new file mode 100644 index 0000000..b92f24a --- /dev/null +++ b/tools/palschema-tools/src/schema-registry.ts @@ -0,0 +1,377 @@ +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import { + basename, + dirname, + isAbsolute, + relative, + resolve, + sep, +} from "node:path"; +import { fileURLToPath } from "node:url"; + +import type { + SchemaIndex, + SchemaIndexEntry, + SchemaVerification, +} from "./types.js"; +import { + isPathContained, + resolveContainedPath, +} from "./path-security.js"; + +const moduleDirectory = dirname(fileURLToPath(import.meta.url)); +const packageDirectory = resolve(moduleDirectory, ".."); +const bundledSchemaDirectory = resolve(moduleDirectory, "schemas"); +const repositorySchemaDirectory = resolve( + packageDirectory, + "../../assets/schemas", +); + +function defaultSchemaDirectory(): string { + const configured = process.env.PALSCHEMA_SCHEMA_DIR; + if (configured) { + return resolve(configured); + } + if (existsSync(resolve(bundledSchemaDirectory, "schema-index.json"))) { + return bundledSchemaDirectory; + } + return repositorySchemaDirectory; +} + +function assertSchemaIndex(value: unknown): asserts value is SchemaIndex { + if ( + typeof value !== "object" || + value === null || + !("formatVersion" in value) || + value.formatVersion !== 1 || + !("schemas" in value) || + !Array.isArray(value.schemas) || + !("palSchemaVersion" in value) || + typeof value.palSchemaVersion !== "string" || + !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value.palSchemaVersion) || + !("baseId" in value) || + typeof value.baseId !== "string" + ) { + throw new Error("Unsupported or malformed schema-index.json."); + } + + let baseId: URL; + try { + baseId = new URL(value.baseId); + } catch { + throw new Error("schema-index.json contains an invalid baseId."); + } + if ( + (baseId.protocol !== "https:" && baseId.protocol !== "http:") || + !baseId.pathname.endsWith("/") || + baseId.hash || + baseId.search + ) { + throw new Error("schema-index.json contains a non-canonical baseId."); + } + + const files = new Set(); + const ids = new Set(); + for (const candidate of value.schemas as unknown[]) { + const entry = candidate as Record; + if ( + typeof candidate !== "object" || + candidate === null || + !("id" in candidate) || + typeof candidate.id !== "string" || + !("file" in candidate) || + typeof candidate.file !== "string" || + !("folder" in candidate) || + (candidate.folder !== null && typeof candidate.folder !== "string") || + !("patterns" in candidate) || + !Array.isArray(candidate.patterns) || + !candidate.patterns.every((pattern) => typeof pattern === "string") || + !("generated" in candidate) || + typeof candidate.generated !== "boolean" || + !("dependencies" in candidate) || + !Array.isArray(candidate.dependencies) || + !candidate.dependencies.every( + (dependency) => typeof dependency === "string", + ) || + !("sha256" in candidate) || + (candidate.sha256 !== null && typeof candidate.sha256 !== "string") + ) { + throw new Error("schema-index.json contains a malformed schema entry."); + } + if (!("supportPatterns" in entry)) { + entry.supportPatterns = []; + } + if ( + !Array.isArray(entry.supportPatterns) || + !entry.supportPatterns.every( + (pattern: unknown) => typeof pattern === "string", + ) + ) { + throw new Error("schema-index.json contains invalid supportPatterns."); + } + assertSingleFileName(candidate.file, "schema file"); + let canonicalId: string; + try { + canonicalId = new URL(candidate.file, baseId).toString(); + } catch { + throw new Error( + `schema-index.json contains an invalid schema ID: ${candidate.id}`, + ); + } + if (candidate.id !== canonicalId) { + throw new Error( + `schema-index.json schema ID is not canonical for ${candidate.file}.`, + ); + } + for (const dependency of candidate.dependencies) { + assertSingleFileName(dependency, "schema dependency"); + } + for (const pattern of entry.supportPatterns as string[]) { + assertSupportPattern(pattern); + } + if (files.has(candidate.file) || ids.has(candidate.id)) { + throw new Error("schema-index.json contains duplicate files or IDs."); + } + files.add(candidate.file); + ids.add(candidate.id); + } + + for (const entry of value.schemas) { + for (const dependency of entry.dependencies) { + if (!files.has(dependency)) { + throw new Error( + `schema-index.json dependency is not declared: ${dependency}`, + ); + } + } + } +} + +function assertSingleFileName(value: string, label: string): void { + if ( + !value || + value === "." || + value === ".." || + isAbsolute(value) || + basename(value) !== value || + value.includes("/") || + value.includes("\\") + ) { + throw new Error(`Invalid ${label} path in schema-index.json: ${value}`); + } +} + +function assertSupportPattern(pattern: string): void { + const segments = pattern.split("/"); + if ( + segments.length !== 2 || + !segments[0] || + segments[0] === "." || + segments[0] === ".." || + segments[0].includes("\\") || + segments[1] !== "*.schema.json" + ) { + throw new Error( + `Unsupported schema support pattern in schema-index.json: ${pattern}`, + ); + } +} + +export class SchemaRegistry { + readonly schemaDirectory: string; + readonly index: SchemaIndex; + private readonly supportFileCache = new Map>(); + private supportPathIndex: Promise> | undefined; + + private constructor(schemaDirectory: string, index: SchemaIndex) { + this.schemaDirectory = schemaDirectory; + this.index = index; + } + + static async load( + schemaDirectory = defaultSchemaDirectory(), + options: { verifyStatic?: boolean } = {}, + ): Promise { + const absoluteDirectory = resolve(schemaDirectory); + const indexPath = resolve(absoluteDirectory, "schema-index.json"); + const index = JSON.parse(await readFile(indexPath, "utf8")) as unknown; + assertSchemaIndex(index); + const registry = new SchemaRegistry(absoluteDirectory, index); + if (options.verifyStatic ?? true) { + await registry.assertStaticIntegrity(); + } + return registry; + } + + findForDocument(file: string): SchemaIndexEntry | null { + const segments = resolve(file).split(sep); + for (let index = segments.length - 2; index >= 0; index -= 1) { + const segment = segments[index]; + const entry = this.index.schemas.find( + (candidate) => candidate.folder === segment, + ); + if (entry) { + return entry; + } + } + return null; + } + + entryForFile(file: string): SchemaIndexEntry | null { + return this.index.schemas.find((entry) => entry.file === file) ?? null; + } + + entryForId(id: string): SchemaIndexEntry | null { + return this.index.schemas.find((entry) => entry.id === id) ?? null; + } + + pathForRelative(file: string): string { + return resolveContainedPath( + this.schemaDirectory, + file, + "Schema path escapes its pack", + ); + } + + pathFor(entry: SchemaIndexEntry): string { + assertSingleFileName(entry.file, "schema file"); + return this.pathForRelative(entry.file); + } + + async readSchema(entry: SchemaIndexEntry): Promise> { + return JSON.parse(await readFile(this.pathFor(entry), "utf8")) as Record< + string, + unknown + >; + } + + async supportFilesFor(entry: SchemaIndexEntry): Promise { + const cached = this.supportFileCache.get(entry.id); + if (cached) { + return cached; + } + const pending = this.discoverSupportFiles(entry); + this.supportFileCache.set(entry.id, pending); + try { + return await pending; + } catch (error) { + this.supportFileCache.delete(entry.id); + throw error; + } + } + + private async discoverSupportFiles( + entry: SchemaIndexEntry, + ): Promise { + const files: string[] = []; + for (const pattern of entry.supportPatterns) { + assertSupportPattern(pattern); + const directoryName = pattern.split("/")[0] as string; + const directory = this.pathForRelative(directoryName); + let children; + try { + children = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + continue; + } + throw error; + } + for (const child of children) { + if (!child.name.endsWith(".schema.json")) { + continue; + } + const relativeFile = `${directoryName}/${child.name}`; + const path = this.pathForRelative(relativeFile); + const metadata = await lstat(path); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error(`Schema support file must be a regular file: ${path}`); + } + const canonical = await realpath(path); + if (!isPathContained(this.schemaDirectory, canonical)) { + throw new Error(`Schema support file escapes its pack: ${path}`); + } + files.push(relativeFile); + } + } + return [...new Set(files)].sort(); + } + + canonicalIdForSupport(entry: SchemaIndexEntry, file: string): string { + return new URL(file, entry.id).toString(); + } + + async pathForId(id: string): Promise { + const normalized = new URL(id); + normalized.hash = ""; + const entry = this.entryForId(normalized.toString()); + if (entry) { + return this.pathFor(entry); + } + this.supportPathIndex ??= this.buildSupportPathIndex(); + const supportPath = (await this.supportPathIndex).get( + normalized.toString(), + ); + return supportPath ?? null; + } + + private async buildSupportPathIndex(): Promise> { + const paths = new Map(); + for (const candidate of this.index.schemas) { + for (const supportFile of await this.supportFilesFor(candidate)) { + paths.set( + this.canonicalIdForSupport(candidate, supportFile), + this.pathForRelative(supportFile), + ); + } + } + return paths; + } + + async assertStaticIntegrity(): Promise { + const failures = (await this.verify()).filter( + (result) => + !result.entry.generated && + (result.status === "missing" || + result.status === "checksum-mismatch"), + ); + if (failures.length > 0) { + throw new Error( + `Static schema integrity check failed: ${failures + .map((failure) => `${failure.entry.file} (${failure.status})`) + .join(", ")}`, + ); + } + } + + async verify(): Promise { + return Promise.all( + this.index.schemas.map(async (entry): Promise => { + const path = this.pathFor(entry); + if (!existsSync(path)) { + return { + entry, + status: entry.generated ? "missing-generated" : "missing", + }; + } + + const content = await readFile(path); + const actualSha256 = createHash("sha256").update(content).digest("hex"); + if (entry.generated && entry.sha256 === null) { + return { entry, status: "present-generated", actualSha256 }; + } + if (entry.sha256 !== actualSha256) { + return { entry, status: "checksum-mismatch", actualSha256 }; + } + return { entry, status: "verified", actualSha256 }; + }), + ); + } + + displayPath(path: string): string { + const relativePath = relative(process.cwd(), path); + return relativePath && !relativePath.startsWith("..") ? relativePath : path; + } +} diff --git a/tools/palschema-tools/src/text-position.ts b/tools/palschema-tools/src/text-position.ts new file mode 100644 index 0000000..5f48f09 --- /dev/null +++ b/tools/palschema-tools/src/text-position.ts @@ -0,0 +1,34 @@ +export type PositionMapper = ( + offset: number, + length?: number, +) => { line: number; column: number; offset: number; length: number }; + +export function createPositionMapper(text: string): PositionMapper { + const lineStarts = [0]; + for (let index = 0; index < text.length; index += 1) { + if (text.charCodeAt(index) === 10) { + lineStarts.push(index + 1); + } + } + + return (offset: number, length = 1) => { + const boundedOffset = Math.max(0, Math.min(offset, text.length)); + let low = 0; + let high = lineStarts.length; + while (low + 1 < high) { + const middle = Math.floor((low + high) / 2); + if ((lineStarts[middle] ?? 0) <= boundedOffset) { + low = middle; + } else { + high = middle; + } + } + const lineStart = lineStarts[low] ?? 0; + return { + line: low + 1, + column: boundedOffset - lineStart + 1, + offset: boundedOffset, + length: Math.max(1, length), + }; + }; +} diff --git a/tools/palschema-tools/src/types.ts b/tools/palschema-tools/src/types.ts new file mode 100644 index 0000000..3f195c8 --- /dev/null +++ b/tools/palschema-tools/src/types.ts @@ -0,0 +1,67 @@ +import type { ErrorObject } from "ajv"; + +export type DiagnosticSeverity = "error" | "warning"; + +export interface SourcePosition { + line: number; + column: number; + offset: number; + length: number; +} + +export interface PalSchemaDiagnostic extends SourcePosition { + code: string; + severity: DiagnosticSeverity; + message: string; + file: string; + source: "palschema"; + schemaPath?: string; + instancePath?: string; +} + +export interface SchemaIndexEntry { + id: string; + file: string; + folder: string | null; + patterns: string[]; + generated: boolean; + dependencies: string[]; + supportPatterns: string[]; + sha256: string | null; +} + +export interface SchemaIndex { + formatVersion: number; + palSchemaVersion: string; + baseId: string; + schemas: SchemaIndexEntry[]; +} + +export interface SchemaVerification { + entry: SchemaIndexEntry; + status: + | "verified" + | "present-generated" + | "missing-generated" + | "missing" + | "checksum-mismatch"; + actualSha256?: string; +} + +export interface ParsedDocument { + data: unknown; + root: import("jsonc-parser").Node | undefined; + diagnostics: PalSchemaDiagnostic[]; +} + +export interface ValidationResult { + file: string; + schema: SchemaIndexEntry | null; + diagnostics: PalSchemaDiagnostic[]; +} + +export interface AjvDiagnosticContext { + error: ErrorObject; + root: import("jsonc-parser").Node | undefined; + file: string; +} diff --git a/tools/palschema-tools/src/validator.ts b/tools/palschema-tools/src/validator.ts new file mode 100644 index 0000000..eb2c5ec --- /dev/null +++ b/tools/palschema-tools/src/validator.ts @@ -0,0 +1,279 @@ +import { readFile } from "node:fs/promises"; +import { basename } from "node:path"; + +import { + Ajv, + type ErrorObject, + type ValidateFunction, +} from "ajv"; +import { findNodeAtLocation, type Node as JsonNode } from "jsonc-parser"; + +import { parseJsoncDocument } from "./jsonc-document.js"; +import { SchemaRegistry } from "./schema-registry.js"; +import { + createPositionMapper, + type PositionMapper, +} from "./text-position.js"; +import type { + AjvDiagnosticContext, + PalSchemaDiagnostic, + SchemaIndexEntry, + ValidationResult, +} from "./types.js"; + +function decodeJsonPointer(pointer: string): Array { + if (!pointer) { + return []; + } + return pointer + .slice(1) + .split("/") + .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")) + .map((segment) => (/^(0|[1-9][0-9]*)$/.test(segment) ? Number(segment) : segment)); +} + +function nodeForError( + root: JsonNode | undefined, + error: ErrorObject, +): JsonNode | undefined { + if (!root) { + return undefined; + } + const path = decodeJsonPointer(error.instancePath); + const node = findNodeAtLocation(root, path); + if (error.keyword === "required" && node) { + return node; + } + return node ?? root; +} + +function ajvDiagnostic( + context: AjvDiagnosticContext, + positionAt: PositionMapper, +): PalSchemaDiagnostic { + const { error, root, file } = context; + const node = nodeForError(root, error); + const location = positionAt(node?.offset ?? 0, node?.length ?? 1); + let message = error.message ?? "Schema validation failed."; + if (error.keyword === "required") { + const missingProperty = String(error.params.missingProperty); + message = `Missing required property ${JSON.stringify(missingProperty)}.`; + } + + return { + ...location, + code: `PS_SCHEMA_${error.keyword.toUpperCase()}`, + severity: "error", + message, + file, + source: "palschema", + schemaPath: error.schemaPath, + instancePath: error.instancePath, + }; +} + +function referencedEnumDefinitions(schema: unknown): string[] { + const serialized = JSON.stringify(schema); + const definitions = new Set(); + const expression = /enums\.schema\.json#\/definitions\/([^"\\]+)/g; + for (const match of serialized.matchAll(expression)) { + const definition = match[1]; + if (definition) { + definitions.add(definition); + } + } + return [...definitions].sort(); +} + +export class PalSchemaValidator { + readonly registry: SchemaRegistry; + private readonly validators = new Map(); + private readonly compilations = new Map>(); + private readonly fallbackEnumEntries = new Set(); + private readonly allowMissingGenerated: boolean; + + private constructor(registry: SchemaRegistry, allowMissingGenerated: boolean) { + this.registry = registry; + this.allowMissingGenerated = allowMissingGenerated; + } + + static async create( + schemaDirectory?: string, + options: { allowMissingGenerated?: boolean } = {}, + ): Promise { + return new PalSchemaValidator( + await SchemaRegistry.load(schemaDirectory), + options.allowMissingGenerated ?? false, + ); + } + + private async compile(entry: SchemaIndexEntry): Promise { + const existing = this.validators.get(entry.id); + if (existing) { + return existing; + } + const pending = this.compilations.get(entry.id); + if (pending) { + return pending; + } + const compilation = this.compileSchema(entry); + this.compilations.set(entry.id, compilation); + try { + return await compilation; + } finally { + if (this.compilations.get(entry.id) === compilation) { + this.compilations.delete(entry.id); + } + } + } + + private async compileSchema( + entry: SchemaIndexEntry, + ): Promise { + const schema = await this.registry.readSchema(entry); + schema.$id ??= entry.id; + const ajv = new Ajv({ + allErrors: true, + strict: false, + validateFormats: false, + }); + + for (const dependencyFile of entry.dependencies) { + const dependency = this.registry.entryForFile(dependencyFile); + if (!dependency) { + throw new Error( + `schema-index.json does not declare ${dependencyFile}.`, + ); + } + try { + const dependencySchema = await this.registry.readSchema(dependency); + dependencySchema.$id ??= dependency.id; + ajv.addSchema(dependencySchema, dependency.id); + } catch (error) { + if ( + (error as NodeJS.ErrnoException).code !== "ENOENT" || + dependencyFile !== "enums.schema.json" + ) { + throw error; + } + this.fallbackEnumEntries.add(entry.id); + const definitions = Object.fromEntries( + referencedEnumDefinitions(schema).map((name) => [name, {}]), + ); + ajv.addSchema( + { + $schema: "http://json-schema.org/draft-07/schema#", + $id: dependency.id, + definitions, + }, + dependency.id, + ); + } + } + + for (const supportFile of await this.registry.supportFilesFor(entry)) { + const supportSchema = JSON.parse( + await readFile(this.registry.pathForRelative(supportFile), "utf8"), + ) as Record; + const canonicalId = this.registry.canonicalIdForSupport( + entry, + supportFile, + ); + supportSchema.$id ??= canonicalId; + ajv.addSchema(supportSchema, canonicalId); + } + + const validate = ajv.compile(schema); + this.validators.set(entry.id, validate); + return validate; + } + + async validateText(text: string, file: string): Promise { + const parsed = parseJsoncDocument(text, file); + const positionAt = createPositionMapper(text); + const entry = this.registry.findForDocument(file); + if (parsed.diagnostics.length > 0) { + return { file, schema: entry, diagnostics: parsed.diagnostics }; + } + if (!entry) { + return { + file, + schema: null, + diagnostics: [ + { + ...positionAt(0), + code: "PS_SCHEMA_UNASSOCIATED", + severity: "warning", + message: + "No PalSchema schema is associated with this folder; JSON/JSONC syntax was checked.", + file, + source: "palschema", + }, + ], + }; + } + + if (entry.generated) { + try { + await this.registry.readSchema(entry); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return { + file, + schema: entry, + diagnostics: [ + { + ...positionAt(0), + code: "PS_SCHEMA_GENERATED_MISSING", + severity: this.allowMissingGenerated ? "warning" : "error", + message: + `${entry.file} is generated by PalSchema at runtime. ` + + "Generate or copy the current schema pack, then set PALSCHEMA_SCHEMA_DIR.", + file, + source: "palschema", + }, + ], + }; + } + throw error; + } + } + + const validate = await this.compile(entry); + const valid = validate(parsed.data); + const diagnostics = (validate.errors ?? []).map((error) => + ajvDiagnostic({ error, root: parsed.root, file }, positionAt), + ); + if (this.fallbackEnumEntries.has(entry.id)) { + diagnostics.push({ + ...positionAt(0), + code: "PS_SCHEMA_ENUMS_MISSING", + severity: this.allowMissingGenerated ? "warning" : "error", + message: + "enums.schema.json is missing; enum values were not constrained during this structural validation.", + file, + source: "palschema", + }); + } + if (!valid && diagnostics.length === 0) { + diagnostics.push({ + ...positionAt(0), + code: "PS_SCHEMA_INVALID", + severity: "error", + message: "Schema validation failed without a detailed Ajv error.", + file, + source: "palschema", + }); + } + + return { file, schema: entry, diagnostics }; + } + + async validateFile(file: string): Promise { + return this.validateText(await readFile(file, "utf8"), file); + } + + describeSchema(result: ValidationResult): string { + return result.schema?.file ?? `syntax-only (${basename(result.file)})`; + } +} diff --git a/tools/palschema-tools/test/cli-smoke.test.mjs b/tools/palschema-tools/test/cli-smoke.test.mjs new file mode 100644 index 0000000..da65c51 --- /dev/null +++ b/tools/palschema-tools/test/cli-smoke.test.mjs @@ -0,0 +1,331 @@ +import assert from "node:assert/strict"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(testDirectory, ".."); +const repositoryRoot = resolve(packageRoot, "../.."); +const cliSource = resolve(packageRoot, "src/cli.ts"); +const tsxImport = import.meta.resolve("tsx"); + +function runCli(arguments_, cwd) { + return spawnSync( + process.execPath, + ["--import", tsxImport, cliSource, ...arguments_], + { + cwd, + encoding: "utf8", + }, + ); +} + +test("init creates a workspace that validate can check without support-file noise", () => { + const workspace = mkdtempSync(resolve(tmpdir(), "palschema-cli-")); + try { + const initialization = runCli(["init", "."], workspace); + assert.equal(initialization.status, 0, initialization.stderr); + + const itemsDirectory = resolve(workspace, "items"); + mkdirSync(itemsDirectory); + writeFileSync( + resolve(itemsDirectory, "example.jsonc"), + '{\n "Example": {\n "Type": "Generic",\n "IconTexture": "/Game/Test/T_Test.T_Test",\n "TypeA": "Any",\n "TypeB": "Any",\n "Rank": 0,\n "Rarity": 0,\n "MaxStackCount": 1\n }\n}\n', + ); + + const validation = runCli(["validate", "."], workspace); + assert.equal(validation.status, 0, validation.stderr); + assert.match(validation.stdout, /PS_SCHEMA_ENUMS_MISSING.*warning/); + assert.match(validation.stdout, /Validated 1 file\(s\): 0 error\(s\), 1 warning\(s\)\./); + + const validationFromOutside = runCli( + ["validate", workspace], + packageRoot, + ); + assert.equal(validationFromOutside.status, 0, validationFromOutside.stderr); + assert.match( + validationFromOutside.stdout, + /Validated 1 file\(s\): 0 error\(s\), 1 warning\(s\)\./, + ); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +}); + +test("validation resolves configuration from each explicit target", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-config-target-")); + try { + const caller = resolve(temporaryRoot, "caller"); + const target = resolve(temporaryRoot, "target"); + mkdirSync(caller); + mkdirSync(resolve(target, "items"), { recursive: true }); + writeFileSync( + resolve(caller, "palschema.config.json"), + '{"schemaDirectory":"missing-schemas"}\n', + ); + writeFileSync( + resolve(target, "palschema.config.json"), + '{"allowMissingGenerated":true}\n', + ); + writeFileSync( + resolve(target, "items", "example.json"), + '{"Example":{"Type":"Generic","IconTexture":"/Game/Test/T.T","TypeA":"Any","TypeB":"Any","Rank":0,"Rarity":0,"MaxStackCount":1}}\n', + ); + + const validation = runCli(["validate", target], caller); + assert.equal(validation.status, 0, validation.stderr); + assert.match(validation.stdout, /PS_SCHEMA_ENUMS_MISSING.*warning/); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("recursive discovery terminates across a directory symlink cycle", () => { + const workspace = mkdtempSync(resolve(tmpdir(), "palschema-cycle-")); + try { + mkdirSync(resolve(workspace, "items")); + writeFileSync(resolve(workspace, "items", "data.json"), "{}\n"); + symlinkSync(workspace, resolve(workspace, "items", "cycle"), "dir"); + const validation = runCli( + ["validate", "--allow-missing-generated", workspace], + packageRoot, + ); + assert.equal(validation.status, 0, validation.stderr); + assert.match(validation.stdout, /Validated 1 file\(s\)/); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +}); + +test("init rejects schema-index traversal before copying", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-init-path-")); + try { + const schemas = resolve(temporaryRoot, "schemas"); + const workspace = resolve(temporaryRoot, "workspace"); + cpSync(resolve(repositoryRoot, "assets/schemas"), schemas, { + recursive: true, + }); + mkdirSync(workspace); + const indexPath = resolve(schemas, "schema-index.json"); + const index = JSON.parse(readFileSync(indexPath, "utf8")); + index.schemas[0].file = "../../escaped.json"; + writeFileSync(indexPath, `${JSON.stringify(index)}\n`); + + const initialization = runCli( + ["init", "--schema-dir", schemas, workspace], + packageRoot, + ); + assert.equal(initialization.status, 2); + assert.match(initialization.stderr, /Invalid schema file path/); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("init copies generated raw schema support files", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-init-raw-")); + try { + const schemas = resolve(temporaryRoot, "schemas"); + const workspace = resolve(temporaryRoot, "workspace"); + cpSync(resolve(repositoryRoot, "assets/schemas"), schemas, { + recursive: true, + }); + mkdirSync(resolve(schemas, "raw")); + mkdirSync(workspace); + writeFileSync( + resolve(schemas, "raw.schema.json"), + '{"type":"object","properties":{"Table":{"$ref":"raw/Table.schema.json"}}}\n', + ); + writeFileSync( + resolve(schemas, "raw", "Table.schema.json"), + '{"type":"object"}\n', + ); + + const initialization = runCli( + ["init", "--schema-dir", schemas, workspace], + packageRoot, + ); + assert.equal(initialization.status, 0, initialization.stderr); + assert.equal( + existsSync( + resolve( + workspace, + ".palschema", + "schemas", + "raw", + "Table.schema.json", + ), + ), + true, + ); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("init --force atomically removes generated schemas absent from the new pack", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-init-refresh-")); + try { + const schemas = resolve(temporaryRoot, "schemas"); + const workspace = resolve(temporaryRoot, "workspace"); + cpSync(resolve(repositoryRoot, "assets/schemas"), schemas, { + recursive: true, + }); + mkdirSync(resolve(schemas, "raw")); + mkdirSync(workspace); + writeFileSync(resolve(schemas, "enums.schema.json"), '{"definitions":{}}\n'); + writeFileSync(resolve(schemas, "raw.schema.json"), '{"type":"object"}\n'); + writeFileSync( + resolve(schemas, "raw", "Table.schema.json"), + '{"type":"object"}\n', + ); + + const first = runCli( + ["init", "--schema-dir", schemas, workspace], + packageRoot, + ); + assert.equal(first.status, 0, first.stderr); + rmSync(resolve(schemas, "enums.schema.json")); + rmSync(resolve(schemas, "raw.schema.json")); + rmSync(resolve(schemas, "raw"), { recursive: true }); + + const refresh = runCli( + ["init", "--force", "--schema-dir", schemas, workspace], + packageRoot, + ); + assert.equal(refresh.status, 0, refresh.stderr); + assert.equal( + existsSync(resolve(workspace, ".palschema/schemas/enums.schema.json")), + false, + ); + assert.equal( + existsSync(resolve(workspace, ".palschema/schemas/raw.schema.json")), + false, + ); + assert.equal( + existsSync(resolve(workspace, ".palschema/schemas/raw")), + false, + ); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("init rejects a symlinked generated schema source without replacing the workspace", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-init-source-link-")); + try { + const schemas = resolve(temporaryRoot, "schemas"); + const workspace = resolve(temporaryRoot, "workspace"); + const outside = resolve(temporaryRoot, "secret.json"); + cpSync(resolve(repositoryRoot, "assets/schemas"), schemas, { + recursive: true, + }); + mkdirSync(workspace); + writeFileSync(outside, '{"secret":"must-not-copy"}\n'); + symlinkSync(outside, resolve(schemas, "enums.schema.json"), "file"); + + const initialization = runCli( + ["init", "--schema-dir", schemas, workspace], + packageRoot, + ); + assert.equal(initialization.status, 2); + assert.match(initialization.stderr, /regular non-symlink file/); + assert.equal( + existsSync(resolve(workspace, ".palschema/schemas/enums.schema.json")), + false, + ); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("schema index requires metadata and canonical IDs", () => { + for (const mutate of [ + (index) => { + delete index.baseId; + }, + (index) => { + index.schemas[0].id = "not-a-url"; + }, + ]) { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-index-metadata-")); + try { + const schemas = resolve(temporaryRoot, "schemas"); + cpSync(resolve(repositoryRoot, "assets/schemas"), schemas, { + recursive: true, + }); + const indexPath = resolve(schemas, "schema-index.json"); + const index = JSON.parse(readFileSync(indexPath, "utf8")); + mutate(index); + writeFileSync(indexPath, `${JSON.stringify(index)}\n`); + const result = runCli( + ["schemas", "list", "--schema-dir", schemas], + packageRoot, + ); + assert.equal(result.status, 2); + assert.match(result.stderr, /schema-index\.json/i); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } + } +}); + +test("init refuses a symlinked managed parent", () => { + const temporaryRoot = mkdtempSync(resolve(tmpdir(), "palschema-init-link-")); + try { + const workspace = resolve(temporaryRoot, "workspace"); + const outside = resolve(temporaryRoot, "outside"); + mkdirSync(workspace); + mkdirSync(outside); + symlinkSync(outside, resolve(workspace, ".palschema"), "dir"); + + const initialization = runCli(["init", workspace], packageRoot); + assert.equal(initialization.status, 2); + assert.match(initialization.stderr, /symlink/); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("init --force refuses symlinked managed files", () => { + for (const relativeTarget of [ + ".vscode/settings.json", + "palschema.config.json", + ]) { + const temporaryRoot = mkdtempSync( + resolve(tmpdir(), "palschema-init-file-link-"), + ); + try { + const workspace = resolve(temporaryRoot, "workspace"); + const outside = resolve(temporaryRoot, "outside.txt"); + mkdirSync(workspace); + if (relativeTarget.startsWith(".vscode/")) { + mkdirSync(resolve(workspace, ".vscode")); + } + writeFileSync(outside, "sentinel\n"); + symlinkSync(outside, resolve(workspace, relativeTarget), "file"); + + const initialization = runCli( + ["init", "--force", workspace], + packageRoot, + ); + assert.equal(initialization.status, 2); + assert.match(initialization.stderr, /symlink/); + assert.equal(readFileSync(outside, "utf8"), "sentinel\n"); + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }); + } + } +}); diff --git a/tools/palschema-tools/test/configuration.test.ts b/tools/palschema-tools/test/configuration.test.ts new file mode 100644 index 0000000..98c1172 --- /dev/null +++ b/tools/palschema-tools/test/configuration.test.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { findProjectConfig } from "../src/configuration.js"; + +test("discovers project config and resolves its schema path", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-config-")); + try { + const nested = join(temporaryRoot, "mods", "Example", "items"); + await mkdir(nested, { recursive: true }); + await writeFile( + join(temporaryRoot, "palschema.config.json"), + `{ + // JSONC is accepted for a friendlier checked-in config. + "schemaDirectory": ".palschema/schemas", + "allowMissingGenerated": true, + }\n`, + ); + + const config = await findProjectConfig(nested); + assert.equal( + config?.schemaDirectory, + join(temporaryRoot, ".palschema", "schemas"), + ); + assert.equal(config?.allowMissingGenerated, true); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); diff --git a/tools/palschema-tools/test/jsonc-document.test.ts b/tools/palschema-tools/test/jsonc-document.test.ts new file mode 100644 index 0000000..c443937 --- /dev/null +++ b/tools/palschema-tools/test/jsonc-document.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseJsoncDocument } from "../src/jsonc-document.js"; + +test("accepts comments in JSONC but rejects trailing commas", () => { + const parsed = parseJsoncDocument( + `{ + // PalSchema supports JSONC. + "Example": { + "Value": 1 + } + }`, + "example.jsonc", + ); + + assert.deepEqual(parsed.diagnostics, []); + assert.deepEqual(parsed.data, { Example: { Value: 1 } }); + + const trailing = parseJsoncDocument('{"Value": 1,}', "example.jsonc"); + assert.equal(trailing.diagnostics.length > 0, true); +}); + +test("rejects comments in strict JSON files", () => { + const parsed = parseJsoncDocument( + '{\n // JSON comments are runtime-incompatible.\n "Value": 1\n}', + "example.json", + ); + assert.equal(parsed.diagnostics.length > 0, true); +}); + +test("reports one-based source positions for syntax errors", () => { + const parsed = parseJsoncDocument('{\n "Example":,\n}', "broken.jsonc"); + + assert.equal(parsed.diagnostics.length > 0, true); + assert.equal(parsed.diagnostics[0]?.line, 2); + assert.equal(parsed.diagnostics[0]?.source, "palschema"); +}); diff --git a/tools/palschema-tools/test/lsp-options.test.ts b/tools/palschema-tools/test/lsp-options.test.ts new file mode 100644 index 0000000..3792538 --- /dev/null +++ b/tools/palschema-tools/test/lsp-options.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseInitializationOptions } from "../src/lsp-options.js"; + +test("validates LSP initialization options at runtime", () => { + assert.deepEqual(parseInitializationOptions(null), {}); + assert.deepEqual( + parseInitializationOptions({ + schemaDirectory: "/tmp/schemas", + allowMissingGenerated: false, + }), + { + schemaDirectory: "/tmp/schemas", + allowMissingGenerated: false, + }, + ); + for (const invalid of [ + [], + 1, + "options", + { schemaDirectory: 7 }, + { allowMissingGenerated: "false" }, + ]) { + assert.throws(() => parseInitializationOptions(invalid)); + } +}); diff --git a/tools/palschema-tools/test/lsp-smoke.test.mjs b/tools/palschema-tools/test/lsp-smoke.test.mjs new file mode 100644 index 0000000..d718b88 --- /dev/null +++ b/tools/palschema-tools/test/lsp-smoke.test.mjs @@ -0,0 +1,407 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { cp, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import test from "node:test"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(testDirectory, ".."); +const repositoryRoot = resolve(packageRoot, "../.."); + +function encodeMessage(message) { + const payload = JSON.stringify(message); + return `Content-Length: ${Buffer.byteLength(payload)}\r\n\r\n${payload}`; +} + +test("serves diagnostics and schema features over LSP", async (context) => { + const temporarySchemaDirectory = await mkdtemp( + resolve(tmpdir(), "palschema-lsp-schemas-"), + ); + await cp( + resolve(repositoryRoot, "assets/schemas"), + temporarySchemaDirectory, + { recursive: true }, + ); + await writeFile( + resolve(temporarySchemaDirectory, "enums.schema.json"), + JSON.stringify({ + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://okaetsu.github.io/PalSchema/schemas/0.6.1/enums.schema.json", + definitions: { + EPalItemTypeA: { type: "string", enum: ["CanaryTypeA"] }, + EPalItemTypeB: { type: "string", enum: ["CanaryTypeB"] }, + }, + }), + ); + context.after(async () => { + await rm(temporarySchemaDirectory, { recursive: true, force: true }); + }); + const child = spawn( + process.execPath, + ["--import", "tsx", "src/lsp-server.ts", "--stdio"], + { + cwd: packageRoot, + stdio: ["pipe", "pipe", "pipe"], + }, + ); + context.after(() => { + if (!child.killed) { + child.kill(); + } + }); + + let buffer = Buffer.alloc(0); + let stderr = ""; + const received = []; + const waiters = []; + + function dispatch(message) { + received.push(message); + for (const waiter of [...waiters]) { + if (waiter.predicate(message)) { + clearTimeout(waiter.timeout); + waiters.splice(waiters.indexOf(waiter), 1); + waiter.resolve(message); + } + } + } + + function consume() { + for (;;) { + const separator = buffer.indexOf("\r\n\r\n"); + if (separator < 0) { + return; + } + const header = buffer.subarray(0, separator).toString("ascii"); + const lengthMatch = /^Content-Length: (\d+)$/im.exec(header); + assert.ok(lengthMatch, `Missing Content-Length header in ${header}`); + const length = Number(lengthMatch[1]); + const bodyStart = separator + 4; + if (buffer.length < bodyStart + length) { + return; + } + const body = buffer + .subarray(bodyStart, bodyStart + length) + .toString("utf8"); + buffer = buffer.subarray(bodyStart + length); + dispatch(JSON.parse(body)); + } + } + + child.stdout.on("data", (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + consume(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString("utf8"); + }); + + function send(message) { + child.stdin.write(encodeMessage(message)); + } + + function waitFor(predicate, label) { + const existing = received.find(predicate); + if (existing) { + return Promise.resolve(existing); + } + return new Promise((resolvePromise, rejectPromise) => { + const timeout = setTimeout(() => { + rejectPromise( + new Error(`Timed out waiting for ${label}. Server stderr:\n${stderr}`), + ); + }, 10_000); + waiters.push({ predicate, resolve: resolvePromise, timeout }); + }); + } + + send({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + processId: process.pid, + rootUri: pathToFileURL(repositoryRoot).toString(), + capabilities: {}, + initializationOptions: { + schemaDirectory: temporarySchemaDirectory, + allowMissingGenerated: true, + }, + }, + }); + const initialize = await waitFor( + (message) => message.id === 1, + "initialize response", + ); + assert.equal(initialize.result.serverInfo.name, "PalSchema Language Server"); + assert.equal(initialize.result.capabilities.hoverProvider, true); + send({ jsonrpc: "2.0", method: "initialized", params: {} }); + + const documentUri = pathToFileURL( + resolve(repositoryRoot, "fixture/items/invalid.jsonc"), + ).toString(); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: documentUri, + languageId: "jsonc", + version: 1, + text: '{\n "Broken": { "Rarity": 99 }\n}\n', + }, + }, + }); + const diagnostics = await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === documentUri, + "published diagnostics", + ); + assert.equal( + diagnostics.params.diagnostics.some( + (diagnostic) => diagnostic.code === "PS_SCHEMA_MAXIMUM", + ), + true, + ); + + const completionUri = pathToFileURL( + resolve(repositoryRoot, "fixture/items/completion.jsonc"), + ).toString(); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: completionUri, + languageId: "jsonc", + version: 1, + text: '{\n "Example": {\n \n }\n}\n', + }, + }, + }); + await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === completionUri, + "completion document diagnostics", + ); + send({ + jsonrpc: "2.0", + id: 2, + method: "textDocument/completion", + params: { + textDocument: { uri: completionUri }, + position: { line: 2, character: 4 }, + }, + }); + const completion = await waitFor( + (message) => message.id === 2, + "completion response", + ); + const completionItems = Array.isArray(completion.result) + ? completion.result + : completion.result?.items ?? []; + assert.equal( + completionItems.some((item) => item.label === "Type"), + true, + ); + + const enumUri = pathToFileURL( + resolve(repositoryRoot, "fixture/items/enum-completion.jsonc"), + ).toString(); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: enumUri, + languageId: "jsonc", + version: 1, + text: '{\n "Example": {\n "TypeA": ""\n }\n}\n', + }, + }, + }); + await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === enumUri, + "enum completion document diagnostics", + ); + send({ + jsonrpc: "2.0", + id: 20, + method: "textDocument/completion", + params: { + textDocument: { uri: enumUri }, + position: { line: 2, character: 14 }, + }, + }); + const enumCompletion = await waitFor( + (message) => message.id === 20, + "offline enum completion response", + ); + const enumItems = Array.isArray(enumCompletion.result) + ? enumCompletion.result + : enumCompletion.result?.items ?? []; + assert.equal( + enumItems.some( + (item) => + item.label === '"CanaryTypeA"' && + item.textEdit?.newText === '"CanaryTypeA"', + ), + true, + ); + + const hoverText = '{\n "Example": {\n "Rarity": 1\n }\n}\n'; + send({ + jsonrpc: "2.0", + method: "textDocument/didChange", + params: { + textDocument: { uri: completionUri, version: 2 }, + contentChanges: [{ text: hoverText }], + }, + }); + await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === completionUri, + "hover document diagnostics", + ); + send({ + jsonrpc: "2.0", + id: 3, + method: "textDocument/hover", + params: { + textDocument: { uri: completionUri }, + position: { line: 2, character: 7 }, + }, + }); + const hover = await waitFor( + (message) => message.id === 3, + "hover response", + ); + assert.match(JSON.stringify(hover.result), /background color/i); + + const closedUri = pathToFileURL( + resolve(repositoryRoot, "fixture/items/closed.jsonc"), + ).toString(); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: closedUri, + languageId: "jsonc", + version: 1, + text: '{\n "Broken": { "Rarity": 99 }\n}\n', + }, + }, + }); + send({ + jsonrpc: "2.0", + method: "textDocument/didClose", + params: { textDocument: { uri: closedUri } }, + }); + const cleared = await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === closedUri && + message.params?.diagnostics?.length === 0, + "closed document diagnostics clear", + ); + const clearIndex = received.indexOf(cleared); + await new Promise((resolvePromise) => setTimeout(resolvePromise, 200)); + assert.equal( + received + .slice(clearIndex + 1) + .some( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === closedUri && + message.params?.diagnostics?.length > 0, + ), + false, + ); + + const largeUri = pathToFileURL( + resolve(repositoryRoot, "fixture/items/large-invalid.json"), + ).toString(); + const largeDocument = Object.fromEntries( + Array.from({ length: 700 }, (_, index) => [ + `Broken${index}`, + { Rarity: 99 }, + ]), + ); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: largeUri, + languageId: "json", + version: 1, + text: JSON.stringify(largeDocument), + }, + }, + }); + const boundedDiagnostics = await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === largeUri, + "bounded diagnostics", + ); + assert.equal(boundedDiagnostics.params.diagnostics.length, 500); + + const rawUri = pathToFileURL( + resolve(repositoryRoot, "fixture/raw/stale.json"), + ).toString(); + send({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: rawUri, + languageId: "json", + version: 1, + text: "{}\n", + }, + }, + }); + const missingRaw = await waitFor( + (message) => + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === rawUri && + message.params?.diagnostics?.some( + (diagnostic) => diagnostic.code === "PS_SCHEMA_GENERATED_MISSING", + ), + "missing raw schema diagnostic", + ); + const malformedStart = received.indexOf(missingRaw) + 1; + await writeFile( + resolve(temporarySchemaDirectory, "raw.schema.json"), + "{malformed\n", + ); + send({ + jsonrpc: "2.0", + method: "textDocument/didChange", + params: { + textDocument: { uri: rawUri, version: 2 }, + contentChanges: [{ text: '{"changed":true}\n' }], + }, + }); + await waitFor( + (message) => + received.indexOf(message) >= malformedStart && + message.method === "textDocument/publishDiagnostics" && + message.params?.uri === rawUri && + message.params?.diagnostics?.length === 0, + "stale diagnostics clear after validator failure", + ); + + send({ jsonrpc: "2.0", id: 4, method: "shutdown", params: null }); + await waitFor((message) => message.id === 4, "shutdown response"); + send({ jsonrpc: "2.0", method: "exit", params: null }); +}); diff --git a/tools/palschema-tools/test/public-schema-copy.test.mjs b/tools/palschema-tools/test/public-schema-copy.test.mjs new file mode 100644 index 0000000..ba12b61 --- /dev/null +++ b/tools/palschema-tools/test/public-schema-copy.test.mjs @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { + cp, + mkdtemp, + readFile, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import test from "node:test"; + +import { copyPublicSchemas } from "../../../scripts/copy-public-schemas.mjs"; + +const repositoryRoot = resolve(import.meta.dirname, "../../.."); + +test("public schema packaging rejects generated Palworld data", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-public-pack-")); + try { + const source = join(temporaryRoot, "source"); + const destination = join(temporaryRoot, "destination"); + await cp(resolve(repositoryRoot, "assets/schemas"), source, { + recursive: true, + }); + await writeFile( + join(source, "enums.schema.json"), + '{"definitions":{"CanaryPrivateGameValue":{"enum":["secret"]}}}\n', + ); + await assert.rejects( + copyPublicSchemas(source, destination), + /runtime-generated schema/, + ); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("public schema packaging rejects an ancestor destination without deleting it", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-public-overlap-")); + try { + const source = join(temporaryRoot, "source"); + const sentinel = join(temporaryRoot, "sentinel.txt"); + await cp(resolve(repositoryRoot, "assets/schemas"), source, { + recursive: true, + }); + await writeFile(sentinel, "preserve\n"); + await assert.rejects( + copyPublicSchemas(source, temporaryRoot), + /must not overlap/, + ); + assert.equal(await readFile(sentinel, "utf8"), "preserve\n"); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); diff --git a/tools/palschema-tools/test/validator.test.ts b/tools/palschema-tools/test/validator.test.ts new file mode 100644 index 0000000..a1db9e3 --- /dev/null +++ b/tools/palschema-tools/test/validator.test.ts @@ -0,0 +1,191 @@ +import assert from "node:assert/strict"; +import { + cp, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import test from "node:test"; + +import { PalSchemaValidator } from "../src/validator.js"; +import { SchemaRegistry } from "../src/schema-registry.js"; + +const repositoryRoot = resolve(import.meta.dirname, "../../.."); +const schemaDirectory = resolve(repositoryRoot, "assets/schemas"); + +test("validates a checked-in JSON example with offline enum fallback", async () => { + const validator = await PalSchemaValidator.create(schemaDirectory, { + allowMissingGenerated: true, + }); + const result = await validator.validateFile( + resolve( + repositoryRoot, + "assets/examples/ExampleMod/items/example_items.json", + ), + ); + + assert.equal( + result.diagnostics.some((diagnostic) => diagnostic.severity === "error"), + false, + ); + assert.equal( + result.diagnostics.some( + (diagnostic) => diagnostic.code === "PS_SCHEMA_ENUMS_MISSING", + ), + true, + ); +}); + +test("requires runtime-generated enum schemas in strict mode", async () => { + const validator = await PalSchemaValidator.create(schemaDirectory); + const result = await validator.validateFile( + resolve( + repositoryRoot, + "assets/examples/ExampleMod/items/example_items.json", + ), + ); + + const diagnostic = result.diagnostics.find( + (item) => item.code === "PS_SCHEMA_ENUMS_MISSING", + ); + assert.ok(diagnostic); + assert.equal(diagnostic.severity, "error"); +}); + +test("reports schema errors at a stable source position", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-tools-")); + try { + const itemsDirectory = join(temporaryRoot, "items"); + await mkdir(itemsDirectory); + const file = join(itemsDirectory, "invalid.jsonc"); + await writeFile(file, '{\n "Broken": { "Rarity": 99 }\n}\n'); + + const validator = await PalSchemaValidator.create(schemaDirectory); + const result = await validator.validateFile(file); + const diagnostic = result.diagnostics.find( + (item) => item.code === "PS_SCHEMA_MAXIMUM", + ); + assert.ok(diagnostic); + assert.equal(diagnostic.line, 2); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("requires runtime-generated raw schemas explicitly", async () => { + const validator = await PalSchemaValidator.create(schemaDirectory); + const result = await validator.validateText( + "{}", + resolve(repositoryRoot, "fixture/raw/example.json"), + ); + + assert.equal(result.diagnostics[0]?.code, "PS_SCHEMA_GENERATED_MISSING"); + assert.equal(result.diagnostics[0]?.severity, "error"); +}); + +test("can downgrade missing generated schemas for offline repository checks", async () => { + const validator = await PalSchemaValidator.create(schemaDirectory, { + allowMissingGenerated: true, + }); + const result = await validator.validateText( + "{}", + resolve(repositoryRoot, "fixture/raw/example.jsonc"), + ); + + assert.equal(result.diagnostics[0]?.code, "PS_SCHEMA_GENERATED_MISSING"); + assert.equal(result.diagnostics[0]?.severity, "warning"); +}); + +test("accepts an unpinned local runtime-generated schema", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-schemas-")); + try { + const sourceIndex = JSON.parse( + await readFile(resolve(schemaDirectory, "schema-index.json"), "utf8"), + ); + await writeFile( + join(temporaryRoot, "schema-index.json"), + `${JSON.stringify(sourceIndex)}\n`, + ); + await writeFile( + join(temporaryRoot, "raw.schema.json"), + '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object"}\n', + ); + + const registry = await SchemaRegistry.load(temporaryRoot, { + verifyStatic: false, + }); + const verification = await registry.verify(); + assert.equal( + verification.find((result) => result.entry.file === "raw.schema.json") + ?.status, + "present-generated", + ); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("rejects a modified static schema before validation", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-integrity-")); + try { + await cp(schemaDirectory, temporaryRoot, { recursive: true }); + await writeFile( + join(temporaryRoot, "items.schema.json"), + '{"type":"object"}\n', + ); + await assert.rejects( + PalSchemaValidator.create(temporaryRoot), + /Static schema integrity check failed.*items\.schema\.json/, + ); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test("loads generated raw table support schemas by canonical ID", async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), "palschema-raw-pack-")); + try { + await cp(schemaDirectory, temporaryRoot, { recursive: true }); + await mkdir(join(temporaryRoot, "raw")); + await writeFile( + join(temporaryRoot, "raw.schema.json"), + JSON.stringify({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + ExampleTable: { $ref: "raw/ExampleTable.schema.json" }, + }, + additionalProperties: false, + }), + ); + await writeFile( + join(temporaryRoot, "raw", "ExampleTable.schema.json"), + JSON.stringify({ + type: "object", + additionalProperties: { + type: "object", + required: ["Value"], + properties: { Value: { type: "string" } }, + }, + }), + ); + + const validator = await PalSchemaValidator.create(temporaryRoot, { + allowMissingGenerated: true, + }); + const result = await validator.validateText( + '{"ExampleTable":{"Row":{"Value":"ok"}}}', + resolve(repositoryRoot, "fixture/raw/example.json"), + ); + assert.equal( + result.diagnostics.some((diagnostic) => diagnostic.severity === "error"), + false, + ); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); diff --git a/tools/palschema-tools/test/watch.test.mjs b/tools/palschema-tools/test/watch.test.mjs new file mode 100644 index 0000000..7978b93 --- /dev/null +++ b/tools/palschema-tools/test/watch.test.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import test from "node:test"; + +const packageRoot = resolve(import.meta.dirname, ".."); +const cliSource = resolve(packageRoot, "src/cli.ts"); +const tsxImport = import.meta.resolve("tsx"); + +test("JSON watch mode emits NDJSON and coalesces to the newest snapshot", async (context) => { + const workspace = await mkdtemp(resolve(tmpdir(), "palschema-watch-")); + const items = resolve(workspace, "items"); + const document = resolve(items, "item.json"); + await mkdir(items); + await writeFile( + resolve(workspace, "palschema.config.json"), + '{"allowMissingGenerated":true}\n', + ); + await writeFile(document, "{}\n"); + + const child = spawn( + process.execPath, + [ + "--import", + tsxImport, + cliSource, + "validate", + "--watch", + "--format", + "json", + workspace, + ], + { cwd: packageRoot, stdio: ["ignore", "pipe", "pipe"] }, + ); + context.after(async () => { + if (!child.killed) { + child.kill("SIGTERM"); + } + await rm(workspace, { recursive: true, force: true }); + }); + + const events = []; + const waiters = []; + let buffer = ""; + let stderr = ""; + + const dispatch = (line) => { + if (!line) { + return; + } + const event = JSON.parse(line); + events.push(event); + for (const waiter of [...waiters]) { + if (waiter.predicate(event)) { + clearTimeout(waiter.timeout); + waiters.splice(waiters.indexOf(waiter), 1); + waiter.resolve(event); + } + } + }; + child.stdout.on("data", (chunk) => { + buffer += chunk.toString("utf8"); + for (;;) { + const newline = buffer.indexOf("\n"); + if (newline < 0) { + break; + } + dispatch(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + } + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString("utf8"); + }); + + const waitFor = (predicate, label) => { + const existing = events.find(predicate); + if (existing) { + return Promise.resolve(existing); + } + return new Promise((resolvePromise, rejectPromise) => { + const timeout = setTimeout( + () => rejectPromise(new Error(`Timed out waiting for ${label}: ${stderr}`)), + 10_000, + ); + waiters.push({ predicate, resolve: resolvePromise, timeout }); + }); + }; + + await waitFor((event) => event.event === "watch-started", "watch start"); + const validationCount = events.filter( + (event) => event.event === "validation", + ).length; + await writeFile(document, '{"Broken":{"Rarity":99}}\n'); + await new Promise((resolvePromise) => setTimeout(resolvePromise, 10)); + await writeFile(document, "{}\n"); + const newest = await waitFor( + (_event) => + events.filter((event) => event.event === "validation").length > + validationCount, + "coalesced validation", + ); + assert.equal(newest.errors, 0); + assert.equal(stderr, ""); + + await writeFile(resolve(workspace, "palschema.config.json"), "{broken\n"); + await waitFor((event) => event.event === "error", "watch validation error"); + child.kill("SIGINT"); + const exitCode = await new Promise((resolvePromise) => + child.once("exit", resolvePromise), + ); + assert.equal(exitCode, 2); +}); diff --git a/tools/palschema-tools/tsconfig.json b/tools/palschema-tools/tsconfig.json new file mode 100644 index 0000000..24d0665 --- /dev/null +++ b/tools/palschema-tools/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/tools/vscode-palschema/.vscodeignore b/tools/vscode-palschema/.vscodeignore new file mode 100644 index 0000000..cc78832 --- /dev/null +++ b/tools/vscode-palschema/.vscodeignore @@ -0,0 +1,6 @@ +src/** +scripts/** +test/** +tsconfig.json +node_modules/** +*.vsix diff --git a/tools/vscode-palschema/LICENSE b/tools/vscode-palschema/LICENSE new file mode 100644 index 0000000..d152b5e --- /dev/null +++ b/tools/vscode-palschema/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Okaetsu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/vscode-palschema/README.md b/tools/vscode-palschema/README.md new file mode 100644 index 0000000..eb590c2 --- /dev/null +++ b/tools/vscode-palschema/README.md @@ -0,0 +1,12 @@ +# PalSchema for VS Code + +This extension adds PalSchema-aware diagnostics, JSON/JSONC completion, hover +documentation, document symbols, and formatting on Linux, Windows, and macOS. + +Its language features attach only to JSON or JSONC files below a PalSchema +loader directory such as `items`, `pals`, `raw`, or `blueprints`. Static +schemas are bundled. The game-generated `enums.schema.json` and +`raw.schema.json` can be selected with the `palschema.schemaDirectory` setting +after copying them from a current PalSchema runtime. + +UE4SS, game data, and generated proprietary schemas are not bundled. diff --git a/tools/vscode-palschema/package.json b/tools/vscode-palschema/package.json new file mode 100644 index 0000000..91d8a22 --- /dev/null +++ b/tools/vscode-palschema/package.json @@ -0,0 +1,73 @@ +{ + "name": "palschema-vscode", + "displayName": "PalSchema", + "description": "PalSchema JSON/JSONC validation, completion, hover help, and formatting.", + "version": "0.6.1", + "publisher": "okaetsu", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Okaetsu/PalSchema.git" + }, + "private": true, + "engines": { + "vscode": "^1.91.0" + }, + "categories": [ + "Programming Languages", + "Linters", + "Formatters" + ], + "keywords": [ + "Palworld", + "PalSchema", + "JSONC", + "modding" + ], + "main": "./dist/extension.js", + "activationEvents": [ + "onLanguage:json", + "onLanguage:jsonc" + ], + "contributes": { + "commands": [ + { + "command": "palschema.restartLanguageServer", + "title": "PalSchema: Restart Language Server" + } + ], + "configuration": { + "title": "PalSchema", + "properties": { + "palschema.schemaDirectory": { + "type": "string", + "default": "", + "scope": "resource", + "description": "Optional schema directory. Relative paths are resolved from the document's workspace folder; empty uses the schemas bundled with the extension." + }, + "palschema.allowMissingGenerated": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "Report missing runtime-generated raw/enums schemas as warnings instead of errors." + } + } + } + }, + "scripts": { + "build": "node scripts/build.mjs", + "package:vsix": "node scripts/package-extension.mjs", + "test": "tsx --test test/*.test.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "vscode-languageclient": "10.1.0" + }, + "devDependencies": { + "@types/vscode": "1.91.0", + "@vscode/vsce": "3.9.2", + "esbuild": "0.28.1", + "tsx": "4.20.3", + "typescript": "5.8.3" + } +} diff --git a/tools/vscode-palschema/scripts/build.mjs b/tools/vscode-palschema/scripts/build.mjs new file mode 100644 index 0000000..283801a --- /dev/null +++ b/tools/vscode-palschema/scripts/build.mjs @@ -0,0 +1,40 @@ +import { mkdir, rm } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { build } from "esbuild"; +import { copyPublicSchemas } from "../../../scripts/copy-public-schemas.mjs"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const extensionRoot = resolve(scriptDirectory, ".."); +const repositoryRoot = resolve(extensionRoot, "../.."); +const outputDirectory = resolve(extensionRoot, "dist"); + +await rm(outputDirectory, { recursive: true, force: true }); +await mkdir(outputDirectory, { recursive: true }); +await Promise.all([ + build({ + entryPoints: [resolve(extensionRoot, "src/extension.ts")], + outfile: resolve(outputDirectory, "extension.js"), + bundle: true, + platform: "node", + format: "cjs", + external: ["vscode"], + target: "node20", + }), + build({ + entryPoints: [ + resolve(repositoryRoot, "tools/palschema-tools/src/lsp-server.ts"), + ], + outfile: resolve(outputDirectory, "server.mjs"), + bundle: true, + platform: "node", + format: "esm", + target: "node20", + }), +]); + +await copyPublicSchemas( + resolve(repositoryRoot, "assets/schemas"), + resolve(outputDirectory, "schemas"), +); diff --git a/tools/vscode-palschema/scripts/package-extension.mjs b/tools/vscode-palschema/scripts/package-extension.mjs new file mode 100644 index 0000000..72d8b2c --- /dev/null +++ b/tools/vscode-palschema/scripts/package-extension.mjs @@ -0,0 +1,41 @@ +import { mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const extensionRoot = resolve(scriptDirectory, ".."); +const repositoryRoot = resolve(extensionRoot, "../.."); +const outputDirectory = resolve(repositoryRoot, "dist"); +const packageDefinition = await import( + new URL("../package.json", import.meta.url), + { with: { type: "json" } } +); +const output = resolve( + outputDirectory, + `palschema-vscode-${packageDefinition.default.version}.vsix`, +); + +await mkdir(outputDirectory, { recursive: true }); + +const executable = + process.platform === "win32" + ? resolve(repositoryRoot, "node_modules/.bin/vsce.cmd") + : resolve(repositoryRoot, "node_modules/.bin/vsce"); +const child = spawn( + executable, + ["package", "--no-dependencies", "--out", output], + { + cwd: extensionRoot, + stdio: "inherit", + }, +); +const exitCode = await new Promise((resolvePromise, rejectPromise) => { + child.once("error", rejectPromise); + child.once("exit", resolvePromise); +}); +if (exitCode !== 0) { + throw new Error(`vsce exited with code ${String(exitCode)}.`); +} + +console.log(output); diff --git a/tools/vscode-palschema/src/extension.ts b/tools/vscode-palschema/src/extension.ts new file mode 100644 index 0000000..c81f23e --- /dev/null +++ b/tools/vscode-palschema/src/extension.ts @@ -0,0 +1,216 @@ +import { isAbsolute, resolve } from "node:path"; + +import { + commands, + RelativePattern, + type ExtensionContext, + type WorkspaceFolder, + Uri, + workspace, +} from "vscode"; +import { + LanguageClient, + TransportKind, + type LanguageClientOptions, + type ServerOptions, +} from "vscode-languageclient/node"; + +import { SerialOperationQueue } from "./serial-operation-queue.js"; +import { startClientsIndependently } from "./start-clients-independently.js"; + +const PALSCHEMA_FOLDERS = [ + "appearance", + "blueprints", + "buildings", + "enums", + "helpguide", + "items", + "npcs", + "pals", + "raw", + "resources", + "skins", + "spawns", + "translations", +]; + +const clients = new Map(); +const clientOperations = new SerialOperationQueue(); + +function configuredSchemaDirectory( + context: ExtensionContext, + folder: WorkspaceFolder, +): string { + const configured = workspace + .getConfiguration("palschema", folder.uri) + .get("schemaDirectory", "") + .trim(); + if (!configured) { + return context.asAbsolutePath("dist/schemas"); + } + if (isAbsolute(configured)) { + return configured; + } + return resolve(folder.uri.fsPath, configured); +} + +function documentSelector(folder: WorkspaceFolder): NonNullable< + LanguageClientOptions["documentSelector"] +> { + return PALSCHEMA_FOLDERS.flatMap((loaderFolder) => [ + { + scheme: "file", + language: "json", + pattern: new RelativePattern( + folder, + `**/${loaderFolder}/**/*.json`, + ) as unknown as string, + }, + { + scheme: "file", + language: "jsonc", + pattern: new RelativePattern( + folder, + `**/${loaderFolder}/**/*.jsonc`, + ) as unknown as string, + }, + ]); +} + +async function resourceExists(uri: Uri): Promise { + try { + await workspace.fs.stat(uri); + return true; + } catch { + return false; + } +} + +async function isPalSchemaWorkspace(folder: WorkspaceFolder): Promise { + const root = folder.uri; + if ( + await resourceExists(Uri.joinPath(root, "palschema.config.json")) + ) { + return true; + } + if ( + (await resourceExists(Uri.joinPath(root, "CMakeLists.txt"))) && + (await resourceExists( + Uri.joinPath(root, "assets", "schemas", "schema-index.json"), + )) + ) { + return true; + } + if ( + (await resourceExists(Uri.joinPath(root, "enabled.txt"))) && + (await resourceExists(Uri.joinPath(root, "mods"))) + ) { + return true; + } + return resourceExists( + Uri.joinPath(root, "Mods", "PalSchema", "enabled.txt"), + ); +} + +async function startClient( + context: ExtensionContext, + folder: WorkspaceFolder, +): Promise { + const serverModule = context.asAbsolutePath("dist/server.mjs"); + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { module: serverModule, transport: TransportKind.ipc }, + }; + const clientOptions: LanguageClientOptions = { + documentSelector: documentSelector(folder), + workspaceFolder: folder, + initializationOptions: { + schemaDirectory: configuredSchemaDirectory(context, folder), + allowMissingGenerated: workspace + .getConfiguration("palschema", folder.uri) + .get("allowMissingGenerated", true), + }, + }; + const client = new LanguageClient( + `palschema-${folder.index}`, + `PalSchema Language Server (${folder.name})`, + serverOptions, + clientOptions, + ); + try { + await client.start(); + clients.set(folder.uri.toString(), client); + return null; + } catch (error) { + try { + await client.stop(); + } catch { + // The client may not have reached a stoppable state. + } + return error instanceof Error ? error : new Error(String(error)); + } +} + +async function stopClients(): Promise { + const running = [...clients.values()]; + clients.clear(); + await Promise.all(running.map((client) => client.stop())); +} + +async function restartClients(context: ExtensionContext): Promise { + await stopClients(); + const folders: WorkspaceFolder[] = []; + for (const folder of workspace.workspaceFolders ?? []) { + if (await isPalSchemaWorkspace(folder)) { + folders.push(folder); + } + } + const failures = await startClientsIndependently( + folders, + (folder) => startClient(context, folder), + ); + for (const { candidate: folder, error } of failures) { + console.error(`Unable to start PalSchema for ${folder.name}.`, error); + } + if (failures.length > 0 && clients.size === 0) { + throw new AggregateError( + failures.map(({ candidate: folder, error }) => + new Error(`Unable to start PalSchema for ${folder.name}`, { + cause: error, + }), + ), + "No PalSchema language server started.", + ); + } +} + +function scheduleRestart(context: ExtensionContext): Promise { + return clientOperations.run(() => restartClients(context)); +} + +function requestRestart(context: ExtensionContext): void { + void scheduleRestart(context).catch((error: unknown) => { + console.error("Unable to restart PalSchema language servers.", error); + }); +} + +export async function activate(context: ExtensionContext): Promise { + context.subscriptions.push( + commands.registerCommand("palschema.restartLanguageServer", () => + scheduleRestart(context), + ), + workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("palschema")) { + requestRestart(context); + } + }), + workspace.onDidChangeWorkspaceFolders(() => { + requestRestart(context); + }), + ); + await scheduleRestart(context); +} + +export async function deactivate(): Promise { + await clientOperations.run(stopClients); +} diff --git a/tools/vscode-palschema/src/serial-operation-queue.ts b/tools/vscode-palschema/src/serial-operation-queue.ts new file mode 100644 index 0000000..6a829f6 --- /dev/null +++ b/tools/vscode-palschema/src/serial-operation-queue.ts @@ -0,0 +1,12 @@ +export class SerialOperationQueue { + private tail: Promise = Promise.resolve(); + + run(operation: () => Promise): Promise { + const result = this.tail.then(operation, operation); + this.tail = result.then( + () => undefined, + () => undefined, + ); + return result; + } +} diff --git a/tools/vscode-palschema/src/start-clients-independently.ts b/tools/vscode-palschema/src/start-clients-independently.ts new file mode 100644 index 0000000..ad985bf --- /dev/null +++ b/tools/vscode-palschema/src/start-clients-independently.ts @@ -0,0 +1,18 @@ +export interface ClientStartFailure { + candidate: T; + error: Error; +} + +export async function startClientsIndependently( + candidates: readonly T[], + start: (candidate: T) => Promise, +): Promise[]> { + const failures: ClientStartFailure[] = []; + for (const candidate of candidates) { + const error = await start(candidate); + if (error) { + failures.push({ candidate, error }); + } + } + return failures; +} diff --git a/tools/vscode-palschema/test/serial-operation-queue.test.ts b/tools/vscode-palschema/test/serial-operation-queue.test.ts new file mode 100644 index 0000000..c654573 --- /dev/null +++ b/tools/vscode-palschema/test/serial-operation-queue.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { SerialOperationQueue } from "../src/serial-operation-queue.js"; + +test("serializes overlapping client lifecycle operations", async () => { + const queue = new SerialOperationQueue(); + const events: string[] = []; + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + + const first = queue.run(async () => { + events.push("first-start"); + await firstGate; + events.push("first-end"); + }); + const second = queue.run(async () => { + events.push("second-start"); + events.push("second-end"); + }); + + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(events, ["first-start"]); + releaseFirst?.(); + await Promise.all([first, second]); + assert.deepEqual(events, [ + "first-start", + "first-end", + "second-start", + "second-end", + ]); +}); + +test("continues after a failed lifecycle operation", async () => { + const queue = new SerialOperationQueue(); + await assert.rejects( + queue.run(async () => { + throw new Error("start failed"); + }), + /start failed/, + ); + assert.equal(await queue.run(async () => "recovered"), "recovered"); +}); diff --git a/tools/vscode-palschema/test/start-clients-independently.test.ts b/tools/vscode-palschema/test/start-clients-independently.test.ts new file mode 100644 index 0000000..02d94e0 --- /dev/null +++ b/tools/vscode-palschema/test/start-clients-independently.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { startClientsIndependently } from "../src/start-clients-independently.js"; + +test("keeps starting healthy workspace clients after one fails", async () => { + const started: string[] = []; + const failures = await startClientsIndependently( + ["healthy-a", "broken", "healthy-b"], + async (candidate) => { + if (candidate === "broken") { + return new Error("invalid schema directory"); + } + started.push(candidate); + return null; + }, + ); + + assert.deepEqual(started, ["healthy-a", "healthy-b"]); + assert.equal(failures.length, 1); + assert.equal(failures[0]?.candidate, "broken"); + assert.match(failures[0]?.error.message ?? "", /invalid schema directory/); +}); diff --git a/tools/vscode-palschema/tsconfig.json b/tools/vscode-palschema/tsconfig.json new file mode 100644 index 0000000..072d5f9 --- /dev/null +++ b/tools/vscode-palschema/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "src", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +}