From d65aaac8e2a1f834ac2e304ba3ba99e11a9f9696 Mon Sep 17 00:00:00 2001 From: Pierre Chalamet Date: Wed, 14 Jan 2026 19:31:42 +0100 Subject: [PATCH 01/22] implement graph (codex) --- Makefile | 6 +- WORKSPACE | 11 + src/Terrabuild.UI/PROJECT | 11 + src/Terrabuild.UI/index.html | 12 + src/Terrabuild.UI/package.json | 27 + src/Terrabuild.UI/pnpm-lock.yaml | 1575 ++++++++++++++++++++++++++ src/Terrabuild.UI/src/App.tsx | 441 ++++++++ src/Terrabuild.UI/src/main.tsx | 10 + src/Terrabuild.UI/src/styles.css | 280 +++++ src/Terrabuild.UI/tsconfig.json | 16 + src/Terrabuild.UI/tsconfig.node.json | 9 + src/Terrabuild.UI/vite.config.ts | 10 + src/Terrabuild/CLI.fs | 11 + src/Terrabuild/Program.fs | 4 + src/Terrabuild/Terrabuild.fsproj | 12 +- src/Terrabuild/Web/GraphServer.fs | 415 +++++++ 16 files changed, 2848 insertions(+), 2 deletions(-) create mode 100644 src/Terrabuild.UI/PROJECT create mode 100644 src/Terrabuild.UI/index.html create mode 100644 src/Terrabuild.UI/package.json create mode 100644 src/Terrabuild.UI/pnpm-lock.yaml create mode 100644 src/Terrabuild.UI/src/App.tsx create mode 100644 src/Terrabuild.UI/src/main.tsx create mode 100644 src/Terrabuild.UI/src/styles.css create mode 100644 src/Terrabuild.UI/tsconfig.json create mode 100644 src/Terrabuild.UI/tsconfig.node.json create mode 100644 src/Terrabuild.UI/vite.config.ts create mode 100644 src/Terrabuild/Web/GraphServer.fs diff --git a/Makefile b/Makefile index c3206f73..21c10604 100644 --- a/Makefile +++ b/Makefile @@ -20,9 +20,13 @@ current_dir = $(shell pwd) # |_______/ |_______| \__/ # -build: +build: webui dotnet build -c $(config) terrabuild.slnx +webui: + cd src/Terrabuild.UI && pnpm install + cd src/Terrabuild.UI && pnpm build + test: dotnet test -c $(config) terrabuild.slnx diff --git a/WORKSPACE b/WORKSPACE index ef734339..5d36bece 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -20,6 +20,7 @@ locals { versions = { dotnet_sdk: "10.0.100" # https://mcr.microsoft.com/artifact/mar/dotnet/sdk/tags + pnpm: "22-10" # https://hub.docker.com/r/guergeiro/pnpm/tags } } @@ -53,3 +54,13 @@ extension @dotnet { evaluate = local.dotnet.evaluate } } + +extension @pnpm { + image = local.is_local_build ? nothing : "docker.io/guergeiro/pnpm:${local.versions.pnpm}" + defaults { + frozen = true + } + env { + CI = true + } +} diff --git a/src/Terrabuild.UI/PROJECT b/src/Terrabuild.UI/PROJECT new file mode 100644 index 00000000..8c9c7710 --- /dev/null +++ b/src/Terrabuild.UI/PROJECT @@ -0,0 +1,11 @@ +project terrabuildui { + @pnpm { } +} + +target install { + @pnpm install { } +} + +target build { + @pnpm build { } +} diff --git a/src/Terrabuild.UI/index.html b/src/Terrabuild.UI/index.html new file mode 100644 index 00000000..f1bba0a4 --- /dev/null +++ b/src/Terrabuild.UI/index.html @@ -0,0 +1,12 @@ + + + + + + Terrabuild Graph + + +
+ + + diff --git a/src/Terrabuild.UI/package.json b/src/Terrabuild.UI/package.json new file mode 100644 index 00000000..6dc084e8 --- /dev/null +++ b/src/Terrabuild.UI/package.json @@ -0,0 +1,27 @@ +{ + "name": "terrabuild-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "packageManager": "pnpm@9.15.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "dagre": "^0.8.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "reactflow": "^11.11.3", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/src/Terrabuild.UI/pnpm-lock.yaml b/src/Terrabuild.UI/pnpm-lock.yaml new file mode 100644 index 00000000..924c80d5 --- /dev/null +++ b/src/Terrabuild.UI/pnpm-lock.yaml @@ -0,0 +1,1575 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dagre: + specifier: ^0.8.5 + version: 0.8.5 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + reactflow: + specifier: ^11.11.3 + version: 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + xterm: + specifier: ^5.3.0 + version: 5.3.0 + xterm-addon-fit: + specifier: ^0.8.0 + version: 0.8.0(xterm@5.3.0) + devDependencies: + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.7.0(vite@5.4.21) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^5.4.11 + version: 5.4.21 + +packages: + + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@reactflow/background@11.3.14': + resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@reactflow/controls@11.2.14': + resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@reactflow/core@11.11.4': + resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@reactflow/minimap@11.7.14': + resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@reactflow/node-resizer@2.2.14': + resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@reactflow/node-toolbar@1.3.14': + resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + baseline-browser-mapping@2.9.14: + resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001764: + resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + dagre@0.8.5: + resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + reactflow@11.11.4: + resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + xterm-addon-fit@0.8.0: + resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==} + deprecated: This package is now deprecated. Move to @xterm/addon-fit instead. + peerDependencies: + xterm: ^5.0.0 + + xterm@5.3.0: + resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + deprecated: This package is now deprecated. Move to @xterm/xterm instead. + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@reactflow/background@11.3.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@reactflow/controls@11.2.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@reactflow/core@11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@types/d3': 7.4.3 + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.11 + '@types/d3-zoom': 3.0.8 + classcat: 5.0.5 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@reactflow/minimap@11.7.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-selection': 3.0.11 + '@types/d3-zoom': 3.0.8 + classcat: 5.0.5 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@reactflow/node-resizer@2.2.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classcat: 5.0.5 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@reactflow/node-toolbar@1.3.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-x64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.9.14: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.14 + caniuse-lite: 1.0.30001764 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + caniuse-lite@1.0.30001764: {} + + classcat@5.0.5: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + dagre@0.8.5: + dependencies: + graphlib: 2.1.8 + lodash: 4.17.21 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + electron-to-chromium@1.5.267: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + reactflow@11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@reactflow/background': 11.3.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/controls': 11.2.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/minimap': 11.7.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-resizer': 2.2.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + rollup@4.55.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 + fsevents: 2.3.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.55.1 + optionalDependencies: + fsevents: 2.3.3 + + xterm-addon-fit@0.8.0(xterm@5.3.0): + dependencies: + xterm: 5.3.0 + + xterm@5.3.0: {} + + yallist@3.1.1: {} + + zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react: 18.3.1 diff --git a/src/Terrabuild.UI/src/App.tsx b/src/Terrabuild.UI/src/App.tsx new file mode 100644 index 00000000..61479ff4 --- /dev/null +++ b/src/Terrabuild.UI/src/App.tsx @@ -0,0 +1,441 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import ReactFlow, { Background, Controls, Node, Edge } from "reactflow"; +import "reactflow/dist/style.css"; +import dagre from "dagre"; +import { Terminal } from "xterm"; +import { FitAddon } from "xterm-addon-fit"; +import "xterm/css/xterm.css"; + +type ProjectInfo = { + id: string; + name?: string | null; + directory: string; + hash: string; +}; + +type GraphNode = { + id: string; + projectId: string; + projectName?: string | null; + projectDir: string; + target: string; + dependencies: string[]; + projectHash: string; + targetHash: string; +}; + +type GraphResponse = { + nodes: Record; +}; + +type TargetSummary = { + project: string; + target: string; + isSuccessful: boolean; + startedAt: string; + endedAt: string; + duration: string; + cache: string; + outputs?: string | null; +}; + +const nodeWidth = 240; +const nodeHeight = 80; + +const layoutGraph = (nodes: Node[], edges: Edge[]) => { + const graph = new dagre.graphlib.Graph(); + graph.setDefaultEdgeLabel(() => ({})); + graph.setGraph({ rankdir: "LR", nodesep: 50, ranksep: 80 }); + + nodes.forEach((node) => { + graph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + edges.forEach((edge) => graph.setEdge(edge.source, edge.target)); + + dagre.layout(graph); + + const layoutedNodes = nodes.map((node) => { + const position = graph.node(node.id); + return { + ...node, + position: { + x: position.x - nodeWidth / 2, + y: position.y - nodeHeight / 2, + }, + }; + }); + + return { nodes: layoutedNodes, edges }; +}; + +const App = () => { + const [targets, setTargets] = useState([]); + const [projects, setProjects] = useState([]); + const [selectedTargets, setSelectedTargets] = useState([]); + const [selectedProjects, setSelectedProjects] = useState([]); + const [graph, setGraph] = useState(null); + const [graphError, setGraphError] = useState(null); + const [buildError, setBuildError] = useState(null); + const [buildRunning, setBuildRunning] = useState(false); + const [forceBuild, setForceBuild] = useState(false); + const [retryBuild, setRetryBuild] = useState(false); + const [parallelism, setParallelism] = useState(""); + const [selectedNode, setSelectedNode] = useState(null); + const [nodeResults, setNodeResults] = useState>( + {} + ); + + const terminalRef = useRef(null); + const terminal = useRef(null); + const fitAddon = useRef(null); + const logAbort = useRef(null); + + useEffect(() => { + const term = new Terminal({ + convertEol: false, + scrollback: 3000, + fontSize: 12, + theme: { + background: "#0e0f12", + foreground: "#d8e1e6", + selectionBackground: "#414b57", + }, + }); + const fit = new FitAddon(); + term.loadAddon(fit); + terminal.current = term; + fitAddon.current = fit; + if (terminalRef.current) { + term.open(terminalRef.current); + fit.fit(); + } + const handleResize = () => fit.fit(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + term.dispose(); + }; + }, []); + + useEffect(() => { + const load = async () => { + const [targetsRes, projectsRes] = await Promise.all([ + fetch("/api/targets"), + fetch("/api/projects"), + ]); + if (targetsRes.ok) { + setTargets(await targetsRes.json()); + } + if (projectsRes.ok) { + setProjects(await projectsRes.json()); + } + }; + load().catch(() => null); + }, []); + + useEffect(() => { + const fetchGraph = async () => { + if (selectedTargets.length === 0) { + setGraph(null); + setGraphError("Select at least one target to load the graph."); + return; + } + setGraphError(null); + const params = new URLSearchParams(); + selectedTargets.forEach((target) => params.append("targets", target)); + selectedProjects.forEach((project) => params.append("projects", project)); + const response = await fetch(`/api/graph?${params.toString()}`); + if (!response.ok) { + setGraphError(await response.text()); + setGraph(null); + return; + } + const data = (await response.json()) as GraphResponse; + setGraph(data); + }; + fetchGraph().catch(() => { + setGraphError("Failed to load graph."); + setGraph(null); + }); + }, [selectedTargets, selectedProjects]); + + const { nodes, edges } = useMemo(() => { + if (!graph) { + return { nodes: [], edges: [] }; + } + const rawNodes = Object.values(graph.nodes); + const flowNodes: Node[] = rawNodes.map((node) => ({ + id: node.id, + data: { + label: `${node.projectName ?? node.projectId} - ${node.target}`, + meta: node, + }, + position: { x: 0, y: 0 }, + className: "tb-node", + })); + const flowEdges: Edge[] = rawNodes.flatMap((node) => + node.dependencies.map((dependency) => ({ + id: `${dependency}-${node.id}`, + source: dependency, + target: node.id, + type: "smoothstep", + className: "tb-edge", + })) + ); + return layoutGraph(flowNodes, flowEdges); + }, [graph]); + + const handleSelectTargets = (event: React.ChangeEvent) => { + const values = Array.from(event.target.selectedOptions).map( + (option) => option.value + ); + setSelectedTargets(values); + }; + + const handleSelectProjects = (event: React.ChangeEvent) => { + const values = Array.from(event.target.selectedOptions).map( + (option) => option.value + ); + setSelectedProjects(values); + }; + + const startLogStream = async () => { + if (!terminal.current) { + return; + } + terminal.current.reset(); + logAbort.current?.abort(); + const controller = new AbortController(); + logAbort.current = controller; + + const response = await fetch("/api/build/log", { signal: controller.signal }); + if (!response.body) { + return; + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + if (value) { + const chunk = decoder.decode(value, { stream: true }); + terminal.current.write(chunk); + } + } + setBuildRunning(false); + }; + + const startBuild = async () => { + if (selectedTargets.length === 0 || buildRunning) { + return; + } + setBuildRunning(true); + setBuildError(null); + const parallel = + parallelism.trim().length > 0 ? Number(parallelism) : null; + const payload = { + targets: selectedTargets, + projects: selectedProjects.length > 0 ? selectedProjects : null, + parallelism: parallel && parallel > 0 ? parallel : null, + force: forceBuild, + retry: retryBuild, + }; + const response = await fetch("/api/build", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + setBuildRunning(false); + setBuildError(await response.text()); + return; + } + startLogStream().catch(() => { + setBuildRunning(false); + }); + }; + + const loadNodeResult = async (node: GraphNode) => { + setSelectedNode(node); + const cacheKey = `${node.projectHash}/${node.target}/${node.targetHash}`; + if (nodeResults[cacheKey]) { + return; + } + const response = await fetch( + `/api/build/result/${node.projectHash}/${node.target}/${node.targetHash}` + ); + if (!response.ok) { + return; + } + const summary = (await response.json()) as TargetSummary; + setNodeResults((prev) => ({ ...prev, [cacheKey]: summary })); + }; + + const selectedResult = + selectedNode && + nodeResults[ + `${selectedNode.projectHash}/${selectedNode.target}/${selectedNode.targetHash}` + ]; + + return ( +
+ +
+
+
+

Execution Graph

+ {graphError && {graphError}} +
+
+ {graph ? ( + + loadNodeResult(node.data.meta as GraphNode) + } + > + + + + ) : ( +
+

Select at least one target to view the graph.

+
+ )} +
+
+
+
+

