diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
| commit | d3261e64152397db2dca4d691a990c6bc2a6f4dd (patch) | |
| tree | fac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/ci | |
| parent | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff) | |
| download | Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip | |
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/ci')
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" |
