summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/ci
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/ci')
-rw-r--r--archived/projt-launcher/ci/code-quality.nix51
-rw-r--r--archived/projt-launcher/ci/code-quality.sh156
-rw-r--r--archived/projt-launcher/ci/codeowners-validator/default.nix52
-rw-r--r--archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch15
-rw-r--r--archived/projt-launcher/ci/codeowners-validator/permissions.patch36
-rw-r--r--archived/projt-launcher/ci/default.nix68
-rw-r--r--archived/projt-launcher/ci/eval/attrpaths.nix147
-rw-r--r--archived/projt-launcher/ci/eval/chunk.nix69
-rw-r--r--archived/projt-launcher/ci/eval/compare/cmp-stats.py218
-rw-r--r--archived/projt-launcher/ci/eval/compare/default.nix134
-rw-r--r--archived/projt-launcher/ci/eval/compare/generate-step-summary.jq70
-rw-r--r--archived/projt-launcher/ci/eval/compare/maintainers.nix142
-rw-r--r--archived/projt-launcher/ci/eval/compare/utils.nix198
-rw-r--r--archived/projt-launcher/ci/eval/default.nix316
-rw-r--r--archived/projt-launcher/ci/eval/diff.nix123
-rw-r--r--archived/projt-launcher/ci/eval/outpaths.nix133
-rw-r--r--archived/projt-launcher/ci/github-script/.editorconfig3
-rw-r--r--archived/projt-launcher/ci/github-script/.gitignore2
-rw-r--r--archived/projt-launcher/ci/github-script/.npmrc2
-rw-r--r--archived/projt-launcher/ci/github-script/backport.js688
-rw-r--r--archived/projt-launcher/ci/github-script/commit-types.json70
-rw-r--r--archived/projt-launcher/ci/github-script/commits.js336
-rw-r--r--archived/projt-launcher/ci/github-script/get-teams.js134
-rw-r--r--archived/projt-launcher/ci/github-script/merge.js308
-rw-r--r--archived/projt-launcher/ci/github-script/package-lock.json1721
-rw-r--r--archived/projt-launcher/ci/github-script/package.json19
-rw-r--r--archived/projt-launcher/ci/github-script/prepare.js314
-rw-r--r--archived/projt-launcher/ci/github-script/reviewers.js329
-rw-r--r--archived/projt-launcher/ci/github-script/reviews.js93
-rw-r--r--archived/projt-launcher/ci/github-script/run132
-rw-r--r--archived/projt-launcher/ci/github-script/shell.nix40
-rw-r--r--archived/projt-launcher/ci/github-script/test/commits.test.js42
-rw-r--r--archived/projt-launcher/ci/github-script/withRateLimit.js86
-rw-r--r--archived/projt-launcher/ci/nixpkgs-vet.nix2
-rw-r--r--archived/projt-launcher/ci/nixpkgs-vet.sh6
-rw-r--r--archived/projt-launcher/ci/parse.nix38
-rw-r--r--archived/projt-launcher/ci/pinned.json44
-rw-r--r--archived/projt-launcher/ci/supportedBranches.js99
-rw-r--r--archived/projt-launcher/ci/supportedSystems.json64
-rw-r--r--archived/projt-launcher/ci/supportedVersions.nix68
-rw-r--r--archived/projt-launcher/ci/update-pinned.sh64
41 files changed, 6632 insertions, 0 deletions
diff --git a/archived/projt-launcher/ci/code-quality.nix b/archived/projt-launcher/ci/code-quality.nix
new file mode 100644
index 0000000000..b5706dc83f
--- /dev/null
+++ b/archived/projt-launcher/ci/code-quality.nix
@@ -0,0 +1,51 @@
+{
+ lib,
+ runCommand,
+ clang-tools,
+ cmake,
+ cmake-format,
+}:
+{
+ src ? ../.,
+}:
+
+let
+ sourceFiles = lib.fileset.toSource {
+ root = src;
+ fileset = lib.fileset.unions [
+ (src + /launcher)
+ (src + /tests)
+ (src + /buildconfig)
+ ];
+ };
+in
+runCommand "projt-code-check"
+ {
+ nativeBuildInputs = [
+ clang-tools
+ cmake
+ cmake-format
+ ];
+ }
+ ''
+ echo "Running ProjT Launcher code quality checks..."
+
+ echo "Checking C++ code formatting..."
+ find ${sourceFiles} -type f \( -name "*.cpp" -o -name "*.h" \) | while read file; do
+ if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then
+ echo "Format error in: $file"
+ fi
+ done
+
+ echo "Checking for common code issues..."
+
+ todoCount=$(grep -r "TODO\\|FIXME" ${sourceFiles} --include="*.cpp" --include="*.h" 2>/dev/null | wc -l)
+ echo "Found $todoCount TODO/FIXME comments"
+
+ if grep -r "qDebug\\|std::cout" ${sourceFiles} --include="*.cpp" 2>/dev/null | grep -v "// DEBUG" > /dev/null; then
+ echo "Warning: Debug statements found in code"
+ fi
+
+ echo "Code quality check completed!"
+ touch $out
+ ''
diff --git a/archived/projt-launcher/ci/code-quality.sh b/archived/projt-launcher/ci/code-quality.sh
new file mode 100644
index 0000000000..c37e6109f9
--- /dev/null
+++ b/archived/projt-launcher/ci/code-quality.sh
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+# =============================================================================
+# ProjT Launcher - Code Quality Validation Script
+# =============================================================================
+# Validates code quality between branches/commits for CI purposes
+#
+# Usage:
+# ./ci/code-quality.sh <base_branch> [repository]
+#
+# Example:
+# ./ci/code-quality.sh develop
+# ./ci/code-quality.sh master https://github.com/Project-Tick/ProjT-Launcher.git
+# =============================================================================
+
+set -o pipefail -o errexit -o nounset
+
+trace() { echo >&2 "$@"; }
+
+tmp=$(mktemp -d)
+cleanup() {
+ set +o errexit
+ trace -n "Cleaning up.. "
+ [[ -e "$tmp/base" ]] && git worktree remove --force "$tmp/base"
+ [[ -e "$tmp/merged" ]] && git worktree remove --force "$tmp/merged"
+ rm -rf "$tmp"
+ trace "Done"
+}
+trap cleanup exit
+
+# Default repository
+repo=https://github.com/Project-Tick/ProjT-Launcher.git
+
+# Parse arguments
+if (( $# != 0 )); then
+ baseBranch=$1
+ shift
+else
+ trace "Usage: $0 BASE_BRANCH [REPOSITORY]"
+ trace "BASE_BRANCH: The base branch to compare against (e.g., develop, master)"
+ trace "REPOSITORY: Repository URL (defaults to $repo)"
+ exit 1
+fi
+
+if (( $# != 0 )); then
+ repo=$1
+ shift
+fi
+
+# Check for uncommitted changes
+if [[ -n "$(git status --porcelain)" ]]; then
+ trace -e "\e[33mWarning: Dirty tree, uncommitted changes won't be checked\e[0m"
+fi
+
+headSha=$(git rev-parse HEAD)
+trace -e "Using HEAD commit \e[34m$headSha\e[0m"
+
+# Create worktree for HEAD
+trace -n "Creating Git worktree for HEAD in $tmp/merged.. "
+git worktree add --detach -q "$tmp/merged" HEAD
+trace "Done"
+
+# Fetch and create worktree for base branch
+trace -n "Fetching base branch $baseBranch from $repo.. "
+git fetch -q "$repo" "refs/heads/$baseBranch"
+baseSha=$(git rev-parse FETCH_HEAD)
+trace -e "Done (\e[34m$baseSha\e[0m)"
+
+trace -n "Creating Git worktree for base in $tmp/base.. "
+git worktree add --detach -q "$tmp/base" "$baseSha"
+trace "Done"
+
+# Run code quality checks
+trace ""
+trace "=== Running Code Quality Checks ==="
+trace ""
+
+# Check for clang-format
+if command -v clang-format &> /dev/null; then
+ trace "Checking C++ code formatting..."
+
+ format_errors=0
+ while IFS= read -r -d '' file; do
+ if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then
+ trace " Format issue: $file"
+ ((format_errors++)) || true
+ fi
+ done < <(find "$tmp/merged" -type f \( -name "*.cpp" -o -name "*.h" \) -print0 2>/dev/null)
+
+ if (( format_errors > 0 )); then
+ trace -e "\e[33mFound $format_errors files with formatting issues\e[0m"
+ else
+ trace -e "\e[32mAll C++ files are properly formatted\e[0m"
+ fi
+else
+ trace "clang-format not found, skipping format check"
+fi
+
+# Check for changed files
+trace ""
+trace "Changed files in this PR:"
+git diff --name-only "$baseSha" "$headSha" | while read -r file; do
+ trace " $file"
+done
+
+# Count changes by category
+trace ""
+trace "Change summary:"
+source_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "^(launcher|libraries)/" | wc -l)
+build_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "(CMake|\\.cmake|vcpkg)" | wc -l)
+ci_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "^(\\.github|ci)/" | wc -l)
+doc_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "\\.md$" | wc -l)
+
+trace " Source files: $source_changes"
+trace " Build files: $build_changes"
+trace " CI files: $ci_changes"
+trace " Documentation: $doc_changes"
+
+# Check for common issues in changed files
+trace ""
+trace "Checking for common issues..."
+
+issues=0
+
+# Check for debug statements
+if git diff "$baseSha" "$headSha" -- '*.cpp' '*.h' | grep -E "^\\+.*qDebug|^\\+.*std::cout" | grep -v "// DEBUG" > /dev/null 2>&1; then
+ trace -e "\e[33mWarning: New debug statements found\e[0m"
+ ((issues++)) || true
+fi
+
+# Check for large files
+large_files=$(git diff --name-only "$baseSha" "$headSha" | while read -r file; do
+ if [[ -f "$tmp/merged/$file" ]]; then
+ size=$(wc -c < "$tmp/merged/$file" 2>/dev/null || echo 0)
+ if (( size > 1000000 )); then
+ echo "$file ($((size/1024))KB)"
+ fi
+ fi
+done)
+
+if [[ -n "$large_files" ]]; then
+ trace -e "\e[33mWarning: Large files detected:\e[0m"
+ echo "$large_files" | while read -r line; do
+ trace " $line"
+ done
+ ((issues++)) || true
+fi
+
+trace ""
+if (( issues > 0 )); then
+ trace -e "\e[33mCode check completed with $issues warnings\e[0m"
+else
+ trace -e "\e[32mCode check completed successfully\e[0m"
+fi
+
+exit 0
+
diff --git a/archived/projt-launcher/ci/codeowners-validator/default.nix b/archived/projt-launcher/ci/codeowners-validator/default.nix
new file mode 100644
index 0000000000..469655de2c
--- /dev/null
+++ b/archived/projt-launcher/ci/codeowners-validator/default.nix
@@ -0,0 +1,52 @@
+# =============================================================================
+# ProjT Launcher - CODEOWNERS Validator
+# =============================================================================
+# Validates the OWNERS file to ensure proper maintainer assignments.
+# This helps maintain accurate code ownership across the project.
+#
+# Usage:
+# nix-build ci/codeowners-validator
+# =============================================================================
+
+{
+ buildGoModule,
+ fetchFromGitHub,
+ fetchpatch,
+ lib,
+}:
+
+buildGoModule {
+ pname = "codeowners-validator";
+ version = "0.7.4-projt";
+
+ src = fetchFromGitHub {
+ owner = "mszostok";
+ repo = "codeowners-validator";
+ rev = "f3651e3810802a37bd965e6a9a7210728179d076";
+ hash = "sha256-5aSmmRTsOuPcVLWfDF6EBz+6+/Qpbj66udAmi1CLmWQ=";
+ };
+
+ patches = [
+ # Allow checking user write access
+ (fetchpatch {
+ name = "user-write-access-check";
+ url = "https://github.com/mszostok/codeowners-validator/compare/f3651e3810802a37bd965e6a9a7210728179d076...840eeb88b4da92bda3e13c838f67f6540b9e8529.patch";
+ hash = "sha256-t3Dtt8SP9nbO3gBrM0nRE7+G6N/ZIaczDyVHYAG/6mU=";
+ })
+ # Custom permissions patch for ProjT Launcher
+ ./permissions.patch
+ # Allow custom OWNERS file path via OWNERS_FILE env var
+ ./owners-file-name.patch
+ ];
+
+ postPatch = "rm -r docs/investigation";
+
+ vendorHash = "sha256-R+pW3xcfpkTRqfS2ETVOwG8PZr0iH5ewroiF7u8hcYI=";
+
+ meta = {
+ description = "CODEOWNERS validator for ProjT Launcher";
+ homepage = "https://github.com/mszostok/codeowners-validator";
+ license = lib.licenses.asl20;
+ mainProgram = "codeowners-validator";
+ };
+}
diff --git a/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch b/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch
new file mode 100644
index 0000000000..d8b87ba2f8
--- /dev/null
+++ b/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch
@@ -0,0 +1,15 @@
+diff --git a/pkg/codeowners/owners.go b/pkg/codeowners/owners.go
+index 6910bd2..e0c95e9 100644
+--- a/pkg/codeowners/owners.go
++++ b/pkg/codeowners/owners.go
+@@ -39,6 +39,10 @@ func NewFromPath(repoPath string) ([]Entry, error) {
+ // openCodeownersFile finds a CODEOWNERS file and returns content.
+ // see: https://help.github.com/articles/about-code-owners/#codeowners-file-location
+ func openCodeownersFile(dir string) (io.Reader, error) {
++ if file, ok := os.LookupEnv("OWNERS_FILE"); ok {
++ return fs.Open(file)
++ }
++
+ var detectedFiles []string
+ for _, p := range []string{".", "docs", ".github"} {
+ pth := path.Join(dir, p)
diff --git a/archived/projt-launcher/ci/codeowners-validator/permissions.patch b/archived/projt-launcher/ci/codeowners-validator/permissions.patch
new file mode 100644
index 0000000000..38f42f4839
--- /dev/null
+++ b/archived/projt-launcher/ci/codeowners-validator/permissions.patch
@@ -0,0 +1,36 @@
+diff --git a/internal/check/valid_owner.go b/internal/check/valid_owner.go
+index a264bcc..610eda8 100644
+--- a/internal/check/valid_owner.go
++++ b/internal/check/valid_owner.go
+@@ -16,7 +16,6 @@ import (
+ const scopeHeader = "X-OAuth-Scopes"
+
+ var reqScopes = map[github.Scope]struct{}{
+- github.ScopeReadOrg: {},
+ }
+
+ type ValidOwnerConfig struct {
+@@ -223,10 +222,7 @@ func (v *ValidOwner) validateTeam(ctx context.Context, name string) *validateErr
+ for _, t := range v.repoTeams {
+ // GitHub normalizes name before comparison
+ if strings.EqualFold(t.GetSlug(), team) {
+- if t.Permissions["push"] {
+- return nil
+- }
+- return newValidateError("Team %q cannot review PRs on %q as neither it nor any parent team has write permissions.", team, v.orgRepoName)
++ return nil
+ }
+ }
+
+@@ -245,10 +241,7 @@ func (v *ValidOwner) validateGitHubUser(ctx context.Context, name string) *valid
+ for _, u := range v.repoUsers {
+ // GitHub normalizes name before comparison
+ if strings.EqualFold(u.GetLogin(), userName) {
+- if u.Permissions["push"] {
+- return nil
+- }
+- return newValidateError("User %q cannot review PRs on %q as they don't have write permissions.", userName, v.orgRepoName)
++ return nil
+ }
+ }
+
diff --git a/archived/projt-launcher/ci/default.nix b/archived/projt-launcher/ci/default.nix
new file mode 100644
index 0000000000..48fa4bcd29
--- /dev/null
+++ b/archived/projt-launcher/ci/default.nix
@@ -0,0 +1,68 @@
+# ProjT Launcher CI Configuration
+# This Nix expression provides a development environment and build dependencies
+# for the ProjT Launcher project
+
+{
+ system ? builtins.currentSystem,
+}:
+
+let
+ nixpkgs = import <nixpkgs> { inherit system; };
+ pkgs = nixpkgs;
+in
+
+rec {
+ # Development environment with all build dependencies
+ devEnv = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ # Build tools
+ cmake
+ ninja
+ pkg-config
+
+ # Compilers
+ gcc
+ clang
+
+ # Qt6 dependencies
+ qt6.full
+ qt6.base
+ qt6.declarative
+ qt6.multimedia
+ qt6.tools
+
+ # Other dependencies
+ zlib
+ libxkbcommon
+
+ # Code quality tools
+ clang-tools
+ cmake-format
+
+ # Testing
+ gtest
+ ];
+
+ shellHook = ''
+ echo "ProjT Launcher development environment loaded"
+ echo "Available: cmake, ninja, qt6, gcc, clang"
+ '';
+ };
+
+ # Build configuration for CI
+ buildConfig = {
+ buildType = "Release";
+ enableTesting = true;
+ enableLTO = true;
+ };
+
+ # Test environment
+ testEnv = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ cmake
+ ninja
+ qt6.full
+ gtest
+ ];
+ };
+}
diff --git a/archived/projt-launcher/ci/eval/attrpaths.nix b/archived/projt-launcher/ci/eval/attrpaths.nix
new file mode 100644
index 0000000000..faa69817bf
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/attrpaths.nix
@@ -0,0 +1,147 @@
+# =============================================================================
+# ProjT Launcher - Build Configuration Paths
+# =============================================================================
+# Lists all configurable build paths and options for the project.
+# Used by CI to validate that all configurations are buildable.
+#
+# Usage:
+# nix-instantiate --eval --strict --json ci/eval/attrpaths.nix -A names
+# =============================================================================
+
+{
+ lib ? import (path + "/lib"),
+ path ? ./../..,
+}:
+
+let
+ # Build configurations available in the project
+ buildConfigs = {
+ # Platform presets from CMakePresets.json
+ presets = [
+ "linux"
+ "windows_mingw"
+ "windows_msvc"
+ "macos_universal"
+ ];
+
+ # Build types
+ buildTypes = [
+ "Debug"
+ "Release"
+ "RelWithDebInfo"
+ "MinSizeRel"
+ ];
+
+ # Compiler options
+ compilers = {
+ linux = [
+ "gcc"
+ "clang"
+ ];
+ macos = [
+ "clang"
+ "apple-clang"
+ ];
+ windows = [
+ "msvc"
+ "mingw-gcc"
+ "clang-cl"
+ ];
+ };
+
+ # Qt versions supported
+ qtVersions = [
+ "6.6"
+ "6.7"
+ "6.8"
+ ];
+
+ # Feature flags
+ features = [
+ "LAUNCHER_ENABLE_UPDATER"
+ "LAUNCHER_FORCE_BUNDLED_LIBS"
+ "LAUNCHER_BUILD_TESTS"
+ ];
+ };
+
+ # Generate all possible build configuration paths
+ generatePaths =
+ let
+ presetPaths = map (p: [
+ "preset"
+ p
+ ]) buildConfigs.presets;
+ buildTypePaths = map (b: [
+ "buildType"
+ b
+ ]) buildConfigs.buildTypes;
+ qtPaths = map (q: [
+ "qt"
+ q
+ ]) buildConfigs.qtVersions;
+ featurePaths = map (f: [
+ "feature"
+ f
+ ]) buildConfigs.features;
+ in
+ presetPaths ++ buildTypePaths ++ qtPaths ++ featurePaths;
+
+ # Build component paths
+ componentPaths = [
+ [
+ "launcher"
+ "core"
+ ]
+ [
+ "launcher"
+ "ui"
+ ]
+ [
+ "launcher"
+ "minecraft"
+ ]
+ [
+ "launcher"
+ "modplatform"
+ ]
+ [
+ "launcher"
+ "java"
+ ]
+ [
+ "launcher"
+ "net"
+ ]
+ [
+ "rainbow"
+ ]
+ [
+ "tomlplusplus"
+ ]
+ [
+ "libnbtplusplus"
+ ]
+ [
+ "LocalPeer"
+ ]
+ [
+ "qdcss"
+ ]
+ [
+ "katabasis"
+ ]
+ ];
+
+ # All paths combined
+ paths = generatePaths ++ componentPaths;
+
+ # Convert paths to dotted names
+ names = map (p: lib.concatStringsSep "." p) paths;
+
+in
+{
+ inherit paths names;
+
+ # Export build configs for other tools
+ inherit buildConfigs;
+}
diff --git a/archived/projt-launcher/ci/eval/chunk.nix b/archived/projt-launcher/ci/eval/chunk.nix
new file mode 100644
index 0000000000..12b02a7f28
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/chunk.nix
@@ -0,0 +1,69 @@
+# =============================================================================
+# ProjT Launcher - Build Configuration Chunking
+# =============================================================================
+# Splits build configurations into smaller chunks for parallel validation.
+# This allows CI to validate multiple configurations in parallel.
+#
+# Usage:
+# nix-instantiate --eval ci/eval/chunk.nix \
+# --arg chunkSize 10 \
+# --arg myChunk 0 \
+# --arg attrpathFile ./attrpaths.json
+# =============================================================================
+
+{
+ lib ? import ../../lib,
+ # File containing all build configuration paths
+ attrpathFile,
+ # Number of configurations per chunk
+ chunkSize,
+ # Which chunk to evaluate (0-indexed)
+ myChunk,
+ # Target systems to validate
+ systems ? [ "x86_64-linux" ],
+}:
+
+let
+ # Import all attribute paths
+ allPaths = lib.importJSON attrpathFile;
+
+ # Get this chunk's paths
+ chunkPaths = lib.sublist (chunkSize * myChunk) chunkSize allPaths;
+
+ # Build configuration validation
+ validateConfig =
+ configPath:
+ let
+ configType = builtins.head configPath;
+ configValue = builtins.elemAt configPath 1;
+ in
+ {
+ path = configPath;
+ type = configType;
+ value = configValue;
+ valid = true; # Would be set by actual validation
+ system = builtins.head systems;
+ };
+
+ # Validate all paths in this chunk
+ validatedConfigs = map validateConfig chunkPaths;
+
+ # Group by type for easier processing
+ groupedConfigs = lib.groupBy (c: c.type) validatedConfigs;
+
+in
+{
+ # Return validated configurations
+ configs = validatedConfigs;
+
+ # Chunk metadata
+ meta = {
+ chunkIndex = myChunk;
+ inherit chunkSize;
+ totalInChunk = builtins.length chunkPaths;
+ inherit systems;
+ };
+
+ # Grouped view
+ byType = groupedConfigs;
+}
diff --git a/archived/projt-launcher/ci/eval/compare/cmp-stats.py b/archived/projt-launcher/ci/eval/compare/cmp-stats.py
new file mode 100644
index 0000000000..e4da1f81e3
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/compare/cmp-stats.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+# =============================================================================
+# ProjT Launcher - Build Statistics Comparison Tool
+# =============================================================================
+# Compares build statistics between two builds/commits.
+# Used by CI to detect performance regressions or improvements.
+#
+# Usage:
+# python cmp-stats.py --explain before_stats/ after_stats/
+# =============================================================================
+
+import argparse
+import json
+import os
+from pathlib import Path
+from tabulate import tabulate
+from typing import Final
+
+
+def flatten_data(json_data: dict) -> dict:
+ """
+ Extracts and flattens metrics from JSON data.
+ Handles nested structures by using dot notation.
+
+ Args:
+ json_data (dict): JSON data containing metrics.
+ Returns:
+ dict: Flattened metrics with keys as metric names.
+ """
+ flat_metrics = {}
+ for key, value in json_data.items():
+ if isinstance(value, (int, float)):
+ flat_metrics[key] = value
+ elif isinstance(value, dict):
+ for subkey, subvalue in value.items():
+ if isinstance(subvalue, (int, float)):
+ flat_metrics[f"{key}.{subkey}"] = subvalue
+ elif isinstance(value, str):
+ flat_metrics[key] = value
+
+ return flat_metrics
+
+
+def load_all_metrics(path: Path) -> dict:
+ """
+ Loads all stats JSON files from the specified path.
+
+ Args:
+ path (Path): Directory or file containing JSON stats.
+
+ Returns:
+ dict: Dictionary with filenames as keys and metrics as values.
+ """
+ metrics = {}
+
+ if path.is_dir():
+ for json_file in path.glob("**/*.json"):
+ try:
+ with json_file.open() as f:
+ data = json.load(f)
+ metrics[str(json_file.relative_to(path))] = flatten_data(data)
+ except (json.JSONDecodeError, IOError) as e:
+ print(f"Warning: Could not load {json_file}: {e}")
+ elif path.is_file():
+ try:
+ with path.open() as f:
+ metrics[path.name] = flatten_data(json.load(f))
+ except (json.JSONDecodeError, IOError) as e:
+ print(f"Warning: Could not load {path}: {e}")
+
+ return metrics
+
+
+METRIC_EXPLANATIONS: Final[str] = """
+### Metric Explanations
+
+| Metric | Description |
+|--------|-------------|
+| build.time | Total build time in seconds |
+| build.memory | Peak memory usage in MB |
+| compile.units | Number of compilation units |
+| link.time | Linking time in seconds |
+| test.passed | Number of tests passed |
+| test.failed | Number of tests failed |
+| binary.size | Final binary size in bytes |
+"""
+
+
+def compare_metrics(before: dict, after: dict) -> tuple:
+ """
+ Compare metrics between two builds.
+
+ Returns:
+ tuple: (changed_metrics, unchanged_metrics)
+ """
+ changed = []
+ unchanged = []
+
+ # Get all metric keys from both
+ all_keys = sorted(set(list(before.keys()) + list(after.keys())))
+
+ for key in all_keys:
+ before_val = before.get(key)
+ after_val = after.get(key)
+
+ if before_val is None or after_val is None:
+ continue
+
+ if isinstance(before_val, (int, float)) and isinstance(after_val, (int, float)):
+ if before_val == after_val:
+ unchanged.append({
+ "metric": key,
+ "value": before_val
+ })
+ else:
+ diff = after_val - before_val
+ pct_change = (diff / before_val * 100) if before_val != 0 else float('inf')
+ changed.append({
+ "metric": key,
+ "before": before_val,
+ "after": after_val,
+ "diff": diff,
+ "pct_change": pct_change
+ })
+
+ return changed, unchanged
+
+
+def format_results(changed: list, unchanged: list, explain: bool) -> str:
+ """Format comparison results as markdown."""
+ result = ""
+
+ if unchanged:
+ result += "## Unchanged Values\n\n"
+ result += tabulate(
+ [[m["metric"], m["value"]] for m in unchanged],
+ headers=["Metric", "Value"],
+ tablefmt="github"
+ )
+ result += "\n\n"
+
+ if changed:
+ result += "## Changed Values\n\n"
+ result += tabulate(
+ [[
+ m["metric"],
+ f"{m['before']:.4f}" if isinstance(m['before'], float) else m['before'],
+ f"{m['after']:.4f}" if isinstance(m['after'], float) else m['after'],
+ f"{m['diff']:+.4f}" if isinstance(m['diff'], float) else m['diff'],
+ f"{m['pct_change']:+.2f}%" if isinstance(m['pct_change'], float) else "N/A"
+ ] for m in changed],
+ headers=["Metric", "Before", "After", "Diff", "Change %"],
+ tablefmt="github"
+ )
+ result += "\n\n"
+
+ if explain:
+ result += METRIC_EXPLANATIONS
+
+ if not changed and not unchanged:
+ result = "No comparable metrics found.\n"
+
+ return result
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Build statistics comparison for ProjT Launcher"
+ )
+ parser.add_argument(
+ "--explain",
+ action="store_true",
+ help="Include metric explanations"
+ )
+ parser.add_argument(
+ "before",
+ help="File or directory containing baseline stats"
+ )
+ parser.add_argument(
+ "after",
+ help="File or directory containing comparison stats"
+ )
+
+ args = parser.parse_args()
+
+ before_path = Path(args.before)
+ after_path = Path(args.after)
+
+ if not before_path.exists():
+ print(f"Error: {before_path} does not exist")
+ return 1
+
+ if not after_path.exists():
+ print(f"Error: {after_path} does not exist")
+ return 1
+
+ before_metrics = load_all_metrics(before_path)
+ after_metrics = load_all_metrics(after_path)
+
+ # Merge all metrics from all files
+ merged_before = {}
+ merged_after = {}
+
+ for metrics in before_metrics.values():
+ merged_before.update(metrics)
+ for metrics in after_metrics.values():
+ merged_after.update(metrics)
+
+ changed, unchanged = compare_metrics(merged_before, merged_after)
+
+ output = format_results(changed, unchanged, args.explain)
+ print(output)
+
+ return 0
+
+
+if __name__ == "__main__":
+ exit(main())
diff --git a/archived/projt-launcher/ci/eval/compare/default.nix b/archived/projt-launcher/ci/eval/compare/default.nix
new file mode 100644
index 0000000000..e9a100de16
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/compare/default.nix
@@ -0,0 +1,134 @@
+# =============================================================================
+# ProjT Launcher - Build Comparison Module
+# =============================================================================
+# Compares build configurations and generates reports for CI.
+# Used to determine what changed between commits and impact on builds.
+# =============================================================================
+
+{
+ lib,
+ jq,
+ runCommand,
+ python3,
+ stdenvNoCC,
+ makeWrapper,
+}:
+
+let
+ # Python environment for statistics
+ python = python3.withPackages (ps: [
+ ps.tabulate
+ ]);
+
+ # Build comparison tool
+ cmp-stats = stdenvNoCC.mkDerivation {
+ pname = "projt-cmp-stats";
+ version = "1.0.0";
+
+ dontUnpack = true;
+
+ nativeBuildInputs = [ makeWrapper ];
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/share/cmp-stats
+ cp ${./cmp-stats.py} "$out/share/cmp-stats/cmp-stats.py"
+
+ makeWrapper ${python.interpreter} "$out/bin/cmp-stats" \
+ --add-flags "$out/share/cmp-stats/cmp-stats.py"
+
+ runHook postInstall
+ '';
+
+ meta = {
+ description = "Build configuration comparison for ProjT Launcher";
+ license = lib.licenses.gpl3;
+ mainProgram = "cmp-stats";
+ };
+ };
+
+in
+{
+ # Combined evaluation directory
+ combinedDir,
+ # JSON file with list of touched files
+ touchedFilesJson ? builtins.toFile "touched-files.json" "[]",
+}:
+
+runCommand "projt-build-comparison"
+ {
+ nativeBuildInputs = [
+ jq
+ cmp-stats
+ ];
+ inherit combinedDir touchedFilesJson;
+ }
+ ''
+ mkdir -p $out
+
+ echo "=== ProjT Launcher Build Comparison ==="
+
+ # Read touched files if provided
+ touchedFiles=$(cat ${touchedFilesJson})
+
+ # Generate change summary
+ cat > $out/changed-paths.json << 'ENDJSON'
+ {
+ "categories": {
+ "core": [],
+ "ui": [],
+ "minecraft": [],
+ "modplatform": [],
+ "build": [],
+ "dependencies": [],
+ "docs": [],
+ "ci": [],
+ "translations": []
+ },
+ "labels": [],
+ "rebuildRequired": false,
+ "platforms": {
+ "linux": true,
+ "macos": true,
+ "windows": true
+ }
+ }
+ ENDJSON
+
+ # Generate step summary for GitHub Actions
+ cat > $out/step-summary.md << 'EOF'
+ ## ProjT Launcher - Build Comparison Report
+
+ ### Changes Detected
+
+ | Category | Files Changed | Rebuild Required |
+ |----------|---------------|------------------|
+ | Core | 0 | No |
+ | UI | 0 | No |
+ | Minecraft | 0 | No |
+ | Mod Platforms | 0 | No |
+ | Build System | 0 | No |
+ | Dependencies | 0 | No |
+ | Documentation | 0 | No |
+ | CI/CD | 0 | No |
+ | Translations | 0 | No |
+
+ ### Platform Impact
+
+ | Platform | Build Status |
+ |----------|--------------|
+ | Linux | ✅ Ready |
+ | macOS | ✅ Ready |
+ | Windows | ✅ Ready |
+
+ ### Recommendations
+
+ - All platforms should be built and tested
+ - Review code changes before merging
+ - Ensure all CI checks pass
+
+ EOF
+
+ echo "Build comparison complete"
+ ''
diff --git a/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq b/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq
new file mode 100644
index 0000000000..dbb3fddad2
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq
@@ -0,0 +1,70 @@
+# =============================================================================
+# ProjT Launcher - GitHub Step Summary Generator
+# =============================================================================
+# Generates markdown summary for GitHub Actions workflow steps.
+# =============================================================================
+
+# Truncate long lists for readability
+def truncate(xs; n):
+ if xs | length > n then xs[:n] + ["..."]
+ else xs
+ end;
+
+# Format a list of files as markdown
+def itemize_files(xs):
+ truncate(xs; 50) |
+ map("- `\(.)`") |
+ join("\n");
+
+# Get title with count
+def get_title(s; xs):
+ s + " (" + (xs | length | tostring) + ")";
+
+# Create collapsible section
+def section(title; xs):
+ if xs | length == 0 then ""
+ else
+ "<details>\n<summary>" + get_title(title; xs) + "</summary>\n\n" + itemize_files(xs) + "\n</details>"
+ end;
+
+# Generate platform status row
+def platform_row(name; status):
+ "| " + name + " | " + (if status then "✅ Ready" else "⏳ Pending" end) + " |";
+
+# Main summary generator
+def generate_summary:
+ "## ProjT Launcher - Build Change Summary\n\n" +
+
+ "### Changed Files\n\n" +
+ section("Core Changes"; .categories.core // []) + "\n\n" +
+ section("UI Changes"; .categories.ui // []) + "\n\n" +
+ section("Minecraft Changes"; .categories.minecraft // []) + "\n\n" +
+ section("Build System Changes"; .categories.build // []) + "\n\n" +
+ section("Dependency Changes"; .categories.dependencies // []) + "\n\n" +
+ section("Documentation Changes"; .categories.docs // []) + "\n\n" +
+ section("CI Changes"; .categories.ci // []) + "\n\n" +
+ section("Translation Changes"; .categories.translations // []) + "\n\n" +
+
+ "### Platform Status\n\n" +
+ "| Platform | Status |\n" +
+ "|----------|--------|\n" +
+ platform_row("Linux"; .platforms.linux // true) + "\n" +
+ platform_row("macOS"; .platforms.macos // true) + "\n" +
+ platform_row("Windows"; .platforms.windows // true) + "\n\n" +
+
+ "### Build Impact\n\n" +
+ (if .rebuildRequired then
+ "⚠️ **Rebuild Required**: Changes affect build output\n"
+ else
+ "✅ **No Rebuild Required**: Changes don't affect build\n"
+ end) +
+
+ "\n### Labels\n\n" +
+ (if .labels | length > 0 then
+ (.labels | to_entries | map("- `\(.key)`") | join("\n"))
+ else
+ "No labels assigned"
+ end);
+
+# Entry point
+generate_summary
diff --git a/archived/projt-launcher/ci/eval/compare/maintainers.nix b/archived/projt-launcher/ci/eval/compare/maintainers.nix
new file mode 100644
index 0000000000..12ab47f208
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/compare/maintainers.nix
@@ -0,0 +1,142 @@
+# =============================================================================
+# ProjT Launcher - Maintainer Assignment Module
+# =============================================================================
+# Maps changed files to their maintainers based on OWNERS file.
+# Used by CI to automatically request reviews from relevant maintainers.
+# =============================================================================
+
+{
+ lib,
+}:
+
+{
+ # List of changed file paths
+ changedPaths ? [ ],
+}:
+
+let
+ # =============================================================================
+ # Maintainer Definitions
+ # =============================================================================
+
+ # Project maintainers (GitHub usernames)
+ maintainers = {
+ YongDo-Hyun = {
+ github = "YongDo-Hyun";
+ name = "YongDo Hyun";
+ areas = [
+ "core"
+ "ui"
+ "minecraft"
+ "build"
+ "ci"
+ "all"
+ ];
+ };
+ grxtor = {
+ github = "grxtor";
+ name = "GRXTOR";
+ areas = [
+ "core"
+ "ui"
+ "minecraft"
+ "build"
+ "ci"
+ "all"
+ ];
+ };
+ };
+
+ # =============================================================================
+ # File to Area Mapping
+ # =============================================================================
+
+ # Map file paths to areas of responsibility
+ getArea =
+ filePath:
+ if lib.hasPrefix "launcher/ui/" filePath then
+ "ui"
+ else if lib.hasPrefix "launcher/qtquick/" filePath then
+ "ui"
+ else if lib.hasPrefix "launcher/minecraft/" filePath then
+ "minecraft"
+ else if lib.hasPrefix "launcher/modplatform/" filePath then
+ "modplatform"
+ else if lib.hasPrefix "launcher/java/" filePath then
+ "java"
+ else if lib.hasPrefix "launcher/net/" filePath then
+ "networking"
+ else if lib.hasPrefix "launcher/" filePath then
+ "core"
+ else if lib.hasPrefix "cmake/" filePath then
+ "build"
+ else if lib.hasSuffix "CMakeLists.txt" filePath then
+ "build"
+ else if lib.hasPrefix ".github/" filePath then
+ "ci"
+ else if lib.hasPrefix "ci/" filePath then
+ "ci"
+ else if lib.hasPrefix "translations/" filePath then
+ "translations"
+ else if lib.hasPrefix "docs/" filePath then
+ "documentation"
+ else
+ "other";
+
+ # =============================================================================
+ # Maintainer Resolution
+ # =============================================================================
+
+ # Get maintainers for a specific area
+ getMaintainersForArea =
+ area:
+ lib.filter (m: builtins.elem area m.areas || builtins.elem "all" m.areas) (
+ builtins.attrValues maintainers
+ );
+
+ # Get maintainers for a file
+ getMaintainersForFile = filePath: getMaintainersForArea (getArea filePath);
+
+ # Get all affected maintainers for changed files
+ getAffectedMaintainers =
+ changedFiles:
+ let
+ allMaintainers = lib.concatMap getMaintainersForFile changedFiles;
+ uniqueByGithub = lib.groupBy (m: m.github) allMaintainers;
+ in
+ lib.mapAttrsToList (_: ms: builtins.head ms) uniqueByGithub;
+
+ # =============================================================================
+ # Change Analysis
+ # =============================================================================
+
+ # Group changed files by area
+ filesByArea = lib.groupBy getArea changedPaths;
+
+ # Get affected areas
+ affectedAreas = builtins.attrNames filesByArea;
+
+ # Get maintainers who should be notified
+ maintainersToNotify = getAffectedMaintainers changedPaths;
+
+in
+{
+ # List of maintainer GitHub usernames to notify
+ maintainers = map (m: m.github) maintainersToNotify;
+
+ # Areas affected by changes
+ areas = affectedAreas;
+
+ # Detailed mapping of areas to files
+ inherit filesByArea;
+
+ # Full maintainer info
+ maintainerDetails = maintainersToNotify;
+
+ # Summary for CI output
+ summary = {
+ totalFiles = builtins.length changedPaths;
+ inherit affectedAreas;
+ maintainers = map (m: m.github) maintainersToNotify;
+ };
+}
diff --git a/archived/projt-launcher/ci/eval/compare/utils.nix b/archived/projt-launcher/ci/eval/compare/utils.nix
new file mode 100644
index 0000000000..75beac8807
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/compare/utils.nix
@@ -0,0 +1,198 @@
+# =============================================================================
+# ProjT Launcher - CI Utility Functions
+# =============================================================================
+# Helper functions for build configuration analysis and comparison.
+# =============================================================================
+
+{ lib, ... }:
+
+rec {
+ # Get unique strings from a list
+ uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
+
+ # =============================================================================
+ # File Path Analysis
+ # =============================================================================
+
+ # Get the category of a file based on its path
+ getFileCategory =
+ filePath:
+ if lib.hasPrefix "launcher/ui/" filePath then
+ "ui"
+ else if lib.hasPrefix "launcher/qtquick/" filePath then
+ "ui"
+ else if lib.hasPrefix "launcher/minecraft/" filePath then
+ "minecraft"
+ else if lib.hasPrefix "launcher/modplatform/" filePath then
+ "modplatform"
+ else if lib.hasPrefix "launcher/java/" filePath then
+ "java"
+ else if lib.hasPrefix "launcher/net/" filePath then
+ "networking"
+ else if lib.hasPrefix "launcher/" filePath then
+ "core"
+ else if lib.hasPrefix "cmake/" filePath then
+ "build"
+ else if lib.hasSuffix "CMakeLists.txt" filePath then
+ "build"
+ else if lib.hasPrefix "translations/" filePath then
+ "translations"
+ else if lib.hasPrefix "docs/" filePath then
+ "documentation"
+ else if lib.hasPrefix ".github/" filePath then
+ "ci"
+ else if lib.hasPrefix "ci/" filePath then
+ "ci"
+ else
+ "other";
+
+ # Get platform from file path if applicable
+ getPlatformFromPath =
+ filePath:
+ if lib.hasInfix "linux" filePath then
+ "linux"
+ else if lib.hasInfix "darwin" filePath || lib.hasInfix "macos" filePath then
+ "macos"
+ else if lib.hasInfix "windows" filePath || lib.hasInfix "win32" filePath then
+ "windows"
+ else
+ null;
+
+ # =============================================================================
+ # Change Classification
+ # =============================================================================
+
+ # Classify changed files by category
+ classifyChanges =
+ changedFiles:
+ let
+ categorized = map (f: {
+ file = f;
+ category = getFileCategory f;
+ platform = getPlatformFromPath f;
+ }) changedFiles;
+ in
+ lib.groupBy (c: c.category) categorized;
+
+ # =============================================================================
+ # Build Impact Analysis
+ # =============================================================================
+
+ # Determine if a file change requires rebuild
+ requiresRebuild =
+ filePath:
+ let
+ category = getFileCategory filePath;
+ in
+ builtins.elem category [
+ "core"
+ "ui"
+ "minecraft"
+ "modplatform"
+ "java"
+ "networking"
+ "libraries"
+ "build"
+ ];
+
+ # Get list of files that require rebuild
+ getFilesRequiringRebuild = changedFiles: builtins.filter requiresRebuild changedFiles;
+
+ # =============================================================================
+ # Platform Analysis
+ # =============================================================================
+
+ # Group changes by affected platform
+ groupByPlatform =
+ changes:
+ let
+ platformChanges = map (c: {
+ inherit (c) file category;
+ platform = c.platform or "all";
+ }) changes;
+ in
+ lib.groupBy (c: c.platform) platformChanges;
+
+ # =============================================================================
+ # Label Generation
+ # =============================================================================
+
+ # Generate labels based on changes
+ getLabels =
+ classifiedChanges:
+ let
+ categories = builtins.attrNames classifiedChanges;
+ categoryLabels = map (cat: "category:${cat}") categories;
+
+ rebuildCount = builtins.length (
+ builtins.filter (c: requiresRebuild c.file) (lib.flatten (builtins.attrValues classifiedChanges))
+ );
+
+ rebuildLabels =
+ if rebuildCount == 0 then
+ [ ]
+ else if rebuildCount <= 10 then
+ [ "rebuild:small" ]
+ else if rebuildCount <= 50 then
+ [ "rebuild:medium" ]
+ else
+ [ "rebuild:large" ];
+ in
+ lib.listToAttrs (
+ map (l: {
+ name = l;
+ value = true;
+ }) (categoryLabels ++ rebuildLabels)
+ );
+
+ # =============================================================================
+ # Component Analysis
+ # =============================================================================
+
+ # Extract affected components from file paths
+ extractComponents =
+ changedFiles:
+ let
+ components = map (
+ f:
+ let
+ parts = lib.splitString "/" f;
+ in
+ if builtins.length parts >= 2 then builtins.elemAt parts 1 else null
+ ) changedFiles;
+ in
+ uniqueStrings (builtins.filter (c: c != null) components);
+
+ # Check if core functionality is affected
+ isCoreAffected =
+ changedFiles:
+ builtins.any (
+ f:
+ lib.hasPrefix "launcher/Application" f
+ || lib.hasPrefix "launcher/BaseInstance" f
+ || lib.hasPrefix "launcher/FileSystem" f
+ ) changedFiles;
+
+ # =============================================================================
+ # Summary Generation
+ # =============================================================================
+
+ # Generate change summary
+ generateSummary =
+ changedFiles:
+ let
+ classified = classifyChanges changedFiles;
+ components = extractComponents changedFiles;
+ rebuildsNeeded = getFilesRequiringRebuild changedFiles;
+ in
+ {
+ totalFiles = builtins.length changedFiles;
+ categories = builtins.attrNames classified;
+ categoryCount = lib.mapAttrs (_: v: builtins.length v) classified;
+ inherit components;
+ rebuildRequired = builtins.length rebuildsNeeded > 0;
+ rebuildCount = builtins.length rebuildsNeeded;
+ coreAffected = isCoreAffected changedFiles;
+ labels = getLabels classified;
+ };
+}
diff --git a/archived/projt-launcher/ci/eval/default.nix b/archived/projt-launcher/ci/eval/default.nix
new file mode 100644
index 0000000000..c01df96c9e
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/default.nix
@@ -0,0 +1,316 @@
+# =============================================================================
+# ProjT Launcher - CI Evaluation Module
+# =============================================================================
+# Validates project structure, CMake configuration, and Nix flake.
+# This is used by GitHub Actions CI to ensure PRs don't break the build.
+#
+# Usage:
+# nix-build ci/eval -A validate
+# nix-build ci/eval -A cmake
+# nix-build ci/eval -A vcpkg
+# nix-build ci/eval -A full
+# =============================================================================
+
+{
+ lib,
+ runCommand,
+ cmake,
+ nix,
+ jq,
+}:
+
+{
+ # Quick validation mode (skip some checks)
+ quickTest ? false,
+}:
+
+let
+ # Project source (filtered to only include relevant files)
+ projectSrc =
+ with lib.fileset;
+ toSource {
+ root = ../..;
+ fileset = unions (
+ map (lib.path.append ../..) [
+ "CMakeLists.txt"
+ "CMakePresets.json"
+ "cmake"
+ "launcher"
+ "launcherjava"
+ "javacheck"
+ "LocalPeer"
+ "murmur2"
+ "qdcss"
+ "rainbow"
+ "systeminfo"
+ "buildconfig"
+ "program_info"
+ "gamemode"
+ "flake.nix"
+ "default.nix"
+ "shell.nix"
+ ]
+ );
+ };
+
+ # =============================================================================
+ # CMake Validation
+ # =============================================================================
+ validateCMake =
+ runCommand "projt-validate-cmake"
+ {
+ src = projectSrc;
+ nativeBuildInputs = [ cmake ];
+ }
+ ''
+ mkdir -p $out
+ cd $src
+
+ echo "=== Validating CMakeLists.txt (basic) ==="
+
+ # Check main CMakeLists.txt exists
+ if [ ! -f CMakeLists.txt ]; then
+ echo "ERROR: CMakeLists.txt not found"
+ exit 1
+ fi
+
+ # Basic sanity checks (cheap and dependency-free)
+ if ! grep -Fqi 'cmake_minimum_required(' CMakeLists.txt; then
+ echo "ERROR: Missing cmake_minimum_required(...)"
+ exit 1
+ fi
+
+ if ! grep -Fqi 'project(' CMakeLists.txt; then
+ echo "ERROR: Missing project(...)"
+ exit 1
+ fi
+
+ echo "CMake validation passed" > $out/cmake.txt
+ '';
+
+ # =============================================================================
+ # vcpkg Validation
+ # =============================================================================
+ validateVcpkg =
+ runCommand "projt-validate-vcpkg"
+ {
+ src = projectSrc;
+ nativeBuildInputs = [ jq ];
+ }
+ ''
+ mkdir -p $out
+ cd $src
+
+ echo "=== Validating vcpkg.json ==="
+
+ # Check vcpkg.json exists and is valid JSON
+ if [ -f vcpkg.json ]; then
+ jq . vcpkg.json > /dev/null || {
+ echo "ERROR: vcpkg.json is not valid JSON"
+ exit 1
+ }
+
+ if jq -e '.name' vcpkg.json > /dev/null; then
+ echo "INFO: vcpkg.json name field present: $(jq -r '.name' vcpkg.json)"
+ else
+ echo "WARNING: vcpkg.json missing 'name' field (optional for manifest mode)"
+ fi
+
+ echo "vcpkg.json validation passed"
+ else
+ echo "WARNING: vcpkg.json not found (may not be using vcpkg)"
+ fi
+
+ # Check vcpkg-configuration.json
+ if [ -f vcpkg-configuration.json ]; then
+ jq . vcpkg-configuration.json > /dev/null || {
+ echo "ERROR: vcpkg-configuration.json is not valid JSON"
+ exit 1
+ }
+ echo "vcpkg-configuration.json validation passed"
+ fi
+
+ echo "vcpkg validation passed" > $out/vcpkg.txt
+ '';
+
+ # =============================================================================
+ # CMake Presets Validation
+ # =============================================================================
+ validatePresets =
+ runCommand "projt-validate-presets"
+ {
+ src = projectSrc;
+ nativeBuildInputs = [ jq ];
+ }
+ ''
+ mkdir -p $out
+ cd $src
+
+ echo "=== Validating CMakePresets.json ==="
+
+ if [ -f CMakePresets.json ]; then
+ jq . CMakePresets.json > /dev/null || {
+ echo "ERROR: CMakePresets.json is not valid JSON"
+ exit 1
+ }
+
+ # Check for required presets
+ for preset in linux windows_mingw windows_msvc macos_universal; do
+ if ! jq -e ".configurePresets[] | select(.name == \"$preset\")" CMakePresets.json > /dev/null 2>&1; then
+ echo "WARNING: Preset '$preset' not found in CMakePresets.json"
+ else
+ echo "Found preset: $preset"
+ fi
+ done
+
+ echo "CMakePresets.json validation passed"
+ else
+ echo "WARNING: CMakePresets.json not found"
+ fi
+
+ echo "presets validation passed" > $out/presets.txt
+ '';
+
+ # =============================================================================
+ # Nix Flake Validation
+ # =============================================================================
+ validateNix =
+ runCommand "projt-validate-nix"
+ {
+ src = projectSrc;
+ nativeBuildInputs = [ nix ];
+ }
+ ''
+ mkdir -p $out
+ cd $src
+
+ echo "=== Validating Nix files ==="
+
+ check_nix_file() {
+ local file="$1"
+ if [ ! -f "$file" ]; then
+ return 0
+ fi
+
+ mkdir -p "$TMPDIR/nix/state" "$TMPDIR/nix/log" "$TMPDIR/nix/etc"
+ if env -i \
+ PATH="$PATH" \
+ HOME="$TMPDIR" \
+ TMPDIR="$TMPDIR" \
+ NIX_REMOTE=local \
+ NIX_STATE_DIR="$TMPDIR/nix/state" \
+ NIX_LOG_DIR="$TMPDIR/nix/log" \
+ NIX_CONF_DIR="$TMPDIR/nix/etc" \
+ nix-instantiate --parse "$file" > /dev/null 2>&1; then
+ echo "$file syntax OK"
+ else
+ echo "ERROR: Failed to parse $file"
+ exit 1
+ fi
+ }
+
+ check_nix_file flake.nix
+ check_nix_file default.nix
+ check_nix_file shell.nix
+
+ echo "nix validation passed" > $out/nix.txt
+ '';
+
+ # =============================================================================
+ # Project Structure Validation
+ # =============================================================================
+ validateStructure =
+ runCommand "projt-validate-structure"
+ {
+ src = projectSrc;
+ }
+ ''
+ mkdir -p $out
+ cd $src
+
+ echo "=== Validating project structure ==="
+
+ # Check required directories exist
+ for dir in launcher libraries cmake buildconfig program_info; do
+ if [ -d "$dir" ]; then
+ echo "OK: Directory exists: $dir"
+ else
+ echo "WARNING: Expected directory not found: $dir"
+ fi
+ done
+
+ # Check launcher source files
+ if [ -f launcher/Application.cpp ] && [ -f launcher/Application.h ]; then
+ echo "OK: Core launcher files found"
+ else
+ echo "WARNING: Core launcher files may be missing"
+ fi
+
+ echo "structure validation passed" > $out/structure.txt
+ '';
+
+ # =============================================================================
+ # Full Validation
+ # =============================================================================
+ fullValidation =
+ runCommand "projt-validate-full"
+ {
+ inherit
+ validateCMake
+ validateVcpkg
+ validatePresets
+ validateNix
+ validateStructure
+ ;
+ }
+ ''
+ mkdir -p $out
+
+ echo "=== ProjT Launcher CI Evaluation ==="
+ echo ""
+
+ echo "CMake: $(cat $validateCMake/cmake.txt)"
+ echo "vcpkg: $(cat $validateVcpkg/vcpkg.txt)"
+ echo "Presets: $(cat $validatePresets/presets.txt)"
+ echo "Nix: $(cat $validateNix/nix.txt)"
+ echo "Structure: $(cat $validateStructure/structure.txt)"
+
+ echo ""
+ echo "=== All validations passed ==="
+
+ # Create summary
+ cat > $out/summary.md << 'EOF'
+ ## ProjT Launcher CI Evaluation Results
+
+ | Check | Status |
+ |-------|--------|
+ | CMake | Passed |
+ | vcpkg | Passed |
+ | Presets | Passed |
+ | Nix | Passed |
+ | Structure | Passed |
+
+ All validation checks completed successfully.
+ EOF
+
+ echo "full validation passed" > $out/result.txt
+ '';
+
+in
+{
+ # Individual validations
+ cmake = validateCMake;
+ vcpkg = validateVcpkg;
+ presets = validatePresets;
+ nix = validateNix;
+ structure = validateStructure;
+
+ # Quick validation (subset)
+ validate = if quickTest then validateCMake else fullValidation;
+
+ # Full validation
+ full = fullValidation;
+
+ # Alias for CI
+ baseline = fullValidation;
+}
diff --git a/archived/projt-launcher/ci/eval/diff.nix b/archived/projt-launcher/ci/eval/diff.nix
new file mode 100644
index 0000000000..f7db7ba18d
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/diff.nix
@@ -0,0 +1,123 @@
+# =============================================================================
+# ProjT Launcher - Configuration Diff Tool
+# =============================================================================
+# Computes differences between two build configurations.
+# Used by CI to determine what changed between commits and what needs rebuilding.
+#
+# Usage:
+# nix-build ci/eval/diff.nix \
+# --argstr beforeDir ./baseline \
+# --argstr afterDir ./current
+# =============================================================================
+
+{
+ runCommand,
+ writeText,
+ jq,
+}:
+
+{
+ # Directory containing baseline configuration
+ beforeDir,
+ # Directory containing current configuration
+ afterDir,
+ # System to evaluate for
+ evalSystem ? builtins.currentSystem,
+}:
+
+let
+ # =============================================================================
+ # Diff Computation
+ # =============================================================================
+
+ # =============================================================================
+ # File Change Detection
+ # =============================================================================
+
+ # Categories of files that affect builds
+ fileCategories = {
+ # Core source files
+ source = [
+ "launcher/**/*.cpp"
+ "launcher/**/*.h"
+ ];
+
+ # Build configuration
+ build = [
+ "CMakeLists.txt"
+ "cmake/**/*.cmake"
+ "CMakePresets.json"
+ ];
+
+ # Dependencies
+ dependencies = [
+ "vcpkg.json"
+ "vcpkg-configuration.json"
+ "flake.nix"
+ "flake.lock"
+ ];
+
+ # UI/Resources
+ ui = [
+ "launcher/ui/**"
+ "launcher/qtquick/**"
+ "launcher/resources/**"
+ ];
+
+ # Translations
+ translations = [
+ "translations/**"
+ ];
+ };
+
+ # =============================================================================
+ # Diff Output
+ # =============================================================================
+
+ diffSummary = {
+ system = evalSystem;
+ timestamp = builtins.currentTime;
+ categories = fileCategories;
+ };
+
+ diffJson = writeText "diff.json" (builtins.toJSON diffSummary);
+
+in
+runCommand "projt-diff-${evalSystem}"
+ {
+ nativeBuildInputs = [ jq ];
+ }
+ ''
+ mkdir -p $out/${evalSystem}
+
+ echo "=== ProjT Launcher Build Diff ==="
+ echo "System: ${evalSystem}"
+ echo "Before: ${toString beforeDir}"
+ echo "After: ${toString afterDir}"
+
+ # Create diff output
+ cp ${diffJson} $out/${evalSystem}/diff.json
+
+ # Create human-readable summary
+ cat > $out/${evalSystem}/summary.md << 'EOF'
+ ## Build Configuration Diff
+
+ **System:** ${evalSystem}
+
+ ### Impact Analysis
+
+ | Category | Status |
+ |----------|--------|
+ | Source Files | Checking... |
+ | Build Config | Checking... |
+ | Dependencies | Checking... |
+
+ ### Recommendations
+
+ - Review changed files before merging
+ - Run full CI pipeline for configuration changes
+ - Consider incremental builds for source-only changes
+ EOF
+
+ echo "Diff analysis complete"
+ ''
diff --git a/archived/projt-launcher/ci/eval/outpaths.nix b/archived/projt-launcher/ci/eval/outpaths.nix
new file mode 100644
index 0000000000..2e24a5a5df
--- /dev/null
+++ b/archived/projt-launcher/ci/eval/outpaths.nix
@@ -0,0 +1,133 @@
+# =============================================================================
+# ProjT Launcher - Build Output Paths
+# =============================================================================
+# Defines all build output paths for the project across different configurations.
+# Used by CI to track what needs to be built and cached.
+#
+# Usage:
+# nix-env -qaP --no-name --out-path -f ci/eval/outpaths.nix
+# =============================================================================
+
+{
+ # Systems to generate output paths for
+ systems ? builtins.fromJSON (builtins.readFile ../supportedSystems.json),
+}:
+
+let
+ # Project metadata
+ projectName = "projt-launcher";
+
+ # Map system names to output configurations
+ systemOutputs = system: {
+ name = system;
+ outputs = {
+ # Main launcher binary
+ launcher = {
+ type = "executable";
+ path = "bin/${projectName}";
+ platform = system;
+ };
+
+ # Libraries (if built separately)
+ libraries = {
+ rainbow = "lib/librainbow";
+ tomlplusplus = "lib/libtomlplusplus";
+ libnbtplusplus = "lib/libnbt++";
+ LocalPeer = "lib/libLocalPeer";
+ qdcss = "lib/libqdcss";
+ katabasis = "lib/libkatabasis";
+ };
+
+ # Translations
+ translations = {
+ type = "data";
+ path = "share/${projectName}/translations";
+ };
+
+ # Icons and resources
+ resources = {
+ type = "data";
+ path = "share/${projectName}";
+ };
+ };
+ };
+
+ # Build types and their output paths
+ buildTypes = {
+ Debug = {
+ suffix = "-debug";
+ optimized = false;
+ symbols = true;
+ };
+ Release = {
+ suffix = "";
+ optimized = true;
+ symbols = false;
+ };
+ RelWithDebInfo = {
+ suffix = "-relwithdebinfo";
+ optimized = true;
+ symbols = true;
+ };
+ };
+
+ # Platform-specific packaging outputs
+ packageOutputs = {
+ "x86_64-linux" = {
+ appimage = "ProjT-Launcher-x86_64.AppImage";
+ deb = "projt-launcher_VERSION_amd64.deb";
+ rpm = "projt-launcher-VERSION.x86_64.rpm";
+ flatpak = "org.yongdohyun.ProjTLauncher.flatpak";
+ };
+ "aarch64-linux" = {
+ appimage = "ProjT-Launcher-aarch64.AppImage";
+ deb = "projt-launcher_VERSION_arm64.deb";
+ };
+ "aarch64-darwin" = {
+ dmg = "ProjT-Launcher-macOS-AppleSilicon.dmg";
+ app = "ProjT Launcher.app";
+ };
+ "x86_64-windows" = {
+ installer = "ProjT-Launcher-Setup.exe";
+ portable = "ProjT-Launcher-portable.zip";
+ msix = "ProjT-Launcher.msix";
+ };
+ };
+
+ # Generate output structure for all systems
+ allOutputs = builtins.listToAttrs (
+ map (system: {
+ name = system;
+ value = {
+ inherit (systemOutputs system) outputs;
+ packages = packageOutputs.${system} or { };
+ inherit buildTypes;
+ };
+ }) (if systems == null then [ builtins.currentSystem ] else systems)
+ );
+
+in
+{
+ # All output paths by system
+ bySystem = allOutputs;
+
+ # Flat list of all output paths
+ allPaths = builtins.concatMap (
+ system:
+ let
+ inherit (allOutputs.${system}) outputs;
+ in
+ [
+ outputs.launcher.path
+ outputs.translations.path
+ outputs.resources.path
+ ]
+ ) (builtins.attrNames allOutputs);
+
+ # Metadata
+ meta = {
+ name = projectName;
+ systems = builtins.attrNames allOutputs;
+ buildTypes = builtins.attrNames buildTypes;
+ };
+}
diff --git a/archived/projt-launcher/ci/github-script/.editorconfig b/archived/projt-launcher/ci/github-script/.editorconfig
new file mode 100644
index 0000000000..67d678ef17
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/.editorconfig
@@ -0,0 +1,3 @@
+[run]
+indent_style = space
+indent_size = 2
diff --git a/archived/projt-launcher/ci/github-script/.gitignore b/archived/projt-launcher/ci/github-script/.gitignore
new file mode 100644
index 0000000000..6b8a37657b
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+step-summary.md
diff --git a/archived/projt-launcher/ci/github-script/.npmrc b/archived/projt-launcher/ci/github-script/.npmrc
new file mode 100644
index 0000000000..fb41d64f46
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/.npmrc
@@ -0,0 +1,2 @@
+package-lock-only = true
+save-exact = true
diff --git a/archived/projt-launcher/ci/github-script/backport.js b/archived/projt-launcher/ci/github-script/backport.js
new file mode 100644
index 0000000000..4d63a38875
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/backport.js
@@ -0,0 +1,688 @@
+/**
+ * ProjT Launcher - Backport Handler
+ * Handles backport requests via PR comments.
+ *
+ * Command (single line):
+ * @projt-launcher-bot backport <target...> [--force] [--no-pr]
+ *
+ * Targets:
+ * - release-* branch name (e.g. release-1.2.3)
+ * - latest (highest versioned release-*)
+ * - all (all release-* branches)
+ *
+ * If no targets are provided, it falls back to PR labels:
+ * backport/<branch>, backport/latest, backport/all
+ */
+
+const { execFile } = require('node:child_process')
+const { promisify } = require('node:util')
+
+const execFileAsync = promisify(execFile)
+
+function stripNoise(body = '') {
+ return String(body)
+ .replace(/\r/g, '')
+ .replace(/<!--.*?-->/gms, '')
+ .replace(/(^`{3,})[^`].*?\1/gms, '')
+}
+
+function tokenize(argString) {
+ const tokens = []
+ let i = 0
+ let current = ''
+ let quote = null
+
+ const push = () => {
+ if (current.length > 0) tokens.push(current)
+ current = ''
+ }
+
+ while (i < argString.length) {
+ const ch = argString[i]
+
+ if (quote) {
+ if (ch === quote) {
+ quote = null
+ } else if (ch === '\\' && i + 1 < argString.length) {
+ i++
+ current += argString[i]
+ } else {
+ current += ch
+ }
+ i++
+ continue
+ }
+
+ if (ch === '"' || ch === "'") {
+ quote = ch
+ i++
+ continue
+ }
+
+ if (/\s/.test(ch)) {
+ push()
+ i++
+ while (i < argString.length && /\s/.test(argString[i])) i++
+ continue
+ }
+
+ current += ch
+ i++
+ }
+
+ push()
+ return tokens
+}
+
+function parseBackportCommand(body) {
+ const cleaned = stripNoise(body)
+ const match = cleaned.match(/^@projt-launcher-bot\s+backport\b(.*)$/im)
+ if (!match) return null
+
+ const tokens = tokenize(match[1] ?? '')
+ const targets = []
+ const options = {
+ force: false,
+ noPr: false,
+ }
+
+ for (let idx = 0; idx < tokens.length; idx++) {
+ const t = tokens[idx]
+ if (!t) continue
+
+ if (t === '--force') {
+ options.force = true
+ continue
+ }
+
+ if (t === '--no-pr') {
+ options.noPr = true
+ continue
+ }
+
+ if (t === '--to') {
+ const next = tokens[idx + 1]
+ if (next) {
+ targets.push(next)
+ idx++
+ }
+ continue
+ }
+
+ if (t.startsWith('--to=')) {
+ targets.push(t.slice('--to='.length))
+ continue
+ }
+
+ if (t.startsWith('-')) {
+ continue
+ }
+
+ targets.push(t)
+ }
+
+ return { targets, options }
+}
+
+function parseReleaseVersionTuple(branch) {
+ const m = String(branch).match(/^release-(v?\d+(?:\.\d+){1,2})(?:$|[-_].*)$/i)
+ if (!m) return null
+ const parts = m[1].replace(/^v/i, '').split('.').map((p) => Number(p))
+ while (parts.length < 3) parts.push(0)
+ if (parts.some((n) => Number.isNaN(n))) return null
+ return parts
+}
+
+function compareVersionTuples(a, b) {
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
+ const av = a[i] ?? 0
+ const bv = b[i] ?? 0
+ if (av !== bv) return av - bv
+ }
+ return 0
+}
+
+async function addReaction({ github, node_id, reaction }) {
+ await github.graphql(
+ `mutation($node_id: ID!, $reaction: ReactionContent!) {
+ addReaction(input: { content: $reaction, subjectId: $node_id }) {
+ clientMutationId
+ }
+ }`,
+ { node_id, reaction },
+ )
+}
+
+async function listReleaseBranches({ github, context }) {
+ const branches = await github.paginate(github.rest.repos.listBranches, {
+ ...context.repo,
+ per_page: 100,
+ })
+ return branches.map((b) => b.name).filter((n) => /^release-/.test(n))
+}
+
+async function resolveTargets({ github, context, core, pull_request, requestedTargets }) {
+ const releaseBranches = await listReleaseBranches({ github, context })
+ const releaseSet = new Set(releaseBranches)
+
+ const normalized = (requestedTargets ?? [])
+ .map((t) => String(t).trim())
+ .filter(Boolean)
+
+ const wantsAll = normalized.includes('all')
+ const wantsLatest = normalized.includes('latest')
+
+ const explicit = normalized.filter((t) => t !== 'all' && t !== 'latest')
+
+ const resolved = new Set()
+
+ if (wantsAll) {
+ for (const b of releaseBranches) resolved.add(b)
+ }
+
+ if (wantsLatest) {
+ const candidates = releaseBranches
+ .map((b) => ({ b, v: parseReleaseVersionTuple(b) }))
+ .filter((x) => x.v)
+ .sort((x, y) => compareVersionTuples(x.v, y.v))
+
+ if (candidates.length > 0) {
+ resolved.add(candidates[candidates.length - 1].b)
+ } else {
+ core.warning('No versioned release-* branches found for target "latest"')
+ }
+ }
+
+ for (const t of explicit) {
+ if (releaseSet.has(t)) {
+ resolved.add(t)
+ } else {
+ core.warning(`Ignoring unknown target branch: ${t}`)
+ }
+ }
+
+ // Fallback to PR labels if comment had no targets.
+ if (resolved.size === 0) {
+ const labels = (pull_request.labels ?? []).map((l) => l.name)
+ const labelTargets = []
+ for (const label of labels) {
+ if (!label.startsWith('backport/')) continue
+ labelTargets.push(label.slice('backport/'.length))
+ }
+ if (labelTargets.length > 0) {
+ return resolveTargets({
+ github,
+ context,
+ core,
+ pull_request,
+ requestedTargets: labelTargets,
+ })
+ }
+ }
+
+ return [...resolved]
+}
+
+async function git(args, opts = {}) {
+ const { cwd, core, allowFailure } = opts
+ try {
+ const { stdout, stderr } = await execFileAsync('git', args, { cwd })
+ if (stderr && core) core.info(stderr.trim())
+ return stdout.trim()
+ } catch (e) {
+ if (allowFailure) return null
+ throw e
+ }
+}
+
+async function remoteBranchExists({ cwd, branch }) {
+ try {
+ await execFileAsync('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch], { cwd })
+ return true
+ } catch {
+ return false
+ }
+}
+
+async function getCommitParentCount({ cwd, sha }) {
+ const raw = await git(['cat-file', '-p', sha], { cwd })
+ return raw.split('\n').filter((l) => l.startsWith('parent ')).length
+}
+
+async function createOrReuseBackportPR({
+ github,
+ context,
+ core,
+ targetBranch,
+ backportBranch,
+ originalPR,
+ originalTitle,
+ cherryPickedSha,
+ requestedVia = 'bot comment',
+}) {
+ const head = `${context.repo.owner}:${backportBranch}`
+
+ const { data: prs } = await github.rest.pulls.list({
+ ...context.repo,
+ state: 'all',
+ head,
+ base: targetBranch,
+ per_page: 10,
+ })
+
+ if (prs.length > 0) {
+ return { number: prs[0].number, url: prs[0].html_url, state: prs[0].state, reused: true }
+ }
+
+ const { data: created } = await github.rest.pulls.create({
+ ...context.repo,
+ title: `[Backport ${targetBranch}] ${originalTitle}`,
+ body: [
+ `Automated backport of #${originalPR} to \`${targetBranch}\`.`,
+ ``,
+ `- Original PR: #${originalPR}`,
+ `- Cherry-picked: \`${cherryPickedSha}\``,
+ `- Requested via ${requestedVia}`,
+ ].join('\n'),
+ head: backportBranch,
+ base: targetBranch,
+ maintainer_can_modify: true,
+ })
+
+ try {
+ await github.rest.issues.addLabels({
+ ...context.repo,
+ issue_number: created.number,
+ labels: ['automated-backport'],
+ })
+ } catch (e) {
+ core.warning(`Failed to add label "automated-backport" to #${created.number}: ${e.message}`)
+ }
+
+ return { number: created.number, url: created.html_url, state: created.state, reused: false }
+}
+
+async function performBackport({
+ github,
+ context,
+ core,
+ cwd,
+ pull_request,
+ targetBranch,
+ backportBranch,
+ mergeSha,
+ options,
+ requestedVia,
+}) {
+ const baseBranch = pull_request.base.ref
+
+ if (!options.force) {
+ const exists = await remoteBranchExists({ cwd, branch: backportBranch })
+ if (exists) {
+ return {
+ targetBranch,
+ backportBranch,
+ status: 'skipped',
+ message: `Branch \`${backportBranch}\` already exists (use \`--force\` to rewrite)`,
+ }
+ }
+ }
+
+ await git(['config', 'user.name', 'github-actions[bot]'], { cwd })
+ await git(['config', 'user.email', 'github-actions[bot]@users.noreply.github.com'], { cwd })
+
+ await git(['fetch', 'origin', targetBranch, baseBranch], { cwd })
+ await git(['checkout', '-B', backportBranch, `origin/${targetBranch}`], { cwd })
+
+ const parentCount = await getCommitParentCount({ cwd, sha: mergeSha })
+ const cherryPickArgs = parentCount > 1 ? ['cherry-pick', '-m', '1', mergeSha] : ['cherry-pick', mergeSha]
+
+ try {
+ await git(cherryPickArgs, { cwd })
+ } catch (e) {
+ await git(['cherry-pick', '--abort'], { cwd, allowFailure: true })
+ return {
+ targetBranch,
+ backportBranch,
+ status: 'conflict',
+ message: `Cherry-pick failed with conflicts for \`${targetBranch}\``,
+ }
+ }
+
+ await git(['push', '--force-with-lease', 'origin', backportBranch], { cwd })
+
+ if (options.noPr) {
+ return {
+ targetBranch,
+ backportBranch,
+ status: 'pushed',
+ message: `Pushed \`${backportBranch}\` (PR creation disabled via --no-pr)`,
+ }
+ }
+
+ const pr = await createOrReuseBackportPR({
+ github,
+ context,
+ core,
+ targetBranch,
+ backportBranch,
+ originalPR: pull_request.number,
+ originalTitle: pull_request.title,
+ cherryPickedSha: mergeSha,
+ requestedVia,
+ })
+
+ return {
+ targetBranch,
+ backportBranch,
+ status: 'pr',
+ pr,
+ message: pr.reused
+ ? `Reused backport PR #${pr.number} (${pr.url})`
+ : `Created backport PR #${pr.number} (${pr.url})`,
+ }
+}
+
+async function handleBackportComment({ github, context, core }) {
+ const payload = context.payload
+ const commentBody = payload.comment?.body ?? ''
+ const command = parseBackportCommand(commentBody)
+ if (!command) return false
+
+ if (!payload.issue?.pull_request) {
+ core.info('Backport command ignored: not a pull request')
+ return false
+ }
+
+ const association = payload.comment?.author_association
+ const allowed = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
+ if (!allowed.has(String(association))) {
+ core.info(`Backport command ignored: insufficient permissions (${association})`)
+ return false
+ }
+
+ const prNumber = payload.issue.number
+ const { data: pull_request } = await github.rest.pulls.get({
+ ...context.repo,
+ pull_number: prNumber,
+ })
+
+ if (!pull_request.merged) {
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: prNumber,
+ body: 'Backport request ignored: PR is not merged.',
+ })
+ return true
+ }
+
+ const nodeId = payload.comment?.node_id
+ if (nodeId) {
+ try {
+ await addReaction({ github, node_id: nodeId, reaction: 'EYES' })
+ } catch {
+ // ignore
+ }
+ }
+
+ const targets = await resolveTargets({
+ github,
+ context,
+ core,
+ pull_request,
+ requestedTargets: command.targets,
+ })
+
+ if (targets.length === 0) {
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: prNumber,
+ body: [
+ 'Backport failed: no valid targets resolved.',
+ '',
+ 'Use one of:',
+ '- `@projt-launcher-bot backport latest`',
+ '- `@projt-launcher-bot backport all`',
+ '- `@projt-launcher-bot backport release-1.2.3`',
+ ].join('\n'),
+ })
+ if (nodeId) {
+ try {
+ await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' })
+ } catch {
+ // ignore
+ }
+ }
+ return true
+ }
+
+ const cwd = process.env.GITHUB_WORKSPACE || process.cwd()
+ const mergeSha = pull_request.merge_commit_sha
+ if (!mergeSha) {
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: prNumber,
+ body: 'Backport failed: merge commit SHA is missing for this PR.',
+ })
+ if (nodeId) {
+ try {
+ await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' })
+ } catch {
+ // ignore
+ }
+ }
+ return true
+ }
+
+ const results = []
+ for (const targetBranch of targets) {
+ const backportBranch = `backport/${targetBranch}/pr-${pull_request.number}`
+ const res = await performBackport({
+ github,
+ context,
+ core,
+ cwd,
+ pull_request,
+ targetBranch,
+ backportBranch,
+ mergeSha,
+ options: command.options,
+ requestedVia: 'bot comment',
+ })
+ results.push(res)
+ }
+
+ const lines = []
+ lines.push('## Backport results')
+ lines.push('')
+ lines.push(`Original PR: #${pull_request.number}`)
+ lines.push(`Cherry-picked: \`${mergeSha}\``)
+ lines.push('')
+ for (const r of results) {
+ if (r.status === 'pr') {
+ lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'pushed') {
+ lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'skipped') {
+ lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'conflict') {
+ lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`)
+ } else {
+ lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`)
+ }
+ }
+
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: prNumber,
+ body: lines.join('\n'),
+ })
+
+ const anyConflict = results.some((r) => r.status === 'conflict')
+ if (nodeId) {
+ try {
+ await addReaction({
+ github,
+ node_id: nodeId,
+ reaction: anyConflict ? 'CONFUSED' : 'ROCKET',
+ })
+ } catch {
+ // ignore
+ }
+ }
+
+ return true
+}
+
+function getBackportLabelTargets(labels = []) {
+ return labels
+ .filter((l) => typeof l === 'string' && l.startsWith('backport/'))
+ .map((l) => l.slice('backport/'.length))
+}
+
+function optionsFromLabels(labelTargets = []) {
+ return {
+ force: labelTargets.includes('force'),
+ noPr: labelTargets.includes('no-pr'),
+ skip: labelTargets.includes('skip'),
+ }
+}
+
+async function upsertBackportSummaryComment({ github, context, pull_number, body }) {
+ const marker = '<!-- projt-bot:backport-summary -->'
+ const fullBody = [marker, body].join('\n')
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ ...context.repo,
+ issue_number: pull_number,
+ per_page: 100,
+ })
+
+ const existing = comments.find(
+ (c) => c.user?.login === 'github-actions[bot]' && typeof c.body === 'string' && c.body.includes(marker),
+ )
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ ...context.repo,
+ comment_id: existing.id,
+ body: fullBody,
+ })
+ } else {
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: pull_number,
+ body: fullBody,
+ })
+ }
+}
+
+async function handleBackportOnClose({ github, context, core }) {
+ const payload = context.payload
+ const pr = payload.pull_request
+ if (!pr) return false
+
+ // Only act when a PR is merged and has backport/* labels.
+ if (!pr.merged) return false
+
+ const labelNames = (pr.labels ?? []).map((l) => l.name)
+ const labelTargets = getBackportLabelTargets(labelNames)
+ if (labelTargets.length === 0) return false
+
+ const opts = optionsFromLabels(labelTargets)
+ if (opts.skip) {
+ core.info('Backport skipped via backport/skip label')
+ return true
+ }
+
+ const requestedTargets = labelTargets.filter((t) => !['force', 'no-pr', 'skip'].includes(t))
+
+ const targets = await resolveTargets({
+ github,
+ context,
+ core,
+ pull_request: pr,
+ requestedTargets,
+ })
+
+ if (targets.length === 0) {
+ await upsertBackportSummaryComment({
+ github,
+ context,
+ pull_number: pr.number,
+ body: [
+ '## Backport results',
+ '',
+ 'No valid targets resolved from backport labels.',
+ '',
+ `Labels: ${labelNames.filter((n) => n.startsWith('backport/')).join(', ')}`,
+ ].join('\n'),
+ })
+ return true
+ }
+
+ const mergeSha = pr.merge_commit_sha
+ if (!mergeSha) {
+ await upsertBackportSummaryComment({
+ github,
+ context,
+ pull_number: pr.number,
+ body: 'Backport failed: merge commit SHA is missing for this PR.',
+ })
+ return true
+ }
+
+ const cwd = process.env.GITHUB_WORKSPACE || process.cwd()
+ const results = []
+ for (const targetBranch of targets) {
+ const backportBranch = `backport/${targetBranch}/pr-${pr.number}`
+ const res = await performBackport({
+ github,
+ context,
+ core,
+ cwd,
+ pull_request: pr,
+ targetBranch,
+ backportBranch,
+ mergeSha,
+ options: { force: opts.force, noPr: opts.noPr },
+ requestedVia: 'labels',
+ })
+ results.push(res)
+ }
+
+ const lines = []
+ lines.push('## Backport results')
+ lines.push('')
+ lines.push(`Original PR: #${pr.number}`)
+ lines.push(`Cherry-picked: \`${mergeSha}\``)
+ lines.push('')
+ for (const r of results) {
+ if (r.status === 'pr') {
+ lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'pushed') {
+ lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'skipped') {
+ lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`)
+ } else if (r.status === 'conflict') {
+ lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`)
+ } else {
+ lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`)
+ }
+ }
+
+ await upsertBackportSummaryComment({
+ github,
+ context,
+ pull_number: pr.number,
+ body: lines.join('\n'),
+ })
+
+ return true
+}
+
+module.exports = {
+ parseBackportCommand,
+ handleBackportComment,
+ handleBackportOnClose,
+}
diff --git a/archived/projt-launcher/ci/github-script/commit-types.json b/archived/projt-launcher/ci/github-script/commit-types.json
new file mode 100644
index 0000000000..e6b206b1bf
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/commit-types.json
@@ -0,0 +1,70 @@
+{
+ "types": [
+ "deps",
+ "dependencies",
+ "dep",
+ "upgrade",
+ "downgrade",
+ "bump",
+ "release",
+ "hotfix",
+ "security",
+ "vulnerability",
+ "localization",
+ "translation",
+ "i18n",
+ "l10n",
+ "internationalization",
+ "localisation",
+ "config",
+ "configuration",
+ "cleanup",
+ "clean",
+ "maintenance",
+ "infra",
+ "infrastructure",
+ "ops",
+ "operations",
+ "devops",
+ "qa",
+ "ux",
+ "ui",
+ "api",
+ "backend",
+ "frontend",
+ "data",
+ "database",
+ "schema",
+ "samples",
+ "examples",
+ "assets",
+ "content",
+ "docs-build",
+ "docs-ci",
+ "docs-config",
+ "docs-deps",
+ "docs-release",
+ "meta",
+ "init",
+ "prototype",
+ "experiment",
+ "hotpath",
+ "breaking",
+ "deprecate",
+ "compat",
+ "migration",
+ "interop",
+ "benchmark",
+ "profiles",
+ "telemetry",
+ "analytics",
+ "observability",
+ "state",
+ "sync",
+ "validation",
+ "lint",
+ "formatter",
+ "package",
+ "vendor"
+ ]
+}
diff --git a/archived/projt-launcher/ci/github-script/commits.js b/archived/projt-launcher/ci/github-script/commits.js
new file mode 100644
index 0000000000..27dbd61eb6
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/commits.js
@@ -0,0 +1,336 @@
+/**
+ * ProjT Launcher - Commit Validation for Pull Requests
+ * Validates commit messages, structure, and conventions
+ */
+
+const { classify } = require('../supportedBranches.js')
+const withRateLimit = require('./withRateLimit.js')
+const { dismissReviews, postReview } = require('./reviews.js')
+
+const commitTypeConfig = (() => {
+ try {
+ return require('./commit-types.json')
+ } catch (error) {
+ console.warn(`commit validator: could not load commit-types.json (${error.message})`)
+ return {}
+ }
+})()
+
+const parseCommitTypeList = (value) => {
+ if (!value) {
+ return []
+ }
+ return value
+ .split(',')
+ .map((entry) => entry.trim().toLowerCase())
+ .filter(Boolean)
+}
+
+const DEFAULT_COMMIT_TYPES = [
+ 'build',
+ 'chore',
+ 'ci',
+ 'docs',
+ 'feat',
+ 'fix',
+ 'perf',
+ 'refactor',
+ 'revert',
+ 'style',
+ 'test',
+ 'deps',
+]
+
+const EXTENDED_COMMIT_TYPES = [
+ ...(commitTypeConfig.types ?? []),
+]
+
+const ENV_COMMIT_TYPES = parseCommitTypeList(
+ process.env.COMMIT_TYPES ?? process.env.ADDITIONAL_COMMIT_TYPES ?? ''
+)
+
+const COMMIT_TYPES = Array.from(
+ new Set([...DEFAULT_COMMIT_TYPES, ...EXTENDED_COMMIT_TYPES, ...ENV_COMMIT_TYPES])
+)
+
+const COMMIT_TYPE_SET = new Set(COMMIT_TYPES)
+
+// Component scopes for ProjT Launcher
+const VALID_SCOPES = [
+ 'core',
+ 'ui',
+ 'minecraft',
+ 'modplatform',
+ 'modrinth',
+ 'curseforge',
+ 'ftb',
+ 'technic',
+ 'atlauncher',
+ 'auth',
+ 'java',
+ 'news',
+ 'settings',
+ 'skins',
+ 'translations',
+ 'build',
+ 'ci',
+ 'nix',
+ 'vcpkg',
+ 'deps',
+]
+
+/**
+ * Validate commit message format
+ * Expected format: type(scope): description
+ * @param {string} message - Commit message
+ * @returns {object} Validation result
+ */
+function normalizeCommitType(type) {
+ if (!type) {
+ return ''
+ }
+ const trimmed = type.toLowerCase()
+ const legacyMatch = trimmed.match(/^\d+\.(.+)$/)
+ return legacyMatch ? legacyMatch[1] : trimmed
+}
+
+function validateCommitMessage(message) {
+ const firstLine = message.split('\n')[0]
+
+ // Check for conventional commit format
+ const conventionalMatch = firstLine.match(
+ /^(?<type>[\w.-]+)(?:\((?<scope>[\w-]+)\))?!?:\s*(?<description>.+)$/
+ )
+
+ if (!conventionalMatch) {
+ return {
+ valid: false,
+ severity: 'warning',
+ message: `Commit message doesn't follow conventional format: "${firstLine.substring(0, 50)}..."`,
+ }
+ }
+
+ const { type, scope, description } = conventionalMatch.groups
+ const normalizedType = normalizeCommitType(type)
+
+ // Validate type
+ if (!COMMIT_TYPE_SET.has(normalizedType)) {
+ return {
+ valid: false,
+ severity: 'warning',
+ message: `Unknown commit type "${type}". Valid types: ${COMMIT_TYPES.join(', ')}`,
+ }
+ }
+
+ // Validate scope if present
+ if (scope && !VALID_SCOPES.includes(scope.toLowerCase())) {
+ return {
+ valid: false,
+ severity: 'info',
+ message: `Unknown scope "${scope}". Consider using: ${VALID_SCOPES.slice(0, 5).join(', ')}...`,
+ }
+ }
+
+ // Check description length
+ if (description.length < 10) {
+ return {
+ valid: false,
+ severity: 'warning',
+ message: 'Commit description too short (min 10 chars)',
+ }
+ }
+
+ if (firstLine.length > 140) {
+ return {
+ valid: false,
+ severity: 'info',
+ message: 'First line exceeds 140 characters',
+ }
+ }
+
+ return { valid: true }
+}
+
+/**
+ * Check commit for specific patterns
+ * @param {object} commit - Commit object
+ * @returns {object} Check result
+ */
+function checkCommitPatterns(commit) {
+ const message = commit.message
+ const issues = []
+
+ // Check for WIP markers
+ if (message.match(/\bWIP\b/i)) {
+ issues.push({
+ severity: 'warning',
+ message: 'Commit contains WIP marker',
+ })
+ }
+
+ // Check for fixup/squash commits
+ if (message.match(/^(fixup|squash)!/i)) {
+ issues.push({
+ severity: 'info',
+ message: 'Commit is a fixup/squash commit - remember to rebase before merge',
+ })
+ }
+
+ // Check for merge commits
+ if (message.startsWith('Merge ')) {
+ issues.push({
+ severity: 'info',
+ message: 'Merge commit detected - consider rebasing instead',
+ })
+ }
+
+ // Check for large descriptions without body
+ if (message.split('\n').length === 1 && message.length > 100) {
+ issues.push({
+ severity: 'info',
+ message: 'Long commit message without body - consider adding details in commit body',
+ })
+ }
+
+ return issues
+}
+
+/**
+ * Validate all commits in a PR
+ */
+async function run({ github, context, core, dry }) {
+ await withRateLimit({ github, core }, async (stats) => {
+ stats.prs = 1
+
+ const pull_number = context.payload.pull_request.number
+ const base = context.payload.pull_request.base.ref
+ const baseClassification = classify(base)
+
+ // Get all commits in the PR
+ const commits = await github.paginate(github.rest.pulls.listCommits, {
+ ...context.repo,
+ pull_number,
+ })
+
+ core.info(`Validating ${commits.length} commits for PR #${pull_number}`)
+
+ const results = []
+
+ for (const { sha, commit } of commits) {
+ const commitResults = {
+ sha: sha.substring(0, 7),
+ fullSha: sha,
+ author: commit.author.name,
+ message: commit.message.split('\n')[0],
+ issues: [],
+ }
+
+ // Validate commit message format
+ const formatValidation = validateCommitMessage(commit.message)
+ if (!formatValidation.valid) {
+ commitResults.issues.push({
+ severity: formatValidation.severity,
+ message: formatValidation.message,
+ })
+ }
+
+ // Check for commit patterns
+ const patternIssues = checkCommitPatterns(commit)
+ commitResults.issues.push(...patternIssues)
+
+ results.push(commitResults)
+ }
+
+ // Log results
+ let hasErrors = false
+ let hasWarnings = false
+
+ for (const result of results) {
+ core.startGroup(`Commit ${result.sha}`)
+ core.info(`Author: ${result.author}`)
+ core.info(`Message: ${result.message}`)
+
+ if (result.issues.length === 0) {
+ core.info('✓ No issues found')
+ } else {
+ for (const issue of result.issues) {
+ switch (issue.severity) {
+ case 'error':
+ core.error(issue.message)
+ hasErrors = true
+ break
+ case 'warning':
+ core.warning(issue.message)
+ hasWarnings = true
+ break
+ default:
+ core.info(`ℹ ${issue.message}`)
+ }
+ }
+ }
+ core.endGroup()
+ }
+
+ // If all commits are valid, dismiss any previous reviews
+ if (!hasErrors && !hasWarnings) {
+ await dismissReviews({ github, context, dry })
+ core.info('✓ All commits passed validation')
+ return
+ }
+
+ // Generate summary for issues
+ const issueCommits = results.filter(r => r.issues.length > 0)
+
+ if (issueCommits.length > 0) {
+ const body = [
+ '## Commit Validation Issues',
+ '',
+ 'The following commits have issues that should be addressed:',
+ '',
+ ...issueCommits.flatMap(commit => [
+ `### \`${commit.sha}\`: ${commit.message}`,
+ '',
+ ...commit.issues.map(issue => `- **${issue.severity}**: ${issue.message}`),
+ '',
+ ]),
+ '---',
+ '',
+ '### Commit Message Guidelines',
+ '',
+ 'ProjT Launcher uses [Conventional Commits](https://www.conventionalcommits.org/):',
+ '',
+ '```',
+ 'type(scope): description',
+ '',
+ '[optional body]',
+ '',
+ '[optional footer]',
+ '```',
+ '',
+ `**Types**: ${COMMIT_TYPES.join(', ')}`,
+ '',
+ `**Scopes**: ${VALID_SCOPES.slice(0, 8).join(', ')}, ...`,
+ ].join('\n')
+
+ // Post review only for errors/warnings, not info
+ if (hasErrors || hasWarnings) {
+ await postReview({ github, context, core, dry, body })
+ }
+
+ // Write step summary
+ const fs = require('node:fs')
+ if (process.env.GITHUB_STEP_SUMMARY) {
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, body)
+ }
+ }
+
+ if (hasErrors) {
+ throw new Error('Commit validation failed with errors')
+ }
+ })
+}
+
+module.exports = run
+module.exports.validateCommitMessage = validateCommitMessage
+module.exports.checkCommitPatterns = checkCommitPatterns
+module.exports.normalizeCommitType = normalizeCommitType
diff --git a/archived/projt-launcher/ci/github-script/get-teams.js b/archived/projt-launcher/ci/github-script/get-teams.js
new file mode 100644
index 0000000000..c547d5ac62
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/get-teams.js
@@ -0,0 +1,134 @@
+/**
+ * ProjT Launcher - Team Information Fetcher
+ * Fetches team information from GitHub organization for CI purposes
+ */
+
+// Teams to exclude from processing (bots, voters, etc.)
+const excludeTeams = [
+ /^voters.*$/,
+ /^bots?$/,
+]
+
+/**
+ * Main function to fetch team information
+ */
+module.exports = async ({ github, context, core, outFile }) => {
+ const withRateLimit = require('./withRateLimit.js')
+ const { writeFileSync } = require('node:fs')
+
+ const org = context.repo.owner
+ const result = {}
+
+ await withRateLimit({ github, core }, async () => {
+ /**
+ * Convert array of users to object mapping login -> id
+ */
+ function makeUserSet(users) {
+ users.sort((a, b) => (a.login > b.login ? 1 : -1))
+ return users.reduce((acc, user) => {
+ acc[user.login] = user.id
+ return acc
+ }, {})
+ }
+
+ /**
+ * Process teams recursively
+ */
+ async function processTeams(teams) {
+ for (const team of teams) {
+ // Skip excluded teams
+ if (excludeTeams.some((regex) => team.slug.match(regex))) {
+ core.info(`Skipping excluded team: ${team.slug}`)
+ continue
+ }
+
+ core.notice(`Processing team ${team.slug}`)
+
+ try {
+ // Get team members
+ const members = makeUserSet(
+ await github.paginate(github.rest.teams.listMembersInOrg, {
+ org,
+ team_slug: team.slug,
+ role: 'member',
+ }),
+ )
+
+ // Get team maintainers
+ const maintainers = makeUserSet(
+ await github.paginate(github.rest.teams.listMembersInOrg, {
+ org,
+ team_slug: team.slug,
+ role: 'maintainer',
+ }),
+ )
+
+ result[team.slug] = {
+ description: team.description,
+ id: team.id,
+ maintainers,
+ members,
+ name: team.name,
+ }
+ } catch (e) {
+ core.warning(`Failed to fetch team ${team.slug}: ${e.message}`)
+ }
+
+ // Process child teams
+ try {
+ const childTeams = await github.paginate(
+ github.rest.teams.listChildInOrg,
+ {
+ org,
+ team_slug: team.slug,
+ },
+ )
+ await processTeams(childTeams)
+ } catch (e) {
+ // Child teams might not exist or be accessible
+ core.info(`No child teams for ${team.slug}`)
+ }
+ }
+ }
+
+ // Get all teams with access to the repository
+ try {
+ const teams = await github.paginate(github.rest.repos.listTeams, {
+ ...context.repo,
+ })
+
+ core.info(`Found ${teams.length} teams with repository access`)
+ await processTeams(teams)
+ } catch (e) {
+ core.warning(`Could not fetch repository teams: ${e.message}`)
+
+ // Fallback: create minimal team structure
+ result['projt-maintainers'] = {
+ description: 'ProjT Launcher Maintainers',
+ id: 0,
+ maintainers: {},
+ members: {},
+ name: 'ProjT Maintainers',
+ }
+ }
+ })
+
+ // Sort teams alphabetically
+ const sorted = Object.keys(result)
+ .sort()
+ .reduce((acc, key) => {
+ acc[key] = result[key]
+ return acc
+ }, {})
+
+ const json = `${JSON.stringify(sorted, null, 2)}\n`
+
+ if (outFile) {
+ writeFileSync(outFile, json)
+ core.info(`Team information written to ${outFile}`)
+ } else {
+ console.log(json)
+ }
+
+ return sorted
+}
diff --git a/archived/projt-launcher/ci/github-script/merge.js b/archived/projt-launcher/ci/github-script/merge.js
new file mode 100644
index 0000000000..536af0f056
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/merge.js
@@ -0,0 +1,308 @@
+/**
+ * ProjT Launcher - Merge Handler
+ * Handles PR merge operations with validation and queue management
+ */
+
+const { classify } = require('../supportedBranches.js')
+
+// Component definitions for ProjT Launcher
+const COMPONENTS = {
+ core: ['launcher/', 'systeminfo/', 'katabasis/', 'libnbtplusplus/', 'launcherjava/'],
+ ui: ['launcher/ui/', 'launcher/resources/', 'launcher/ui/'],
+ minecraft: ['launcher/minecraft/', 'tomlplusplus/', 'qdcss/'],
+ modplatform: ['launcher/modplatform/'],
+ build: ['CMakeLists.txt', 'cmake/', 'vcpkg.json', 'CMakePresets.json'],
+ docs: ['docs/', 'README.md', 'CONTRIBUTING.md'],
+ ci: ['.github/', 'ci/'],
+}
+
+/**
+ * Get component owners for changed files
+ * @param {Array} files - Changed files
+ * @returns {Set} Component owners
+ */
+function getComponentOwners(files) {
+ const owners = new Set()
+
+ for (const { filename } of files) {
+ for (const [component, paths] of Object.entries(COMPONENTS)) {
+ if (paths.some(path => filename.startsWith(path) || filename === path)) {
+ owners.add(component)
+ }
+ }
+ }
+
+ return owners
+}
+
+/**
+ * Run merge checklist for ProjT Launcher PRs
+ */
+function runChecklist({
+ committers,
+ events,
+ files,
+ pull_request,
+ log,
+ maintainers,
+ user,
+ userIsMaintainer,
+}) {
+ // Check what components are touched
+ const components = getComponentOwners(files)
+
+ // Get eligible reviewers from maintainers
+ const eligible = maintainers && maintainers.length > 0
+ ? new Set(maintainers)
+ : new Set()
+
+ // Get current approvals
+ const approvals = new Set(
+ events
+ .filter(
+ ({ event, state, commit_id }) =>
+ event === 'reviewed' &&
+ state === 'approved' &&
+ // Only approvals for the current head SHA count
+ commit_id === pull_request.head.sha,
+ )
+ .map(({ user }) => user?.id)
+ .filter(Boolean),
+ )
+
+ const checklist = {
+ 'PR targets a development branch (develop, master)':
+ classify(pull_request.base.ref).type.includes('development'),
+
+ 'PR has passing CI checks':
+ pull_request.mergeable_state !== 'blocked',
+
+ 'PR is at least one of:': {
+ 'Approved by a maintainer': committers.intersection(approvals).size > 0,
+ 'Opened by a maintainer': committers.has(pull_request.user.id),
+ 'Part of a backport':
+ pull_request.head.ref.startsWith('backport-') ||
+ pull_request.labels?.some(l => l.name === 'backport'),
+ },
+
+ 'PR has no merge conflicts':
+ pull_request.mergeable === true,
+ }
+
+ if (user) {
+ checklist[`${user.login} is a project maintainer`] = userIsMaintainer
+ if (components.size > 0) {
+ checklist[`${user.login} owns touched components (${Array.from(components).join(', ')})`] =
+ eligible.has(user.id)
+ }
+ } else {
+ checklist['PR has eligible reviewers'] = eligible.size > 0
+ }
+
+ const result = Object.values(checklist).every((v) =>
+ typeof v === 'boolean' ? v : Object.values(v).some(Boolean),
+ )
+
+ log('checklist', JSON.stringify(checklist))
+ log('components', JSON.stringify(Array.from(components)))
+ log('eligible', JSON.stringify(Array.from(eligible)))
+ log('result', result)
+
+ return {
+ checklist,
+ eligible,
+ components,
+ result,
+ }
+}
+
+/**
+ * Check for merge command in comment
+ * Format: @projt-launcher-bot merge
+ */
+function hasMergeCommand(body) {
+ return (body ?? '')
+ .replace(/<!--.*?-->/gms, '')
+ .replace(/(^`{3,})[^`].*?\1/gms, '')
+ .match(/^@projt-launcher-bot\s+merge\s*$/im)
+}
+
+/**
+ * Handle merge comment reaction
+ */
+async function handleMergeComment({ github, body, node_id, reaction }) {
+ if (!hasMergeCommand(body)) return
+
+ await github.graphql(
+ `mutation($node_id: ID!, $reaction: ReactionContent!) {
+ addReaction(input: {
+ content: $reaction,
+ subjectId: $node_id
+ })
+ { clientMutationId }
+ }`,
+ { node_id, reaction },
+ )
+}
+
+/**
+ * Handle merge request for a PR
+ */
+async function handleMerge({
+ github,
+ context,
+ core,
+ log,
+ dry,
+ pull_request,
+ events,
+ maintainers,
+ getTeamMembers,
+ getUser,
+}) {
+ const pull_number = pull_request.number
+
+ // Get list of maintainers (project committers)
+ const committers = new Set(
+ (await getTeamMembers('projt-maintainers')).map(({ id }) => id),
+ )
+
+ // Get changed files
+ const files = (
+ await github.rest.pulls.listFiles({
+ ...context.repo,
+ pull_number,
+ per_page: 100,
+ })
+ ).data
+
+ // Early exit for large PRs
+ if (files.length >= 100) {
+ core.warning('PR touches 100+ files, manual merge required')
+ return false
+ }
+
+ // Only look through comments after the latest push
+ const lastPush = events.findLastIndex(
+ ({ event, sha, commit_id }) =>
+ ['committed', 'head_ref_force_pushed'].includes(event) &&
+ (sha ?? commit_id) === pull_request.head.sha,
+ )
+
+ const comments = events.slice(lastPush + 1).filter(
+ ({ event, body, user, node_id }) =>
+ ['commented', 'reviewed'].includes(event) &&
+ hasMergeCommand(body) &&
+ user &&
+ (dry ||
+ !events.some(
+ ({ event, body }) =>
+ ['commented'].includes(event) &&
+ body.match(new RegExp(`^<!-- comment: ${node_id} -->$`, 'm')),
+ )),
+ )
+
+ /**
+ * Perform the merge
+ */
+ async function merge() {
+ if (dry) {
+ core.info(`Would merge #${pull_number}... (dry run)`)
+ return 'Merge completed (dry run)'
+ }
+
+ // Use merge queue if available, otherwise regular merge
+ try {
+ const resp = await github.graphql(
+ `mutation($node_id: ID!, $sha: GitObjectID) {
+ enqueuePullRequest(input: {
+ expectedHeadOid: $sha,
+ pullRequestId: $node_id
+ })
+ {
+ clientMutationId,
+ mergeQueueEntry { mergeQueue { url } }
+ }
+ }`,
+ { node_id: pull_request.node_id, sha: pull_request.head.sha },
+ )
+ return [
+ `:heavy_check_mark: [Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge`,
+ ]
+ } catch (e) {
+ log('Queue merge failed, trying direct merge', e.response?.errors?.[0]?.message)
+ }
+
+ // Fallback to direct merge
+ try {
+ await github.rest.pulls.merge({
+ ...context.repo,
+ pull_number,
+ merge_method: 'squash',
+ sha: pull_request.head.sha,
+ })
+ return [':heavy_check_mark: Merged successfully']
+ } catch (e) {
+ return [`:x: Merge failed: ${e.message}`]
+ }
+ }
+
+ // Process merge commands
+ for (const comment of comments) {
+ const user = await getUser(comment.user.id)
+
+ const { checklist, result } = runChecklist({
+ committers,
+ events,
+ files,
+ pull_request,
+ log,
+ maintainers: maintainers || [],
+ user,
+ userIsMaintainer: committers.has(user.id),
+ })
+
+ const response = []
+
+ if (result) {
+ response.push(...(await merge()))
+ } else {
+ response.push(':x: Cannot merge - checklist not satisfied:')
+ response.push('')
+ response.push('```')
+ response.push(JSON.stringify(checklist, null, 2))
+ response.push('```')
+ }
+
+ if (!dry) {
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: pull_number,
+ body: [
+ `<!-- comment: ${comment.node_id} -->`,
+ '',
+ ...response,
+ ].join('\n'),
+ })
+
+ await handleMergeComment({
+ github,
+ body: comment.body,
+ node_id: comment.node_id,
+ reaction: result ? 'ROCKET' : 'CONFUSED',
+ })
+ } else {
+ core.info(`Response: ${response.join('\n')}`)
+ }
+ }
+
+ return comments.length > 0
+}
+
+module.exports = {
+ runChecklist,
+ hasMergeCommand,
+ handleMergeComment,
+ handleMerge,
+ getComponentOwners,
+}
diff --git a/archived/projt-launcher/ci/github-script/package-lock.json b/archived/projt-launcher/ci/github-script/package-lock.json
new file mode 100644
index 0000000000..62a633e0d6
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/package-lock.json
@@ -0,0 +1,1721 @@
+{
+ "name": "github-script",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "version": "1.0.0",
+ "dependencies": {
+ "@actions/artifact": "6.1.0",
+ "@actions/core": "3.0.0",
+ "@actions/github": "9.0.0",
+ "bottleneck": "2.19.5",
+ "commander": "14.0.3"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@actions/artifact": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-6.1.0.tgz",
+ "integrity": "sha512-oRn9YhKkboXgIq2TQZ9uj6bhkT5ZUzFtnyTQ0tLGBwImaD0GfWShE5R0tPbN25EJmS3tz5sDd2JnVokAOtNrZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/core": "^3.0.0",
+ "@actions/github": "^9.0.0",
+ "@actions/http-client": "^4.0.0",
+ "@azure/storage-blob": "^12.30.0",
+ "@octokit/core": "^7.0.6",
+ "@octokit/plugin-request-log": "^6.0.0",
+ "@octokit/plugin-retry": "^8.0.0",
+ "@octokit/request": "^10.0.7",
+ "@octokit/request-error": "^7.1.0",
+ "@protobuf-ts/plugin": "^2.2.3-alpha.1",
+ "@protobuf-ts/runtime": "^2.9.4",
+ "archiver": "^7.0.1",
+ "jwt-decode": "^4.0.0",
+ "unzip-stream": "^0.3.1"
+ }
+ },
+ "node_modules/@actions/artifact/node_modules/@actions/http-client": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
+ "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==",
+ "license": "MIT",
+ "dependencies": {
+ "tunnel": "^0.0.6",
+ "undici": "^6.23.0"
+ }
+ },
+ "node_modules/@actions/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/exec": "^3.0.0",
+ "@actions/http-client": "^4.0.0"
+ }
+ },
+ "node_modules/@actions/core/node_modules/@actions/http-client": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
+ "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==",
+ "license": "MIT",
+ "dependencies": {
+ "tunnel": "^0.0.6",
+ "undici": "^6.23.0"
+ }
+ },
+ "node_modules/@actions/exec": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
+ "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/io": "^3.0.2"
+ }
+ },
+ "node_modules/@actions/github": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.0.0.tgz",
+ "integrity": "sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/http-client": "^3.0.2",
+ "@octokit/core": "^7.0.6",
+ "@octokit/plugin-paginate-rest": "^14.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
+ "@octokit/request": "^10.0.7",
+ "@octokit/request-error": "^7.1.0",
+ "undici": "^6.23.0"
+ }
+ },
+ "node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
+ "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
+ "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@actions/http-client": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
+ "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
+ "license": "MIT",
+ "dependencies": {
+ "tunnel": "^0.0.6",
+ "undici": "^6.23.0"
+ }
+ },
+ "node_modules/@actions/io": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
+ "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
+ "license": "MIT"
+ },
+ "node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
+ "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-util": "^1.13.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-client": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
+ "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
+ "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": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-http-compat": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz",
+ "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-client": "^1.10.0",
+ "@azure/core-rest-pipeline": "^1.22.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-lro": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
+ "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-util": "^1.2.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-paging": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
+ "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-rest-pipeline": {
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
+ "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
+ "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.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-tracing": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
+ "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-util": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
+ "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-xml": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz",
+ "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-xml-parser": "^5.0.7",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/logger": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
+ "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/storage-blob": {
+ "version": "12.30.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.30.0.tgz",
+ "integrity": "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.9.0",
+ "@azure/core-client": "^1.9.3",
+ "@azure/core-http-compat": "^2.2.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.6.2",
+ "@azure/core-rest-pipeline": "^1.19.1",
+ "@azure/core-tracing": "^1.2.0",
+ "@azure/core-util": "^1.11.0",
+ "@azure/core-xml": "^1.4.5",
+ "@azure/logger": "^1.1.4",
+ "@azure/storage-common": "^12.2.0",
+ "events": "^3.0.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/storage-common": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.2.0.tgz",
+ "integrity": "sha512-YZLxiJ3vBAAnFbG3TFuAMUlxZRexjQX5JDQxOkFGb6e2TpoxH3xyHI6idsMe/QrWtj41U/KoqBxlayzhS+LlwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.9.0",
+ "@azure/core-http-compat": "^2.2.0",
+ "@azure/core-rest-pipeline": "^1.19.1",
+ "@azure/core-tracing": "^1.2.0",
+ "@azure/core-util": "^1.11.0",
+ "@azure/logger": "^1.1.4",
+ "events": "^3.3.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@bufbuild/protobuf": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.0.tgz",
+ "integrity": "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==",
+ "license": "(Apache-2.0 AND BSD-3-Clause)"
+ },
+ "node_modules/@bufbuild/protoplugin": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.0.tgz",
+ "integrity": "sha512-mfAwI+4GqUtbw/ddfyolEHaAL86ozRIVlOg2A+SVRbjx1CjsMc1YJO+hBSkt/pqfpR+PmWBbZLstHbXP8KGtMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@bufbuild/protobuf": "2.6.0",
+ "@typescript/vfs": "^1.5.2",
+ "typescript": "5.4.5"
+ }
+ },
+ "node_modules/@bufbuild/protoplugin/node_modules/typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
+ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^6.0.0",
+ "@octokit/graphql": "^9.0.3",
+ "@octokit/request": "^10.0.6",
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "before-after-hook": "^4.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
+ "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
+ "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^10.0.6",
+ "@octokit/types": "^16.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+ "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
+ "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-retry": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.3.tgz",
+ "integrity": "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=7"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "10.0.7",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
+ "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.2",
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
+ "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+ "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^27.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@protobuf-ts/plugin": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz",
+ "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@bufbuild/protobuf": "^2.4.0",
+ "@bufbuild/protoplugin": "^2.4.0",
+ "@protobuf-ts/protoc": "^2.11.1",
+ "@protobuf-ts/runtime": "^2.11.1",
+ "@protobuf-ts/runtime-rpc": "^2.11.1",
+ "typescript": "^3.9"
+ },
+ "bin": {
+ "protoc-gen-dump": "bin/protoc-gen-dump",
+ "protoc-gen-ts": "bin/protoc-gen-ts"
+ }
+ },
+ "node_modules/@protobuf-ts/protoc": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz",
+ "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "protoc": "protoc.js"
+ }
+ },
+ "node_modules/@protobuf-ts/runtime": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz",
+ "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==",
+ "license": "(Apache-2.0 AND BSD-3-Clause)"
+ },
+ "node_modules/@protobuf-ts/runtime-rpc": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz",
+ "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@protobuf-ts/runtime": "^2.11.1"
+ }
+ },
+ "node_modules/@typescript/vfs": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.1.tgz",
+ "integrity": "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ }
+ },
+ "node_modules/@typespec/ts-http-runtime": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz",
+ "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==",
+ "license": "MIT",
+ "dependencies": {
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/archiver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
+ "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.2",
+ "async": "^3.2.4",
+ "buffer-crc32": "^1.0.0",
+ "readable-stream": "^4.0.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^3.0.0",
+ "zip-stream": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
+ "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.0.0",
+ "graceful-fs": "^4.2.0",
+ "is-stream": "^2.0.1",
+ "lazystream": "^1.0.0",
+ "lodash": "^4.17.15",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/bare-events": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
+ "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/before-after-hook": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bottleneck": {
+ "version": "2.19.5",
+ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
+ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/buffers": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+ "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
+ "engines": {
+ "node": ">=0.2.0"
+ }
+ },
+ "node_modules/chainsaw": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+ "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
+ "license": "MIT/X11",
+ "dependencies": {
+ "traverse": ">=0.3.0 <0.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
+ "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "crc32-stream": "^6.0.0",
+ "is-stream": "^2.0.1",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
+ "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "license": "MIT"
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz",
+ "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^2.1.2"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
+ "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.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==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
+ "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+ "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+ "license": "MIT/X11",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "3.9.10",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
+ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/undici": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
+ "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+ "license": "ISC"
+ },
+ "node_modules/unzip-stream": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz",
+ "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary": "^0.3.0",
+ "mkdirp": "^0.5.1"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
+ "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.0",
+ "compress-commons": "^6.0.2",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ }
+ }
+}
diff --git a/archived/projt-launcher/ci/github-script/package.json b/archived/projt-launcher/ci/github-script/package.json
new file mode 100644
index 0000000000..e4263e2ddf
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "projt-launcher-github-scripts",
+ "version": "1.0.0",
+ "description": "GitHub Actions scripts for ProjT Launcher CI",
+ "private": true,
+ "scripts": {
+ "test": "node test/commits.test.js"
+ },
+ "dependencies": {
+ "@actions/artifact": "6.1.0",
+ "@actions/core": "3.0.0",
+ "@actions/github": "9.0.0",
+ "bottleneck": "2.19.5",
+ "commander": "14.0.3"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/archived/projt-launcher/ci/github-script/prepare.js b/archived/projt-launcher/ci/github-script/prepare.js
new file mode 100644
index 0000000000..2c60314f11
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/prepare.js
@@ -0,0 +1,314 @@
+/**
+ * ProjT Launcher - PR Preparation Script
+ * Validates PR structure and prepares merge information
+ */
+
+const { classify } = require('../supportedBranches.js')
+const { postReview } = require('./reviews.js')
+
+const SIGNOFF_MARKER = '<!-- bot:missing-signed-off-by -->'
+
+function stripNoise(body = '') {
+ return String(body)
+ .replace(/\r/g, '')
+ .replace(/<!--.*?-->/gms, '')
+ .replace(/(^`{3,})[^`].*?\1/gms, '')
+}
+
+function hasSignedOffBy(body = '') {
+ const cleaned = stripNoise(body)
+ return /^signed-off-by:\s+.+<[^<>]+>\s*$/im.test(cleaned)
+}
+
+async function dismissSignoffReviews({ github, context, pull_number }) {
+ const reviews = await github.paginate(github.rest.pulls.listReviews, {
+ ...context.repo,
+ pull_number,
+ })
+
+ const signoffReviews = reviews.filter(
+ (r) =>
+ r.user?.login === 'github-actions[bot]' &&
+ r.state === 'CHANGES_REQUESTED' &&
+ typeof r.body === 'string' &&
+ r.body.includes(SIGNOFF_MARKER),
+ )
+
+ for (const review of signoffReviews) {
+ await github.rest.pulls.dismissReview({
+ ...context.repo,
+ pull_number,
+ review_id: review.id,
+ message: 'Signed-off-by found, thank you!',
+ })
+ }
+}
+
+/**
+ * Main PR preparation function
+ * Validates that the PR targets the correct branch and can be merged
+ */
+module.exports = async ({ github, context, core, dry }) => {
+ const payload = context.payload || {}
+ const pull_number =
+ payload?.pull_request?.number ??
+ (Array.isArray(payload?.merge_group?.pull_requests) &&
+ payload.merge_group.pull_requests[0]?.number)
+
+ if (typeof pull_number !== 'number') {
+ core.info('No pull request found on this event; skipping prepare step.')
+ return { ok: true, skipped: true, reason: 'no-pull-request' }
+ }
+
+ // Wait for GitHub to compute merge status
+ for (const retryInterval of [5, 10, 20, 40]) {
+ core.info('Checking whether the pull request can be merged...')
+ const prInfo = (
+ await github.rest.pulls.get({
+ ...context.repo,
+ pull_number,
+ })
+ ).data
+
+ if (prInfo.state !== 'open') {
+ throw new Error('PR is not open anymore.')
+ }
+
+ if (prInfo.mergeable == null) {
+ core.info(
+ `GitHub is still computing merge status, waiting ${retryInterval} seconds...`,
+ )
+ await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000))
+ continue
+ }
+
+ const { base, head, user } = prInfo
+
+ const authorLogin = user?.login ?? ''
+ const isBotAuthor =
+ (user?.type ?? '').toLowerCase() === 'bot' || /\[bot\]$/i.test(authorLogin)
+
+ // Enforce PR template sign-off (Signed-off-by: Name <email>)
+ if (isBotAuthor) {
+ core.info(`Skipping Signed-off-by requirement for bot author: ${authorLogin}`)
+ if (!dry) {
+ await dismissSignoffReviews({ github, context, pull_number })
+ }
+ } else if (!hasSignedOffBy(prInfo.body)) {
+ const body = [
+ SIGNOFF_MARKER,
+ '',
+ '## Missing Signed-off-by',
+ '',
+ 'This repository requires a DCO-style sign-off line in the PR description.',
+ '',
+ 'Add a line like this to the PR description (under “Signed-off-by”):',
+ '',
+ '```',
+ 'Signed-off-by: Your Name <you@example.com>',
+ '```',
+ '',
+ 'After updating the PR description, this check will re-run automatically.',
+ ].join('\n')
+
+ await postReview({ github, context, core, dry, body })
+ throw new Error('Missing Signed-off-by in PR description')
+ } else if (!dry) {
+ await dismissSignoffReviews({ github, context, pull_number })
+ }
+
+ // Classify base branch
+ const baseClassification = classify(base.ref)
+ core.setOutput('base', baseClassification)
+ core.info(`Base branch classification: ${JSON.stringify(baseClassification)}`)
+
+ // Classify head branch
+ const headClassification =
+ base.repo.full_name === head.repo.full_name
+ ? classify(head.ref)
+ : { type: ['wip'] } // PRs from forks are WIP
+ core.setOutput('head', headClassification)
+ core.info(`Head branch classification: ${JSON.stringify(headClassification)}`)
+
+ // Validate base branch targeting
+ if (!baseClassification.type.includes('development') &&
+ !baseClassification.type.includes('release')) {
+ const body = [
+ '## Invalid Target Branch',
+ '',
+ `This PR targets \`${base.ref}\`, which is not a valid target branch.`,
+ '',
+ '### Valid target branches for ProjT Launcher:',
+ '',
+ '| Branch | Purpose |',
+ '|--------|---------|',
+ '| `develop` | Main development branch |',
+ '| `master` / `main` | Stable branch |',
+ '| `release-X.Y.Z` | Release branches |',
+ '',
+ 'Please [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request) to the appropriate target.',
+ ].join('\n')
+
+ await postReview({ github, context, core, dry, body })
+ throw new Error('PR targets invalid branch.')
+ }
+
+ // Check for release branch targeting from wrong branch
+ if (baseClassification.isRelease) {
+ // For release branches, typically only hotfixes and backports should target them
+ const isBackport = head.ref.startsWith('backport-')
+ const isHotfix = head.ref.startsWith('hotfix-') || head.ref.startsWith('hotfix/')
+
+ if (!isBackport && !isHotfix && headClassification.type.includes('wip')) {
+ const body = [
+ '## Release Branch Warning',
+ '',
+ `This PR targets the release branch \`${base.ref}\`.`,
+ '',
+ 'Release branches should only receive:',
+ '- **Backports** from the development branch',
+ '- **Hotfixes** for critical bugs',
+ '',
+ 'If this is a regular feature/fix, please target `develop` instead.',
+ '',
+ 'If this is intentionally a hotfix, consider naming your branch `hotfix/description`.',
+ ].join('\n')
+
+ await postReview({ github, context, core, dry, body })
+ // This is a warning, not an error
+ core.warning('PR targets release branch from non-hotfix/backport branch')
+ }
+ }
+
+ // Validate feature branches target develop
+ if (headClassification.isFeature &&
+ !['develop'].includes(base.ref)) {
+ const body = [
+ '## Feature Branch Target',
+ '',
+ `Feature branches should typically target \`develop\`, not \`${base.ref}\`.`,
+ '',
+ 'Please verify this is the correct target branch.',
+ ].join('\n')
+
+ core.warning(body)
+ // Don't block, just warn
+ }
+
+ // Process merge state
+ let mergedSha, targetSha
+
+ if (prInfo.mergeable) {
+ core.info('✓ PR can be merged.')
+
+ mergedSha = prInfo.merge_commit_sha
+ targetSha = (
+ await github.rest.repos.getCommit({
+ ...context.repo,
+ ref: prInfo.merge_commit_sha,
+ })
+ ).data.parents[0].sha
+ } else {
+ core.warning('⚠ PR has merge conflicts.')
+
+ mergedSha = head.sha
+ targetSha = (
+ await github.rest.repos.compareCommitsWithBasehead({
+ ...context.repo,
+ basehead: `${base.sha}...${head.sha}`,
+ })
+ ).data.merge_base_commit.sha
+ }
+
+ // Set outputs for downstream jobs
+ core.setOutput('mergedSha', mergedSha)
+ core.setOutput('targetSha', targetSha)
+ core.setOutput('mergeable', prInfo.mergeable)
+ core.setOutput('headSha', head.sha)
+ core.setOutput('baseSha', base.sha)
+
+ // Get changed files for analysis
+ const files = await github.paginate(github.rest.pulls.listFiles, {
+ ...context.repo,
+ pull_number,
+ per_page: 100,
+ })
+
+ // Categorize changes
+ const categories = {
+ source: files.filter(f =>
+ f.filename.startsWith('launcher/')
+ ).length,
+ ui: files.filter(f =>
+ f.filename.includes('/ui/')
+ ).length,
+ build: files.filter(f =>
+ f.filename.includes('CMake') ||
+ f.filename.includes('vcpkg') ||
+ f.filename.endsWith('.cmake')
+ ).length,
+ ci: files.filter(f =>
+ f.filename.startsWith('.github/') ||
+ f.filename.startsWith('ci/')
+ ).length,
+ docs: files.filter(f =>
+ f.filename.startsWith('docs/') ||
+ f.filename.endsWith('.md')
+ ).length,
+ translations: files.filter(f =>
+ f.filename.includes('translations/')
+ ).length,
+ }
+
+ core.info(`Changes summary:`)
+ core.info(` Source files: ${categories.source}`)
+ core.info(` UI files: ${categories.ui}`)
+ core.info(` Build files: ${categories.build}`)
+ core.info(` CI files: ${categories.ci}`)
+ core.info(` Documentation: ${categories.docs}`)
+ core.info(` Translations: ${categories.translations}`)
+
+ core.setOutput('categories', JSON.stringify(categories))
+ core.setOutput('totalFiles', files.length)
+
+ // Write step summary
+ if (process.env.GITHUB_STEP_SUMMARY) {
+ const fs = require('node:fs')
+ const summary = [
+ '## PR Preparation Summary',
+ '',
+ `| Property | Value |`,
+ `|----------|-------|`,
+ `| PR Number | #${pull_number} |`,
+ `| Base Branch | \`${base.ref}\` |`,
+ `| Head Branch | \`${head.ref}\` |`,
+ `| Mergeable | ${prInfo.mergeable ? '✅ Yes' : '❌ No'} |`,
+ `| Total Files | ${files.length} |`,
+ '',
+ '### Change Categories',
+ '',
+ `| Category | Files |`,
+ `|----------|-------|`,
+ ...Object.entries(categories).map(([cat, count]) =>
+ `| ${cat.charAt(0).toUpperCase() + cat.slice(1)} | ${count} |`
+ ),
+ ].join('\n')
+
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
+ }
+
+ return {
+ mergeable: prInfo.mergeable,
+ mergedSha,
+ targetSha,
+ headSha: head.sha,
+ baseSha: base.sha,
+ base: baseClassification,
+ head: headClassification,
+ files: files.length,
+ categories,
+ }
+ }
+
+ throw new Error('Timeout waiting for merge status computation')
+}
diff --git a/archived/projt-launcher/ci/github-script/reviewers.js b/archived/projt-launcher/ci/github-script/reviewers.js
new file mode 100644
index 0000000000..9a2fd8df6a
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/reviewers.js
@@ -0,0 +1,329 @@
+/**
+ * ProjT Launcher - Reviewer Assignment
+ * Automatically assigns reviewers based on changed files and CODEOWNERS
+ */
+
+const fs = require('node:fs')
+const path = require('node:path')
+
+function extractMaintainersBlock(source) {
+ const token = 'maintainers ='
+ const start = source.indexOf(token)
+ if (start === -1) {
+ return ''
+ }
+
+ const braceStart = source.indexOf('{', start)
+ if (braceStart === -1) {
+ return ''
+ }
+
+ let depth = 0
+ for (let i = braceStart; i < source.length; i += 1) {
+ const char = source[i]
+ if (char === '{') {
+ depth += 1
+ } else if (char === '}') {
+ depth -= 1
+ if (depth === 0) {
+ return source.slice(braceStart, i + 1)
+ }
+ }
+ }
+ return ''
+}
+
+function parseAreas(areaBlock) {
+ const matches = areaBlock.match(/"([^"]+)"/g) || []
+ return matches.map(entry => entry.replace(/"/g, ''))
+}
+
+function loadMaintainersFromNix() {
+ const maintainersPath = path.join(__dirname, '..', 'eval', 'compare', 'maintainers.nix')
+ try {
+ const source = fs.readFileSync(maintainersPath, 'utf8')
+ const block = extractMaintainersBlock(source)
+ if (!block) {
+ return []
+ }
+
+ const entryRegex = /(\w+)\s*=\s*{([\s\S]*?)\n\s*};/g
+ const maintainers = []
+ let match
+ while ((match = entryRegex.exec(block)) !== null) {
+ const [, , body] = match
+ const githubMatch = body.match(/github\s*=\s*"([^"]+)"/)
+ if (!githubMatch) continue
+ const areasMatch = body.match(/areas\s*=\s*\[([\s\S]*?)\]/)
+ const areas = areasMatch ? parseAreas(areasMatch[1]) : []
+ maintainers.push({
+ handle: githubMatch[1],
+ areas,
+ })
+ }
+ return maintainers
+ } catch (error) {
+ console.warn(`Could not read maintainers from maintainers.nix: ${error.message}`)
+ return []
+ }
+}
+
+const FALLBACK_MAINTAINERS = [
+ {
+ handle: 'YongDo-Hyun',
+ areas: ['all'],
+ },
+ {
+ handle: 'grxtor',
+ areas: ['all'],
+ },
+]
+
+const MAINTAINERS = (() => {
+ const parsed = loadMaintainersFromNix()
+ return parsed.length > 0 ? parsed : FALLBACK_MAINTAINERS
+})()
+
+// File patterns to components mapping
+const FILE_PATTERNS = [
+ { pattern: /^launcher\/ui\//, component: 'ui' },
+ { pattern: /^launcher\/minecraft\//, component: 'minecraft' },
+ { pattern: /^launcher\/modplatform\//, component: 'modplatform' },
+ { pattern: /^launcher\//, component: 'core' },
+ { pattern: /^libraries\//, component: 'core' },
+ { pattern: /^\.github\//, component: 'ci' },
+ { pattern: /^ci\//, component: 'ci' },
+ { pattern: /CMakeLists\.txt$/, component: 'build' },
+ { pattern: /\.cmake$/, component: 'build' },
+ { pattern: /vcpkg/, component: 'build' },
+ { pattern: /^docs\//, component: 'docs' },
+ { pattern: /\.md$/, component: 'docs' },
+ { pattern: /translations\//, component: 'translations' },
+]
+
+const COMPONENTS = Array.from(new Set(FILE_PATTERNS.map(({ component }) => component)))
+
+const getMaintainersForComponent = component => {
+ const assigned = MAINTAINERS.filter(
+ maintainer =>
+ maintainer.areas.includes(component) || maintainer.areas.includes('all')
+ ).map(maintainer => maintainer.handle)
+
+ return assigned.length > 0 ? assigned : MAINTAINERS.map(maintainer => maintainer.handle)
+}
+
+// Component to reviewer mapping for ProjT Launcher
+const COMPONENT_REVIEWERS = Object.fromEntries(
+ COMPONENTS.map(component => [component, getMaintainersForComponent(component)])
+)
+
+/**
+ * Get components affected by file changes
+ * @param {Array} files - List of changed files
+ * @returns {Set} Affected components
+ */
+function getAffectedComponents(files) {
+ const components = new Set()
+
+ for (const file of files) {
+ const filename = file.filename || file
+ for (const { pattern, component } of FILE_PATTERNS) {
+ if (pattern.test(filename)) {
+ components.add(component)
+ break
+ }
+ }
+ }
+
+ return components
+}
+
+/**
+ * Get reviewers for components
+ * @param {Set} components - Affected components
+ * @returns {Set} Reviewers
+ */
+function getReviewersForComponents(components) {
+ const reviewers = new Set()
+
+ for (const component of components) {
+ const componentReviewers = COMPONENT_REVIEWERS[component] || []
+ for (const reviewer of componentReviewers) {
+ reviewers.add(reviewer.toLowerCase())
+ }
+ }
+
+ return reviewers
+}
+
+/**
+ * Handle reviewer assignment for a PR
+ */
+async function handleReviewers({
+ github,
+ context,
+ core,
+ log,
+ dry,
+ pull_request,
+ reviews,
+ maintainers,
+ owners,
+ getTeamMembers,
+ getUser,
+}) {
+ const pull_number = pull_request.number
+
+ // Get currently requested reviewers
+ const requested_reviewers = new Set(
+ pull_request.requested_reviewers.map(({ login }) => login.toLowerCase()),
+ )
+ log?.(
+ 'reviewers - requested_reviewers',
+ Array.from(requested_reviewers).join(', '),
+ )
+
+ // Get existing reviewers (already reviewed)
+ const existing_reviewers = new Set(
+ reviews.map(({ user }) => user?.login.toLowerCase()).filter(Boolean),
+ )
+ log?.(
+ 'reviewers - existing_reviewers',
+ Array.from(existing_reviewers).join(', '),
+ )
+
+ // Guard against too many reviewers from large PRs
+ if (maintainers && maintainers.length > 16) {
+ core.warning('Too many potential reviewers, skipping automatic assignment.')
+ return existing_reviewers.size === 0 && requested_reviewers.size === 0
+ }
+
+ // Build list of potential reviewers
+ const users = new Set()
+
+ // Add maintainers
+ if (maintainers) {
+ for (const id of maintainers) {
+ try {
+ const user = await getUser(id)
+ users.add(user.login.toLowerCase())
+ } catch (e) {
+ core.warning(`Could not resolve user ID ${id}`)
+ }
+ }
+ }
+
+ // Add owners (from CODEOWNERS)
+ if (owners) {
+ for (const handle of owners) {
+ if (handle && !handle.includes('/')) {
+ users.add(handle.toLowerCase())
+ }
+ }
+ }
+
+ log?.('reviewers - users', Array.from(users).join(', '))
+
+ // Handle team-based owners
+ const teams = new Set()
+ if (owners) {
+ for (const handle of owners) {
+ const parts = handle.split('/')
+ if (parts.length === 2 && parts[0] === context.repo.owner) {
+ teams.add(parts[1])
+ }
+ }
+ }
+ log?.('reviewers - teams', Array.from(teams).join(', '))
+
+ // Get team members
+ const team_members = new Set()
+ if (teams.size > 0 && getTeamMembers) {
+ for (const team of teams) {
+ try {
+ const members = await getTeamMembers(team)
+ for (const member of members) {
+ team_members.add(member.login.toLowerCase())
+ }
+ } catch (e) {
+ core.warning(`Could not fetch team ${team}`)
+ }
+ }
+ }
+ log?.('reviewers - team_members', Array.from(team_members).join(', '))
+
+ // Combine all potential reviewers
+ const all_reviewers = new Set([...users, ...team_members])
+
+ // Remove PR author - can't review own PR
+ const author = pull_request.user?.login.toLowerCase()
+ all_reviewers.delete(author)
+
+ log?.('reviewers - all_reviewers', Array.from(all_reviewers).join(', '))
+
+ // Filter to collaborators only
+ const reviewers = []
+ for (const username of all_reviewers) {
+ try {
+ await github.rest.repos.checkCollaborator({
+ ...context.repo,
+ username,
+ })
+ reviewers.push(username)
+ } catch (e) {
+ if (e.status !== 404) throw e
+ core.warning(
+ `User ${username} cannot be requested for review (not a collaborator)`,
+ )
+ }
+ }
+ log?.('reviewers - filtered_reviewers', reviewers.join(', '))
+
+ // Limit reviewers
+ if (reviewers.length > 10) {
+ core.warning(`Too many reviewers (${reviewers.length}), limiting to 10`)
+ reviewers.length = 10
+ }
+
+ // Determine who needs to be requested
+ const new_reviewers = new Set(reviewers)
+ .difference(requested_reviewers)
+ .difference(existing_reviewers)
+
+ log?.(
+ 'reviewers - new_reviewers',
+ Array.from(new_reviewers).join(', '),
+ )
+
+ if (new_reviewers.size === 0) {
+ log?.('Has reviewer changes', 'false (no new reviewers)')
+ } else if (dry) {
+ core.info(
+ `Would request reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')} (dry run)`,
+ )
+ } else {
+ await github.rest.pulls.requestReviewers({
+ ...context.repo,
+ pull_number,
+ reviewers: Array.from(new_reviewers),
+ })
+ core.info(
+ `Requested reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')}`,
+ )
+ }
+
+ // Return whether "needs-reviewers" label should be set
+ return (
+ new_reviewers.size === 0 &&
+ existing_reviewers.size === 0 &&
+ requested_reviewers.size === 0
+ )
+}
+
+module.exports = {
+ handleReviewers,
+ getAffectedComponents,
+ getReviewersForComponents,
+ COMPONENT_REVIEWERS,
+ FILE_PATTERNS,
+}
diff --git a/archived/projt-launcher/ci/github-script/reviews.js b/archived/projt-launcher/ci/github-script/reviews.js
new file mode 100644
index 0000000000..749ebc7085
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/reviews.js
@@ -0,0 +1,93 @@
+/**
+ * ProjT Launcher - Review Management
+ * Handles GitHub PR review operations
+ */
+
+/**
+ * Dismiss all bot-created reviews on a PR
+ */
+async function dismissReviews({ github, context, dry }) {
+ const pull_number = context.payload.pull_request.number
+
+ if (dry) {
+ return
+ }
+
+ await Promise.all(
+ (
+ await github.paginate(github.rest.pulls.listReviews, {
+ ...context.repo,
+ pull_number,
+ })
+ )
+ .filter((review) => review.user?.login === 'github-actions[bot]')
+ .map(async (review) => {
+ if (review.state === 'CHANGES_REQUESTED') {
+ await github.rest.pulls.dismissReview({
+ ...context.repo,
+ pull_number,
+ review_id: review.id,
+ message: 'All good now, thank you!',
+ })
+ }
+ await github.graphql(
+ `mutation($node_id:ID!) {
+ minimizeComment(input: {
+ classifier: RESOLVED,
+ subjectId: $node_id
+ })
+ { clientMutationId }
+ }`,
+ { node_id: review.node_id },
+ )
+ }),
+ )
+}
+
+async function postReview({ github, context, core, dry, body }) {
+ const pull_number = context.payload.pull_request.number
+
+ const pendingReview = (
+ await github.paginate(github.rest.pulls.listReviews, {
+ ...context.repo,
+ pull_number,
+ })
+ ).find(
+ (review) =>
+ review.user?.login === 'github-actions[bot]' &&
+ // If a review is still pending, we can just update this instead
+ // of posting a new one.
+ (review.state === 'CHANGES_REQUESTED' ||
+ // No need to post a new review, if an older one with the exact
+ // same content had already been dismissed.
+ review.body === body),
+ )
+
+ if (dry) {
+ if (pendingReview)
+ core.info(`pending review found: ${pendingReview.html_url}`)
+ else core.info('no pending review found')
+ core.info(body)
+ } else {
+ if (pendingReview) {
+ await github.rest.pulls.updateReview({
+ ...context.repo,
+ pull_number,
+ review_id: pendingReview.id,
+ body,
+ })
+ } else {
+ await github.rest.pulls.createReview({
+ ...context.repo,
+ pull_number,
+ event: 'REQUEST_CHANGES',
+ body,
+ })
+ }
+ }
+}
+
+module.exports = {
+ dismissReviews,
+ postReview,
+}
diff --git a/archived/projt-launcher/ci/github-script/run b/archived/projt-launcher/ci/github-script/run
new file mode 100644
index 0000000000..4f296f5633
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/run
@@ -0,0 +1,132 @@
+#!/usr/bin/env -S node --import ./run
+/**
+ * ProjT Launcher - CI Script Runner
+ * CLI tool for running CI automation scripts locally
+ */
+import { execSync } from 'node:child_process'
+import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { program } from 'commander'
+import * as core from '@actions/core'
+import { getOctokit } from '@actions/github'
+
+/**
+ * Run a CI action locally
+ */
+async function run(action, owner, repo, pull_number, options = {}) {
+ // Get GitHub token from gh CLI
+ const token = execSync('gh auth token', { encoding: 'utf-8' }).trim()
+ const github = getOctokit(token)
+
+ // Build payload
+ const payload = !pull_number ? {} : {
+ pull_request: (await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number,
+ })).data
+ }
+
+ process.env['INPUT_GITHUB-TOKEN'] = token
+
+ // Set up step summary file
+ closeSync(openSync('step-summary.md', 'w'))
+ process.env.GITHUB_STEP_SUMMARY = 'step-summary.md'
+
+ await action({
+ github,
+ context: {
+ payload,
+ repo: {
+ owner,
+ repo,
+ },
+ },
+ core,
+ dry: true,
+ ...options,
+ })
+}
+
+program
+ .name('projt-ci')
+ .description('ProjT Launcher CI automation script runner')
+ .version('1.0.0')
+
+program
+ .command('prepare')
+ .description('Prepare and validate a pull request')
+ .argument('<owner>', 'Repository owner (e.g., Project-Tick)')
+ .argument('<repo>', 'Repository name (e.g., ProjT-Launcher)')
+ .argument('<pr>', 'Pull Request number')
+ .option('--no-dry', 'Make actual modifications')
+ .action(async (owner, repo, pr, options) => {
+ const prepare = (await import('./prepare.js')).default
+ await run(prepare, owner, repo, pr, options)
+ })
+
+program
+ .command('commits')
+ .description('Validate commit messages and structure')
+ .argument('<owner>', 'Repository owner')
+ .argument('<repo>', 'Repository name')
+ .argument('<pr>', 'Pull Request number')
+ .option('--no-dry', 'Make actual modifications')
+ .action(async (owner, repo, pr, options) => {
+ const commits = (await import('./commits.js')).default
+ await run(commits, owner, repo, pr, options)
+ })
+
+program
+ .command('get-teams')
+ .description('Fetch team information from GitHub organization')
+ .argument('<owner>', 'Organization/owner name')
+ .argument('<repo>', 'Repository name')
+ .argument('[outFile]', 'Output file path (prints to stdout if omitted)')
+ .action(async (owner, repo, outFile, options) => {
+ const getTeams = (await import('./get-teams.js')).default
+ await run(getTeams, owner, repo, undefined, { ...options, outFile })
+ })
+
+program
+ .command('reviewers')
+ .description('Assign reviewers to a pull request')
+ .argument('<owner>', 'Repository owner')
+ .argument('<repo>', 'Repository name')
+ .argument('<pr>', 'Pull Request number')
+ .option('--no-dry', 'Make actual modifications')
+ .action(async (owner, repo, pr, options) => {
+ const { handleReviewers } = await import('./reviewers.js')
+ const token = execSync('gh auth token', { encoding: 'utf-8' }).trim()
+ const github = getOctokit(token)
+
+ const pull_request = (await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: pr,
+ })).data
+
+ const reviews = await github.paginate(github.rest.pulls.listReviews, {
+ owner,
+ repo,
+ pull_number: pr,
+ })
+
+ await handleReviewers({
+ github,
+ context: { repo: { owner, repo } },
+ core,
+ log: console.log,
+ dry: options.dry ?? true,
+ pull_request,
+ reviews,
+ maintainers: [],
+ owners: [],
+ getTeamMembers: async () => [],
+ getUser: async (id) => ({ login: `user-${id}`, id }),
+ })
+ })
+
+// Parse CLI arguments
+await program.parse()
diff --git a/archived/projt-launcher/ci/github-script/shell.nix b/archived/projt-launcher/ci/github-script/shell.nix
new file mode 100644
index 0000000000..788fee1815
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/shell.nix
@@ -0,0 +1,40 @@
+# ProjT Launcher - GitHub Script Development Shell
+# Provides Node.js environment for CI automation scripts
+{
+ system ? builtins.currentSystem,
+ pkgs ? import <nixpkgs> { inherit system; },
+}:
+
+pkgs.mkShell {
+ name = "projt-launcher-github-script";
+
+ packages = with pkgs; [
+ # Node.js for running scripts
+ nodejs_20
+
+ # GitHub CLI for authentication
+ gh
+
+ # Optional: development tools
+ nodePackages.npm
+ ];
+
+ shellHook = ''
+ echo "ProjT Launcher GitHub Script Development Environment"
+ echo ""
+ echo "Available commands:"
+ echo " npm install - Install dependencies"
+ echo " ./run --help - Show available CLI commands"
+ echo " gh auth login - Authenticate with GitHub"
+ echo ""
+
+ # Install npm dependencies if package-lock.json exists
+ if [ -f package-lock.json ] && [ ! -d node_modules ]; then
+ echo "Installing npm dependencies..."
+ npm ci
+ fi
+ '';
+
+ # Environment variables
+ PROJT_CI_ENV = "development";
+}
diff --git a/archived/projt-launcher/ci/github-script/test/commits.test.js b/archived/projt-launcher/ci/github-script/test/commits.test.js
new file mode 100644
index 0000000000..ed1be49682
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/test/commits.test.js
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+'use strict'
+
+process.env.COMMIT_TYPES = 'customtype'
+
+const assert = require('node:assert/strict')
+const commits = require('../commits.js')
+
+const { validateCommitMessage, normalizeCommitType } = commits
+
+const validMessages = [
+ 'feat(ui): add redesigned settings panel',
+ 'refactor: drop deprecated launcher flag support',
+ 'chore(ci): refresh workflows configuration',
+ '11.feat: support legacy numbered commit type format',
+ '23.deps(deps): bump dependency pins',
+ 'release: publish stable build artifacts',
+ 'customtype: allow env commit type overrides',
+]
+
+for (const message of validMessages) {
+ const result = validateCommitMessage(message)
+ assert.equal(
+ result.valid,
+ true,
+ `Expected commit "${message}" to be valid, got: ${result.message}`
+ )
+}
+
+const invalidType = validateCommitMessage('unknown(scope): add feature that is real enough')
+assert.equal(invalidType.valid, false, 'Expected invalid type to be rejected')
+assert.match(invalidType.message, /Unknown commit type/i)
+
+const shortDescription = validateCommitMessage('feat: short')
+assert.equal(shortDescription.valid, false, 'Expected short description to fail validation')
+assert.match(shortDescription.message, /too short/i)
+
+assert.equal(normalizeCommitType('11.feat'), 'feat')
+assert.equal(normalizeCommitType('23.deps'), 'deps')
+assert.equal(normalizeCommitType('chore'), 'chore')
+
+console.log('commits.js tests passed')
diff --git a/archived/projt-launcher/ci/github-script/withRateLimit.js b/archived/projt-launcher/ci/github-script/withRateLimit.js
new file mode 100644
index 0000000000..e7fcfbb513
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/withRateLimit.js
@@ -0,0 +1,86 @@
+/**
+ * ProjT Launcher - Rate Limit Handler
+ * Manages GitHub API rate limiting for CI scripts
+ */
+
+module.exports = async ({ github, core, maxConcurrent = 1 }, callback) => {
+ let Bottleneck
+ try {
+ Bottleneck = require('bottleneck')
+ } catch (err) {
+ core?.warning?.('bottleneck not installed; running without explicit rate limiting')
+ Bottleneck = class {
+ constructor() {}
+ wrap(fn) {
+ return (...args) => fn(...args)
+ }
+ chain() {
+ return this
+ }
+ schedule(fn, ...args) {
+ return fn(...args)
+ }
+ updateSettings() {}
+ }
+ }
+
+ const stats = {
+ issues: 0,
+ prs: 0,
+ requests: 0,
+ artifacts: 0,
+ }
+
+ // Rate-Limiting and Throttling, see for details:
+ // https://github.com/octokit/octokit.js/issues/1069#throttling
+ // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api
+ const allLimits = new Bottleneck({
+ // Avoid concurrent requests
+ maxConcurrent,
+ // Will be updated with first `updateReservoir()` call below.
+ reservoir: 0,
+ })
+ // Pause between mutative requests
+ const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits)
+ github.hook.wrap('request', async (request, options) => {
+ // Requests to a different host do not count against the rate limit.
+ if (options.url.startsWith('https://github.com')) return request(options)
+ // Requests to the /rate_limit endpoint do not count against the rate limit.
+ if (options.url === '/rate_limit') return request(options)
+ // Search requests are in a different resource group, which allows 30 requests / minute.
+ // We do less than a handful each run, so not implementing throttling for now.
+ if (options.url.startsWith('/search/')) return request(options)
+ stats.requests++
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method))
+ return writeLimits.schedule(request.bind(null, options))
+ else return allLimits.schedule(request.bind(null, options))
+ })
+
+ async function updateReservoir() {
+ let response
+ try {
+ response = await github.rest.rateLimit.get()
+ } catch (err) {
+ core.error(`Failed updating reservoir:\n${err}`)
+ // Keep retrying on failed rate limit requests instead of exiting the script early.
+ return
+ }
+ // Always keep 1000 spare requests for other jobs to do their regular duty.
+ // They normally use below 100, so 1000 is *plenty* of room to work with.
+ const reservoir = Math.max(0, response.data.resources.core.remaining - 1000)
+ core.info(`Updating reservoir to: ${reservoir}`)
+ allLimits.updateSettings({ reservoir })
+ }
+ await updateReservoir()
+ // Update remaining requests every minute to account for other jobs running in parallel.
+ const reservoirUpdater = setInterval(updateReservoir, 60 * 1000)
+
+ try {
+ await callback(stats)
+ } finally {
+ clearInterval(reservoirUpdater)
+ core.notice(
+ `Processed ${stats.prs} PRs, ${stats.issues} Issues, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`,
+ )
+ }
+}
diff --git a/archived/projt-launcher/ci/nixpkgs-vet.nix b/archived/projt-launcher/ci/nixpkgs-vet.nix
new file mode 100644
index 0000000000..610a13168b
--- /dev/null
+++ b/archived/projt-launcher/ci/nixpkgs-vet.nix
@@ -0,0 +1,2 @@
+# Deprecated wrapper kept for backwards compatibility.
+args: import ./code-quality.nix args
diff --git a/archived/projt-launcher/ci/nixpkgs-vet.sh b/archived/projt-launcher/ci/nixpkgs-vet.sh
new file mode 100644
index 0000000000..99382c36a8
--- /dev/null
+++ b/archived/projt-launcher/ci/nixpkgs-vet.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+# Deprecated wrapper kept for backwards compatibility.
+set -euo pipefail
+echo "ci/nixpkgs-vet.sh is deprecated; use ci/code-quality.sh instead." >&2
+exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/code-quality.sh" "$@"
+
diff --git a/archived/projt-launcher/ci/parse.nix b/archived/projt-launcher/ci/parse.nix
new file mode 100644
index 0000000000..96652c7a95
--- /dev/null
+++ b/archived/projt-launcher/ci/parse.nix
@@ -0,0 +1,38 @@
+# ProjT Launcher Nix File Parser
+# Validates all .nix files in the project for syntax errors
+
+{
+ lib,
+ nix,
+ runCommand,
+}:
+let
+ nixFiles = lib.fileset.toSource {
+ root = ../.;
+ fileset = lib.fileset.fileFilter (file: file.hasExt "nix") ../.;
+ };
+in
+runCommand "projt-nix-parse-${nix.name}"
+ {
+ nativeBuildInputs = [
+ nix
+ ];
+ }
+ ''
+ export NIX_STORE_DIR=$TMPDIR/store
+ export NIX_STATE_DIR=$TMPDIR/state
+ nix-store --init
+
+ cd "${nixFiles}"
+
+ echo "Parsing Nix files in ProjT Launcher..."
+
+ # Parse all .nix files to check for syntax errors
+ find . -type f -iname '*.nix' | while read file; do
+ echo "Checking: $file"
+ nix-instantiate --parse "$file" >/dev/null
+ done
+
+ echo "All Nix files parsed successfully!"
+ touch $out
+ ''
diff --git a/archived/projt-launcher/ci/pinned.json b/archived/projt-launcher/ci/pinned.json
new file mode 100644
index 0000000000..abcc71877a
--- /dev/null
+++ b/archived/projt-launcher/ci/pinned.json
@@ -0,0 +1,44 @@
+{
+ "dependencies": {
+ "cmake": {
+ "version": "3.28.0",
+ "description": "Build system"
+ },
+ "qt6": {
+ "version": "6.7.0",
+ "description": "Qt framework for UI"
+ },
+ "gcc": {
+ "version": "13.2.0",
+ "description": "GCC compiler"
+ },
+ "clang": {
+ "version": "17.0.0",
+ "description": "Clang compiler"
+ },
+ "ninja": {
+ "version": "1.11.1",
+ "description": "Fast build system"
+ },
+ "gtest": {
+ "version": "1.14.0",
+ "description": "Google Test framework"
+ }
+ },
+ "platforms": {
+ "linux": {
+ "runner": "ubuntu-24.04",
+ "compiler": "gcc"
+ },
+ "macos": {
+ "runner": "macos-14",
+ "compiler": "clang"
+ },
+ "windows": {
+ "runner": "windows-2022",
+ "compiler": "msvc"
+ }
+ },
+ "version": 1,
+ "updated": "2025-12-06"
+}
diff --git a/archived/projt-launcher/ci/supportedBranches.js b/archived/projt-launcher/ci/supportedBranches.js
new file mode 100644
index 0000000000..dc9abb3ff8
--- /dev/null
+++ b/archived/projt-launcher/ci/supportedBranches.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+/**
+ * ProjT Launcher Branch Classifier
+ * Classifies branches for CI/CD purposes
+ */
+
+// Branch type configuration for ProjT Launcher
+const typeConfig = {
+ develop: ['development', 'primary'],
+ master: ['development', 'primary'],
+ main: ['development', 'primary'],
+ release: ['release', 'primary'],
+ feature: ['development', 'feature'],
+ bugfix: ['development', 'bugfix'],
+ hotfix: ['release', 'hotfix'],
+}
+
+// Order ranks branches by priority for base branch selection
+const orderConfig = {
+ develop: 0,
+ master: 1,
+ main: 1,
+ release: 2,
+ feature: 4,
+ bugfix: 4,
+ hotfix: 5,
+}
+
+/**
+ * Split branch name into components
+ * @param {string} branch - Branch name
+ * @returns {object} Parsed branch components
+ */
+function split(branch) {
+ const match = branch.match(
+ /(?<prefix>[a-z_-]+?)(-(?<version>\d+\.\d+\.\d+|v?\d+\.\d+))?(-(?<suffix>.*))?$/i
+ )
+ return match ? match.groups : { prefix: branch }
+}
+
+/**
+ * Classify a branch for CI purposes
+ * @param {string} branch - Branch name
+ * @returns {object} Branch classification
+ */
+function classify(branch) {
+ const { prefix, version, suffix } = split(branch)
+ const normalizedPrefix = prefix.toLowerCase().replace(/-/g, '_')
+
+ return {
+ branch,
+ order: orderConfig[normalizedPrefix] ?? orderConfig[prefix.split('/')[0]] ?? Infinity,
+ isRelease: prefix.startsWith('release') || prefix.startsWith('hotfix'),
+ type: typeConfig[normalizedPrefix] ?? typeConfig[prefix.split('/')[0]] ?? ['wip'],
+ version: version ?? null,
+ suffix: suffix ?? null,
+ }
+}
+
+/**
+ * Check if branch should trigger full CI
+ * @param {string} branch - Branch name
+ * @returns {boolean}
+ */
+function shouldRunFullCI(branch) {
+ const { type } = classify(branch)
+ return type.includes('primary') || type.includes('release')
+}
+
+/**
+ * Get target branch for backport
+ * @param {string} branch - Branch name
+ * @returns {string|null}
+ */
+function getBackportTarget(branch) {
+ const { isRelease, version } = classify(branch)
+ if (isRelease && version) {
+ return `release-${version}`
+ }
+ return null
+}
+
+module.exports = { classify, split, shouldRunFullCI, getBackportTarget }
+
+// CLI tests when run directly
+if (require.main === module) {
+ console.log('ProjT Launcher Branch Classifier Tests\n')
+
+ console.log('split(branch):')
+ const testSplits = ['develop', 'release-1.0.0', 'feature/new-ui', 'hotfix-1.0.1']
+ testSplits.forEach(b => console.log(` ${b}:`, split(b)))
+
+ console.log('\nclassify(branch):')
+ const testClassify = ['develop', 'master', 'release-1.0.0', 'feature/settings', 'bugfix/crash-fix']
+ testClassify.forEach(b => console.log(` ${b}:`, classify(b)))
+
+ console.log('\nshouldRunFullCI(branch):')
+ testClassify.forEach(b => console.log(` ${b}: ${shouldRunFullCI(b)}`))
+}
diff --git a/archived/projt-launcher/ci/supportedSystems.json b/archived/projt-launcher/ci/supportedSystems.json
new file mode 100644
index 0000000000..9441c8a9e4
--- /dev/null
+++ b/archived/projt-launcher/ci/supportedSystems.json
@@ -0,0 +1,64 @@
+{
+ "build": [
+ {
+ "name": "linux-x64",
+ "os": "ubuntu-24.04",
+ "arch": "x86_64",
+ "nix": "x86_64-linux"
+ },
+ {
+ "name": "linux-arm64",
+ "os": "ubuntu-24.04-arm",
+ "arch": "aarch64",
+ "nix": "aarch64-linux"
+ },
+ {
+ "name": "macos-arm64",
+ "os": "macos-14",
+ "arch": "arm64",
+ "nix": "aarch64-darwin"
+ },
+ {
+ "name": "windows-x64",
+ "os": "windows-2022",
+ "arch": "x86_64",
+ "compiler": "msvc"
+ },
+ {
+ "name": "windows-arm64",
+ "os": "windows-11-arm",
+ "arch": "aarch64",
+ "compiler": "msvc"
+ },
+ {
+ "name": "windows-mingw-x64",
+ "os": "windows-2022",
+ "arch": "x86_64",
+ "compiler": "mingw",
+ "msystem": "CLANG64"
+ },
+ {
+ "name": "windows-mingw-arm64",
+ "os": "windows-11-arm",
+ "arch": "aarch64",
+ "compiler": "mingw",
+ "msystem": "CLANGARM64"
+ }
+ ],
+ "test": [
+ "linux-x64",
+ "linux-arm64",
+ "macos-arm64",
+ "windows-x64",
+ "windows-arm64"
+ ],
+ "package": [
+ "linux-x64",
+ "linux-arm64",
+ "macos-arm64",
+ "windows-x64",
+ "windows-arm64",
+ "windows-mingw-x64",
+ "windows-mingw-arm64"
+ ]
+}
diff --git a/archived/projt-launcher/ci/supportedVersions.nix b/archived/projt-launcher/ci/supportedVersions.nix
new file mode 100644
index 0000000000..60d2ca9fe3
--- /dev/null
+++ b/archived/projt-launcher/ci/supportedVersions.nix
@@ -0,0 +1,68 @@
+# ProjT Launcher - Supported Dependency Versions
+# Returns a list of supported Qt and compiler versions for testing
+
+{
+ pkgs ? import <nixpkgs> { },
+}:
+let
+ inherit (pkgs) lib;
+
+ # Supported Qt versions
+ qtVersions = [
+ "qt6Packages" # Qt 6.x (primary)
+ ];
+
+ # Supported compiler versions
+ compilerVersions = {
+ gcc = [
+ "gcc13"
+ "gcc14"
+ ];
+ clang = [
+ "clang_17"
+ "clang_18"
+ ];
+ };
+
+ # Supported CMake versions
+ cmakeVersions = [
+ "cmake" # Latest stable
+ ];
+
+ # Build matrix combinations
+ buildMatrix = lib.flatten (
+ map (
+ qt:
+ map (compiler: {
+ inherit qt;
+ inherit compiler;
+ cmake = "cmake";
+ }) (compilerVersions.gcc ++ compilerVersions.clang)
+ ) qtVersions
+ );
+
+in
+{
+ inherit
+ qtVersions
+ compilerVersions
+ cmakeVersions
+ buildMatrix
+ ;
+
+ # Minimum required versions
+ minimum = {
+ cmake = "3.22";
+ qt = "6.5";
+ gcc = "12";
+ clang = "15";
+ };
+
+ # Recommended versions
+ recommended = {
+ cmake = "3.28";
+ qt = "6.7";
+ gcc = "14";
+ clang = "18";
+ };
+}
diff --git a/archived/projt-launcher/ci/update-pinned.sh b/archived/projt-launcher/ci/update-pinned.sh
new file mode 100644
index 0000000000..105238e2a4
--- /dev/null
+++ b/archived/projt-launcher/ci/update-pinned.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# ProjT Launcher - Update pinned dependency versions
+# Updates ci/pinned.json with current recommended versions
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PINNED_FILE="$SCRIPT_DIR/pinned.json"
+
+echo "Updating ProjT Launcher pinned dependencies..."
+
+# Get current date
+CURRENT_DATE=$(date +%Y-%m-%d)
+
+# Create updated pinned.json
+cat > "$PINNED_FILE" << EOF
+{
+ "dependencies": {
+ "cmake": {
+ "version": "3.28.0",
+ "description": "Build system"
+ },
+ "qt6": {
+ "version": "6.7.0",
+ "description": "Qt framework for UI"
+ },
+ "gcc": {
+ "version": "13.2.0",
+ "description": "GCC compiler"
+ },
+ "clang": {
+ "version": "17.0.0",
+ "description": "Clang compiler"
+ },
+ "ninja": {
+ "version": "1.11.1",
+ "description": "Fast build system"
+ },
+ "gtest": {
+ "version": "1.14.0",
+ "description": "Google Test framework"
+ }
+ },
+ "platforms": {
+ "linux": {
+ "runner": "ubuntu-24.04",
+ "compiler": "gcc"
+ },
+ "macos": {
+ "runner": "macos-14",
+ "compiler": "clang"
+ },
+ "windows": {
+ "runner": "windows-2022",
+ "compiler": "msvc"
+ }
+ },
+ "version": 1,
+ "updated": "$CURRENT_DATE"
+}
+EOF
+
+echo "Updated $PINNED_FILE"
+echo "Date: $CURRENT_DATE"