Build Log

+ + {buildRunning ? "Live" : "Idle"} + +
+
+
+
+
+ ); +}; + +export default App; diff --git a/src/Terrabuild.UI/src/main.tsx b/src/Terrabuild.UI/src/main.tsx new file mode 100644 index 00000000..16958a20 --- /dev/null +++ b/src/Terrabuild.UI/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/Terrabuild.UI/src/styles.css b/src/Terrabuild.UI/src/styles.css new file mode 100644 index 00000000..529d7642 --- /dev/null +++ b/src/Terrabuild.UI/src/styles.css @@ -0,0 +1,280 @@ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap"); + +:root { + color-scheme: light; + font-family: "Space Grotesk", "Segoe UI", sans-serif; + --bg: #0f131a; + --bg-alt: #111822; + --panel: #141c27; + --panel-border: #223042; + --text: #e8eef3; + --muted: #9aa6b2; + --accent: #f4b34f; + --accent-strong: #f48e39; + --accent-soft: rgba(244, 179, 79, 0.15); + --success: #55d68d; + --danger: #ff6d6d; + --shadow: 0 24px 60px rgba(5, 10, 16, 0.45); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: radial-gradient(circle at top, #1b2533 0%, #0f131a 55%); + color: var(--text); +} + +#root { + min-height: 100vh; +} + +.app { + display: grid; + grid-template-columns: minmax(260px, 340px) 1fr; + min-height: 100vh; +} + +.panel { + padding: 32px 28px; + background: linear-gradient(160deg, #182231 0%, #0f141d 80%); + border-right: 1px solid var(--panel-border); + display: flex; + flex-direction: column; + gap: 24px; + box-shadow: var(--shadow); +} + +.panel-header h1 { + margin: 0; + font-size: 28px; + font-weight: 700; + letter-spacing: 0.02em; +} + +.eyebrow { + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.3em; + color: var(--accent); + margin: 0 0 8px; +} + +.panel-section { + display: grid; + gap: 16px; +} + +.panel-section h2 { + margin: 0 0 8px; + font-size: 16px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--muted); +} + +label { + display: grid; + gap: 8px; + font-size: 13px; + color: var(--muted); +} + +select, +input[type="number"] { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--panel-border); + background: var(--panel); + color: var(--text); + font-size: 14px; + font-family: inherit; +} + +select { + min-height: 120px; +} + +select option:checked { + background: var(--accent-soft); +} + +.row { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.checkbox { + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 13px; + color: var(--text); +} + +.checkbox input { + accent-color: var(--accent); +} + +button.primary { + padding: 12px 16px; + border-radius: 999px; + border: none; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #1a1206; + font-weight: 700; + font-size: 14px; + cursor: pointer; + letter-spacing: 0.05em; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +button.primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +button.primary:not(:disabled):hover { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(244, 142, 57, 0.35); +} + +.error { + color: var(--danger); + font-size: 13px; +} + +.muted { + color: var(--muted); + font-size: 13px; +} + +.details .detail-card { + background: rgba(18, 26, 38, 0.7); + border: 1px solid var(--panel-border); + border-radius: 16px; + padding: 16px; + display: grid; + gap: 10px; +} + +.detail-card h3 { + margin: 0; + font-size: 16px; +} + +.detail-row { + display: flex; + justify-content: space-between; + font-size: 13px; + color: var(--muted); +} + +.detail-row span:last-child { + color: var(--text); +} + +.ok { + color: var(--success); +} + +.fail { + color: var(--danger); +} + +.workspace { + display: grid; + grid-template-rows: minmax(0, 1fr) 280px; + gap: 18px; + padding: 24px; +} + +.graph, +.terminal { + background: var(--bg-alt); + border-radius: 20px; + border: 1px solid var(--panel-border); + box-shadow: var(--shadow); + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; +} + +.graph header, +.terminal header { + padding: 16px 20px; + border-bottom: 1px solid var(--panel-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.graph h2, +.terminal h2 { + margin: 0; + font-size: 16px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.graph-canvas { + height: 100%; +} + +.graph-empty { + height: 100%; + display: grid; + place-items: center; + color: var(--muted); +} + +.terminal-body { + font-family: "Fira Code", monospace; + height: 100%; +} + +.status { + font-size: 12px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.2em; +} + +.status.live { + color: var(--accent); +} + +.tb-node { + border-radius: 14px; + border: 1px solid rgba(244, 179, 79, 0.35); + background: linear-gradient(160deg, #1a2432 0%, #131b27 80%); + color: var(--text); + font-size: 12px; + padding: 6px; +} + +.tb-edge path { + stroke: rgba(244, 179, 79, 0.5); +} + +@media (max-width: 980px) { + .app { + grid-template-columns: 1fr; + } + + .panel { + border-right: none; + border-bottom: 1px solid var(--panel-border); + } + + .workspace { + grid-template-rows: 420px 260px; + } +} diff --git a/src/Terrabuild.UI/tsconfig.json b/src/Terrabuild.UI/tsconfig.json new file mode 100644 index 00000000..a823d001 --- /dev/null +++ b/src/Terrabuild.UI/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "strict": true, + "jsx": "react-jsx", + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/src/Terrabuild.UI/tsconfig.node.json b/src/Terrabuild.UI/tsconfig.node.json new file mode 100644 index 00000000..16dfedc6 --- /dev/null +++ b/src/Terrabuild.UI/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/Terrabuild.UI/vite.config.ts b/src/Terrabuild.UI/vite.config.ts new file mode 100644 index 00000000..5e902133 --- /dev/null +++ b/src/Terrabuild.UI/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/src/Terrabuild/CLI.fs b/src/Terrabuild/CLI.fs index b88d067d..bae8f504 100644 --- a/src/Terrabuild/CLI.fs +++ b/src/Terrabuild/CLI.fs @@ -103,6 +103,15 @@ with | Type _-> "Select projects based on extension types." | Project _ -> "Select projets base on id." +[] +type GraphArgs = + | [] Workspace of path:string +with + interface IArgParserTemplate with + member this.Usage = + match this with + | Workspace _ -> "Root of workspace. If not specified, current directory is used." + [] type ClearArgs = @@ -145,6 +154,7 @@ type TerrabuildArgs = | [] Logs of ParseResults | [] Run of ParseResults | [] Serve of ParseResults + | [] Graph of ParseResults | [] Clear of ParseResults | [] Login of ParseResults | [] Logout of ParseResults @@ -159,6 +169,7 @@ with | Logs _ -> "dump logs." | Run _ -> "Run specified targets." | Serve _ -> "Serve specified targets." + | Graph _ -> "Visualize and build graph in a web UI." | Clear _ -> "Clear specified caches." | Login _ -> "Connect to backend." | Logout _ -> "Disconnect from backend." diff --git a/src/Terrabuild/Program.fs b/src/Terrabuild/Program.fs index 54c238a9..44c26919 100644 --- a/src/Terrabuild/Program.fs +++ b/src/Terrabuild/Program.fs @@ -300,6 +300,9 @@ let processCommandLine (parser: ArgumentParser) (result: ParseRe RunTargetOptions.Engine = None } runTarget options + let graph (graphArgs: ParseResults) = + GraphServer.start graphArgs + let logs (logsArgs: ParseResults) = let targets = logsArgs.GetResult(LogsArgs.Target) |> Seq.map String.toLower let wsDir = @@ -366,6 +369,7 @@ let processCommandLine (parser: ArgumentParser) (result: ParseRe | p when p.Contains(TerrabuildArgs.Logs) -> p.GetResult(TerrabuildArgs.Logs) |> logs | p when p.Contains(TerrabuildArgs.Run) -> p.GetResult(TerrabuildArgs.Run) |> run | p when p.Contains(TerrabuildArgs.Serve) -> p.GetResult(TerrabuildArgs.Serve) |> serve + | p when p.Contains(TerrabuildArgs.Graph) -> p.GetResult(TerrabuildArgs.Graph) |> graph | p when p.Contains(TerrabuildArgs.Clear) -> p.GetResult(TerrabuildArgs.Clear) |> clear | p when p.Contains(TerrabuildArgs.Login) -> p.GetResult(TerrabuildArgs.Login) |> login | p when p.Contains(TerrabuildArgs.Logout) -> p.GetResult(TerrabuildArgs.Logout) |> logout diff --git a/src/Terrabuild/Terrabuild.fsproj b/src/Terrabuild/Terrabuild.fsproj index ef73d5e1..a43eba9f 100644 --- a/src/Terrabuild/Terrabuild.fsproj +++ b/src/Terrabuild/Terrabuild.fsproj @@ -41,6 +41,7 @@ + @@ -54,6 +55,7 @@ + @@ -74,6 +76,14 @@ + + + PreserveNewest + PreserveNewest + ui\%(RecursiveDir)%(Filename)%(Extension) + + + Terrabuild @@ -89,4 +99,4 @@ Terrabuild.png - \ No newline at end of file + diff --git a/src/Terrabuild/Web/GraphServer.fs b/src/Terrabuild/Web/GraphServer.fs new file mode 100644 index 00000000..08927024 --- /dev/null +++ b/src/Terrabuild/Web/GraphServer.fs @@ -0,0 +1,415 @@ +module GraphServer + +open System +open System.IO +open System.Text +open System.Diagnostics +open System.Threading +open System.Threading.Channels +open System.Threading.Tasks +open System.Collections.Concurrent +open System.Runtime.InteropServices +open Argu +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.FileProviders +open Microsoft.Extensions.Hosting +open Collections +open Environment +open CLI +open Errors + +type BuildRequest = { + Targets: string list + Projects: string list option + Parallelism: int option + Force: bool + Retry: bool +} + +type ProjectInfo = { + Id: string + Name: string option + Directory: string + Hash: string +} + +type BuildLogState = { + Buffer: StringBuilder + Subscribers: ConcurrentDictionary> + Lock: obj +} + +type BuildState = { + mutable Active: Process option + mutable ExitCode: int option + Lock: obj +} + +let rec private findWorkspace dir = + if FS.combinePath dir "WORKSPACE" |> IO.exists then + Some dir + else + dir |> FS.parentDirectory |> Option.bind findWorkspace + +let private createLogState () = + { Buffer = StringBuilder() + Subscribers = ConcurrentDictionary>() + Lock = obj() } + +let private createBuildState () = + { Active = None + ExitCode = None + Lock = obj() } + +let private appendLog (state: BuildLogState) (text: string) = + if String.IsNullOrEmpty(text) |> not then + lock state.Lock (fun () -> + state.Buffer.Append(text) |> ignore + ) + for KeyValue(_, channel) in state.Subscribers do + channel.Writer.TryWrite(text) |> ignore + +let private clearLog (state: BuildLogState) = + lock state.Lock (fun () -> + state.Buffer.Clear() |> ignore + ) + +let private completeLogStreams (state: BuildLogState) = + for KeyValue(_, channel) in state.Subscribers do + channel.Writer.TryComplete() |> ignore + +let private logSnapshot (state: BuildLogState) = + lock state.Lock (fun () -> state.Buffer.ToString()) + +let private parseCsvValues (values: Microsoft.Extensions.Primitives.StringValues) = + values.ToArray() + |> Array.choose (fun (value: string | null) -> + if isNull value || value = "" then None else Some (string value)) + |> Array.collect (fun (value: string) -> + value.Split(',', StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)) + |> Array.toList + +let private readBody (ctx: HttpContext) = + task { + use reader = new StreamReader(ctx.Request.Body, Encoding.UTF8) + return! reader.ReadToEndAsync() + } + +let private buildConfig (workspace: string) (targets: string list) (projects: string list option) = + let previousDir = System.Environment.CurrentDirectory + System.Environment.CurrentDirectory <- workspace + try + let homeDir = Cache.createHome() + let tmpDir = Cache.createTmp() + let sharedDir = ".terrabuild" + IO.createDirectory sharedDir + let sourceControl = SourceControls.Factory.create() + let options = + { ConfigOptions.Options.Workspace = workspace + ConfigOptions.Options.HomeDir = homeDir + ConfigOptions.Options.TmpDir = tmpDir + ConfigOptions.Options.SharedDir = sharedDir + ConfigOptions.Options.WhatIf = false + ConfigOptions.Options.Debug = false + ConfigOptions.Options.MaxConcurrency = 1 + ConfigOptions.Options.Force = false + ConfigOptions.Options.Retry = false + ConfigOptions.Options.LocalOnly = true + ConfigOptions.Options.StartedAt = DateTime.UtcNow + ConfigOptions.Options.Targets = targets |> Seq.map String.toLower |> Set + ConfigOptions.Options.LogTypes = sourceControl.LogTypes + ConfigOptions.Options.Configuration = None + ConfigOptions.Options.Environment = None + ConfigOptions.Options.Note = None + ConfigOptions.Options.Label = None + ConfigOptions.Options.Types = None + ConfigOptions.Options.Labels = None + ConfigOptions.Options.Projects = projects |> Option.map (fun items -> items |> Seq.map String.toLower |> Set) + ConfigOptions.Options.Variables = Map.empty + ConfigOptions.Options.Engine = None + ConfigOptions.Options.HeadCommit = sourceControl.HeadCommit + ConfigOptions.Options.CommitLog = sourceControl.CommitLog + ConfigOptions.Options.BranchOrTag = sourceControl.BranchOrTag + ConfigOptions.Options.Run = sourceControl.Run } + Configuration.read options + finally + System.Environment.CurrentDirectory <- previousDir + +let private buildBatchGraph workspace targets projects = + let options, config = buildConfig workspace targets projects + let cache = Cache.Cache(Storages.Factory.create None, None) :> Cache.ICache + let options = { options with MaxConcurrency = System.Environment.ProcessorCount |> max 1 } + let graph = GraphPipeline.Node.build options config + let graph = GraphPipeline.Action.build options cache graph + let graph = GraphPipeline.Cascade.build graph + GraphPipeline.Batch.build options config graph + +let private resolveWorkspace (workspace: string option) = + match workspace with + | Some ws -> ws + | _ -> + match Environment.currentDir() |> findWorkspace with + | Some ws -> ws + | _ -> raiseInvalidArg "Can't find workspace root directory. Check you are in a workspace." + +let private pickPort () = + let listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0) + listener.Start() + let port = (listener.LocalEndpoint :?> Net.IPEndPoint).Port + listener.Stop() + port + +let private isDotnetHost (path: string) = + let file = + Path.GetFileName(path) + |> Option.ofObj + |> Option.defaultValue "" + |> fun name -> name.ToLowerInvariant() + file = "dotnet" || file = "dotnet.exe" + +let private openBrowser (url: string) = + try + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then + Process.Start(ProcessStartInfo("cmd", $"/c start {url}")) |> ignore + elif RuntimeInformation.IsOSPlatform(OSPlatform.OSX) then + Process.Start(ProcessStartInfo("open", url)) |> ignore + else + Process.Start(ProcessStartInfo("xdg-open", url)) |> ignore + with _ -> () + +let private createBuildCommand (workspace: string) (request: BuildRequest) = + let exePath = System.Environment.ProcessPath |> Option.ofObj |> Option.defaultValue "dotnet" + let assemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location + let targets = request.Targets |> Seq.map String.toLower |> String.join " " + let projectArgs = + match request.Projects with + | Some projects when projects.Length > 0 -> + projects |> Seq.map String.toLower |> String.join " " |> sprintf " -p %s" + | _ -> "" + let parallelArg = + match request.Parallelism with + | Some value when value > 0 -> $" --parallel {value}" + | _ -> "" + let forceArg = if request.Force then " -f" else "" + let retryArg = if request.Retry then " -r" else "" + let baseArgs = $"run {targets} -w \"{workspace}\"{projectArgs}{parallelArg}{forceArg}{retryArg}" + if isDotnetHost exePath then + exePath, $"\"{assemblyPath}\" {baseArgs}" + else + exePath, baseArgs + +let private startBuildProcess (workspace: string) (request: BuildRequest) (logState: BuildLogState) (buildState: BuildState) = + lock buildState.Lock (fun () -> + match buildState.Active with + | Some proc when not proc.HasExited -> Error "Build already running." + | _ -> + clearLog logState + completeLogStreams logState + let command, args = createBuildCommand workspace request + let psi = + ProcessStartInfo( + FileName = command, + Arguments = args, + WorkingDirectory = workspace, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + ) + let proc = new Process(StartInfo = psi, EnableRaisingEvents = true) + if proc.Start() |> not then + Error "Failed to start build process." + else + buildState.Active <- Some proc + buildState.ExitCode <- None + + let streamToLog (stream: Stream) = + task { + use reader = new StreamReader(stream, Encoding.UTF8) + let buffer = Array.zeroCreate 4096 + let mutable keepReading = true + while keepReading do + let! read = reader.ReadAsync(buffer, 0, buffer.Length) + if read = 0 then + keepReading <- false + else + appendLog logState (String(buffer, 0, read)) + } + + streamToLog proc.StandardOutput.BaseStream |> ignore + streamToLog proc.StandardError.BaseStream |> ignore + + proc.Exited.Add(fun _ -> + buildState.ExitCode <- Some proc.ExitCode + buildState.Active <- None + completeLogStreams logState + ) + Ok proc.Id + ) + +let start (graphArgs: ParseResults) = + let workspace = + graphArgs.TryGetResult(CLI.GraphArgs.Workspace) + |> resolveWorkspace + let uiRoot = Path.Combine(AppContext.BaseDirectory, "ui") + let port = pickPort() + let url = $"http://127.0.0.1:{port}" + let builder = WebApplication.CreateBuilder() + builder.WebHost.UseUrls(url) |> ignore + + let app = builder.Build() + + let logState = createLogState() + let buildState = createBuildState() + let workspaceLock = obj() + + let fileProvider = new PhysicalFileProvider(uiRoot) + let defaultFiles = + DefaultFilesOptions( + FileProvider = fileProvider, + RequestPath = PathString.Empty + ) + defaultFiles.DefaultFileNames.Clear() + defaultFiles.DefaultFileNames.Add("index.html") + + app.UseDefaultFiles(defaultFiles) |> ignore + app.UseStaticFiles(StaticFileOptions(FileProvider = fileProvider, RequestPath = PathString.Empty)) |> ignore + + app.MapGet("/api/targets", Func>(fun _ -> + task { + let targets = + lock workspaceLock (fun () -> + let _, config = buildConfig workspace [] None + config.Targets |> Map.keys |> Seq.sort |> Seq.toList + ) + let json = Json.Serialize targets + return Results.Text(json, "application/json") + })) + |> ignore + + app.MapGet("/api/projects", Func>(fun _ -> + task { + let projects = + lock workspaceLock (fun () -> + let _, config = buildConfig workspace [] None + config.Projects + |> Map.toList + |> List.map (fun (_, project) -> + { ProjectInfo.Id = project.Id + ProjectInfo.Name = project.Name + ProjectInfo.Directory = project.Directory + ProjectInfo.Hash = project.Hash }) + |> List.sortBy (fun project -> project.Id) + ) + let json = Json.Serialize projects + return Results.Text(json, "application/json") + })) + |> ignore + + app.MapGet("/api/graph", Func>(fun ctx -> + task { + let targets = ctx.Request.Query.["targets"] |> parseCsvValues + let projects = ctx.Request.Query.["projects"] |> parseCsvValues |> function | [] -> None | values -> Some values + if targets.IsEmpty then + return Results.BadRequest("At least one target is required.") + else + let graph = + lock workspaceLock (fun () -> + buildBatchGraph workspace targets projects + ) + let json = Json.Serialize graph + return Results.Text(json, "application/json") + })) + |> ignore + + app.MapPost("/api/build", Func>(fun ctx -> + task { + let! body = readBody ctx + let request = + try + Json.Deserialize body |> Ok + with ex -> + Error ex.Message + match request with + | Error err -> return Results.BadRequest(err) + | Ok request -> + if request.Targets.IsEmpty then + return Results.BadRequest("At least one target is required.") + else + match startBuildProcess workspace request logState buildState with + | Ok pid -> return Results.Json {| pid = pid; startedAt = DateTime.UtcNow |} + | Error reason -> return Results.Conflict(reason) + })) + |> ignore + + app.MapGet("/api/build/log", Func(fun ctx -> + task { + ctx.Response.Headers.CacheControl <- "no-cache" + ctx.Response.Headers.ContentType <- "text/plain" + let id = Guid.NewGuid() + let channel = Channel.CreateUnbounded() + logState.Subscribers.TryAdd(id, channel) |> ignore + + let snapshot = logSnapshot logState + if String.IsNullOrEmpty(snapshot) |> not then + let bytes = Encoding.UTF8.GetBytes(snapshot) + do! ctx.Response.Body.WriteAsync(bytes, 0, bytes.Length, ctx.RequestAborted) + do! ctx.Response.Body.FlushAsync(ctx.RequestAborted) + + try + let reader = channel.Reader + let mutable keepReading = true + while keepReading && not ctx.RequestAborted.IsCancellationRequested do + let! next = reader.ReadAsync(ctx.RequestAborted) + let bytes = Encoding.UTF8.GetBytes(next) + do! ctx.Response.Body.WriteAsync(bytes, 0, bytes.Length, ctx.RequestAborted) + do! ctx.Response.Body.FlushAsync(ctx.RequestAborted) + with + | :? OperationCanceledException -> () + | :? ChannelClosedException -> () + + let mutable removed = Unchecked.defaultof> + logState.Subscribers.TryRemove(id, &removed) |> ignore + })) + |> ignore + + app.MapGet("/api/build/result/{projectHash}/{targetName}/{targetHash}", Func>(fun ctx -> + task { + let getRouteValue name = + match ctx.Request.RouteValues.TryGetValue(name) with + | true, null -> None + | true, (:? string as value) -> Some value + | _ -> None + + match getRouteValue "projectHash", getRouteValue "targetName", getRouteValue "targetHash" with + | Some projectHash, Some targetName, Some targetHash -> + let cacheKey = $"{projectHash}/{targetName}/{targetHash}" + let cache = Cache.Cache(Storages.Factory.create None, None) :> Cache.ICache + match cache.TryGetSummary false cacheKey with + | Some summary -> + let json = Json.Serialize summary + return Results.Text(json, "application/json") + | None -> return Results.NotFound() + | _ -> + return Results.BadRequest("Missing route values.") + })) + |> ignore + + app.MapFallback(Func(fun ctx -> + task { + let indexFile = Path.Combine(uiRoot, "index.html") + if File.Exists(indexFile) then + ctx.Response.ContentType <- "text/html" + let! html = File.ReadAllTextAsync(indexFile) + do! ctx.Response.WriteAsync(html) + else + ctx.Response.StatusCode <- 404 + do! ctx.Response.WriteAsync("UI assets not found. Build Terrabuild.UI first.") + })) + |> ignore + + let runTask = app.RunAsync() + openBrowser url + runTask.Wait() + 0 From c7cacfd0e9db14171e63d5c2f6f8257b09dc64af Mon Sep 17 00:00:00 2001 From: Pierre Chalamet Date: Wed, 14 Jan 2026 19:45:32 +0100 Subject: [PATCH 02/22] light/dark mode, --no-open --- .gitignore | 1 + Makefile | 3 ++ src/Terrabuild.UI/src/App.tsx | 13 +++++++ src/Terrabuild.UI/src/styles.css | 60 +++++++++++++++++++++++++++++++ src/Terrabuild/CLI.fs | 2 ++ src/Terrabuild/Web/GraphServer.fs | 4 ++- 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ee955b40..8ec76fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -422,3 +422,4 @@ terrabuild-debug.md *.fsyacc.output !**/results/terrabuild-debug.* +.pnpm-store diff --git a/Makefile b/Makefile index 21c10604..8cd4029c 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,9 @@ terrabuild: # |__| |_______|_______/ |__| |_______/ # +ui: webui + $(terrabuild) graph --no-open + test-scaffold: $(terrabuild) scaffold --workspace tests/scaffold --debug --log diff --git a/src/Terrabuild.UI/src/App.tsx b/src/Terrabuild.UI/src/App.tsx index 61479ff4..17602ece 100644 --- a/src/Terrabuild.UI/src/App.tsx +++ b/src/Terrabuild.UI/src/App.tsx @@ -84,6 +84,11 @@ const App = () => { const [nodeResults, setNodeResults] = useState>( {} ); + const [theme, setTheme] = useState<"dark" | "light">("dark"); + + useEffect(() => { + document.body.dataset.theme = theme; + }, [theme]); const terminalRef = useRef(null); const terminal = useRef(null); @@ -287,6 +292,14 @@ const App = () => {

Terrabuild

Graph Console

+