From 833c85f4ac05270af5360047fd7199961b02f385 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Thu, 19 Mar 2026 12:50:13 +0100 Subject: [PATCH] bench: Cloud benchmark --- README.md | 45 + bench/history.jsonl | 10 +- devenv.nix | 6 + package-lock.json | 1141 +++++++++---------------- package.json | 2 +- scripts/cloud_bench_orchestrate.mjs | 1192 +++++++++++++++++++++++++++ scripts/run_bench_cloud.sh | 205 +++++ scripts/run_bench_collect.sh | 12 + scripts/run_bench_update.sh | 352 ++++---- 9 files changed, 2036 insertions(+), 929 deletions(-) create mode 100755 scripts/cloud_bench_orchestrate.mjs create mode 100755 scripts/run_bench_cloud.sh diff --git a/README.md b/README.md index 9b6509f..780aa5a 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,51 @@ Run it with: mix bench ``` +### Cloud benchmark (Hetzner Cloud) + +For distributed runs (one server node + multiple client nodes), use: + +```bash +./scripts/run_bench_cloud.sh +``` + +or invoke the orchestrator directly: + +```bash +node scripts/cloud_bench_orchestrate.mjs +``` + +Prerequisites: + +- [`hcloud`](https://github.com/hetznercloud/cli) CLI installed +- Hetzner Cloud token exported as `HCLOUD_TOKEN` +- local `docker`, `git`, `ssh`, and `scp` available + +Example: + +```bash +export HCLOUD_TOKEN=... +./scripts/run_bench_cloud.sh --quick +``` + +Outputs: + +- raw client logs per run: `bench/cloud_artifacts//...` +- JSONL history entries (local + cloud): `bench/history.jsonl` + +Useful history/render commands: + +```bash +# List available machines and runs in history +./scripts/run_bench_update.sh --list + +# Regenerate chart + README table for a machine +./scripts/run_bench_update.sh + +# Regenerate from all machines +./scripts/run_bench_update.sh all +``` + Current comparison results: | metric | parrhesia-pg | parrhesia-mem | strfry | nostr-rs-relay | mem/pg | strfry/pg | nostr-rs/pg | diff --git a/bench/history.jsonl b/bench/history.jsonl index 77c9cdf..6432d52 100644 --- a/bench/history.jsonl +++ b/bench/history.jsonl @@ -1,5 +1,5 @@ -{"timestamp":"2026-03-18T22:14:37Z","machine_id":"agent","git_tag":"v0.2.0","git_commit":"b20dbf6","runs":3,"versions":{"parrhesia":"0.2.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":14.666666666666666,"connect_max_ms":25.666666666666668,"echo_tps":77133,"echo_mibs":42.233333333333334,"event_tps":1602.6666666666667,"event_mibs":1.0666666666666667,"req_tps":2418,"req_mibs":12.5},"parrhesia-memory":{"connect_avg_ms":9,"connect_max_ms":16,"echo_tps":64218.333333333336,"echo_mibs":35.166666666666664,"event_tps":1578.3333333333333,"event_mibs":1,"req_tps":2431.3333333333335,"req_mibs":12.633333333333333},"strfry":{"connect_avg_ms":3.3333333333333335,"connect_max_ms":6,"echo_tps":63682.666666666664,"echo_mibs":35.6,"event_tps":3477.3333333333335,"event_mibs":2.2333333333333334,"req_tps":1804,"req_mibs":11.733333333333334},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.333333333333333,"echo_tps":160009,"echo_mibs":87.63333333333333,"event_tps":762,"event_mibs":0.4666666666666666,"req_tps":831,"req_mibs":2.2333333333333334}}} -{"timestamp":"2026-03-18T22:22:12Z","machine_id":"agent","git_tag":"v0.3.0","git_commit":"8c8d5a8","runs":3,"versions":{"parrhesia":"0.3.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":13,"connect_max_ms":21.666666666666668,"echo_tps":70703.33333333333,"echo_mibs":38.7,"event_tps":1970.6666666666667,"event_mibs":1.3,"req_tps":3614,"req_mibs":20.966666666666665},"parrhesia-memory":{"connect_avg_ms":13,"connect_max_ms":22.333333333333332,"echo_tps":60452.333333333336,"echo_mibs":33.1,"event_tps":1952.6666666666667,"event_mibs":1.3,"req_tps":3616,"req_mibs":20.766666666666666},"strfry":{"connect_avg_ms":3.6666666666666665,"connect_max_ms":6,"echo_tps":63128.666666666664,"echo_mibs":35.300000000000004,"event_tps":3442,"event_mibs":2.2333333333333334,"req_tps":1804,"req_mibs":11.699999999999998},"nostr-rs-relay":{"connect_avg_ms":2,"connect_max_ms":3.3333333333333335,"echo_tps":164995.33333333334,"echo_mibs":90.36666666666667,"event_tps":761.6666666666666,"event_mibs":0.5,"req_tps":846.3333333333334,"req_mibs":2.333333333333333}}} -{"timestamp":"2026-03-18T22:30:08Z","machine_id":"agent","git_tag":"v0.4.0","git_commit":"b86b5db","runs":3,"versions":{"parrhesia":"0.4.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":11.333333333333334,"connect_max_ms":20.666666666666668,"echo_tps":69139.33333333333,"echo_mibs":37.833333333333336,"event_tps":1938.6666666666667,"event_mibs":1.3,"req_tps":4619.666666666667,"req_mibs":26.266666666666666},"parrhesia-memory":{"connect_avg_ms":10,"connect_max_ms":17.333333333333332,"echo_tps":62715.333333333336,"echo_mibs":34.333333333333336,"event_tps":1573,"event_mibs":1.0333333333333334,"req_tps":4768,"req_mibs":23.733333333333334},"strfry":{"connect_avg_ms":3.3333333333333335,"connect_max_ms":6,"echo_tps":60956.666666666664,"echo_mibs":34.06666666666667,"event_tps":3380.6666666666665,"event_mibs":2.2,"req_tps":1820.3333333333333,"req_mibs":11.800000000000002},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.333333333333333,"echo_tps":161165.33333333334,"echo_mibs":88.26666666666665,"event_tps":768,"event_mibs":0.5,"req_tps":847.3333333333334,"req_mibs":2.3000000000000003}}} -{"timestamp":"2026-03-18T22:36:37Z","machine_id":"agent","git_tag":"v0.5.0","git_commit":"e557eba","runs":3,"versions":{"parrhesia":"0.5.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":34.666666666666664,"connect_max_ms":61.666666666666664,"echo_tps":72441,"echo_mibs":39.666666666666664,"event_tps":1897.3333333333333,"event_mibs":1.2333333333333334,"req_tps":13.333333333333334,"req_mibs":0.03333333333333333},"parrhesia-memory":{"connect_avg_ms":43.333333333333336,"connect_max_ms":74.66666666666667,"echo_tps":62704.666666666664,"echo_mibs":34.300000000000004,"event_tps":1370,"event_mibs":0.8666666666666667,"req_tps":47,"req_mibs":0.16666666666666666},"strfry":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.666666666666667,"echo_tps":61189.333333333336,"echo_mibs":34.2,"event_tps":3426.6666666666665,"event_mibs":2.2,"req_tps":1811.3333333333333,"req_mibs":11.766666666666666},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4,"echo_tps":152654.33333333334,"echo_mibs":83.63333333333333,"event_tps":772.6666666666666,"event_mibs":0.5,"req_tps":878.3333333333334,"req_mibs":2.4}}} -{"timestamp":"2026-03-18T21:35:03Z","machine_id":"agent","git_tag":"v0.6.0","git_commit":"7b337d9","runs":3,"versions":{"parrhesia":"0.6.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":26.666666666666668,"connect_max_ms":45.333333333333336,"echo_tps":68100.33333333333,"echo_mibs":37.233333333333334,"event_tps":1647.3333333333333,"event_mibs":1.0666666666666667,"req_tps":3576.6666666666665,"req_mibs":18.833333333333332},"parrhesia-memory":{"connect_avg_ms":14.666666666666666,"connect_max_ms":24.333333333333332,"echo_tps":55978,"echo_mibs":30.633333333333336,"event_tps":882,"event_mibs":0.5666666666666668,"req_tps":6888,"req_mibs":36.06666666666666},"strfry":{"connect_avg_ms":3,"connect_max_ms":4.666666666666667,"echo_tps":67718.33333333333,"echo_mibs":37.86666666666667,"event_tps":3548.3333333333335,"event_mibs":2.3,"req_tps":1808,"req_mibs":11.699999999999998},"nostr-rs-relay":{"connect_avg_ms":2,"connect_max_ms":3.3333333333333335,"echo_tps":166178,"echo_mibs":91.03333333333335,"event_tps":787,"event_mibs":0.5,"req_tps":860.6666666666666,"req_mibs":2.4}}} +{"schema_version":2,"timestamp":"2026-03-18T21:35:03Z","machine_id":"agent","git_tag":"v0.6.0","git_commit":"7b337d9","runs":3,"versions":{"parrhesia":"0.6.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":26.666666666666668,"connect_max_ms":45.333333333333336,"echo_tps":68100.33333333333,"echo_mibs":37.233333333333334,"event_tps":1647.3333333333333,"event_mibs":1.0666666666666667,"req_tps":3576.6666666666665,"req_mibs":18.833333333333332},"parrhesia-memory":{"connect_avg_ms":14.666666666666666,"connect_max_ms":24.333333333333332,"echo_tps":55978,"echo_mibs":30.633333333333336,"event_tps":882,"event_mibs":0.5666666666666668,"req_tps":6888,"req_mibs":36.06666666666666},"strfry":{"connect_avg_ms":3,"connect_max_ms":4.666666666666667,"echo_tps":67718.33333333333,"echo_mibs":37.86666666666667,"event_tps":3548.3333333333335,"event_mibs":2.3,"req_tps":1808,"req_mibs":11.699999999999998},"nostr-rs-relay":{"connect_avg_ms":2,"connect_max_ms":3.3333333333333335,"echo_tps":166178,"echo_mibs":91.03333333333335,"event_tps":787,"event_mibs":0.5,"req_tps":860.6666666666666,"req_mibs":2.4}},"run_id":"local-2026-03-18T21:35:03Z-agent-7b337d9","source":{"kind":"local","git_tag":"v0.6.0","git_commit":"7b337d9"},"infra":{"provider":"local"},"bench":{"runs":3,"targets":["parrhesia-pg","parrhesia-memory","strfry","nostr-rs-relay"]}} +{"schema_version":2,"timestamp":"2026-03-18T22:14:37Z","machine_id":"agent","git_tag":"v0.2.0","git_commit":"b20dbf6","runs":3,"versions":{"parrhesia":"0.2.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":14.666666666666666,"connect_max_ms":25.666666666666668,"echo_tps":77133,"echo_mibs":42.233333333333334,"event_tps":1602.6666666666667,"event_mibs":1.0666666666666667,"req_tps":2418,"req_mibs":12.5},"parrhesia-memory":{"connect_avg_ms":9,"connect_max_ms":16,"echo_tps":64218.333333333336,"echo_mibs":35.166666666666664,"event_tps":1578.3333333333333,"event_mibs":1,"req_tps":2431.3333333333335,"req_mibs":12.633333333333333},"strfry":{"connect_avg_ms":3.3333333333333335,"connect_max_ms":6,"echo_tps":63682.666666666664,"echo_mibs":35.6,"event_tps":3477.3333333333335,"event_mibs":2.2333333333333334,"req_tps":1804,"req_mibs":11.733333333333334},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.333333333333333,"echo_tps":160009,"echo_mibs":87.63333333333333,"event_tps":762,"event_mibs":0.4666666666666666,"req_tps":831,"req_mibs":2.2333333333333334}},"run_id":"local-2026-03-18T22:14:37Z-agent-b20dbf6","source":{"kind":"local","git_tag":"v0.2.0","git_commit":"b20dbf6"},"infra":{"provider":"local"},"bench":{"runs":3,"targets":["parrhesia-pg","parrhesia-memory","strfry","nostr-rs-relay"]}} +{"schema_version":2,"timestamp":"2026-03-18T22:22:12Z","machine_id":"agent","git_tag":"v0.3.0","git_commit":"8c8d5a8","runs":3,"versions":{"parrhesia":"0.3.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":13,"connect_max_ms":21.666666666666668,"echo_tps":70703.33333333333,"echo_mibs":38.7,"event_tps":1970.6666666666667,"event_mibs":1.3,"req_tps":3614,"req_mibs":20.966666666666665},"parrhesia-memory":{"connect_avg_ms":13,"connect_max_ms":22.333333333333332,"echo_tps":60452.333333333336,"echo_mibs":33.1,"event_tps":1952.6666666666667,"event_mibs":1.3,"req_tps":3616,"req_mibs":20.766666666666666},"strfry":{"connect_avg_ms":3.6666666666666665,"connect_max_ms":6,"echo_tps":63128.666666666664,"echo_mibs":35.300000000000004,"event_tps":3442,"event_mibs":2.2333333333333334,"req_tps":1804,"req_mibs":11.699999999999998},"nostr-rs-relay":{"connect_avg_ms":2,"connect_max_ms":3.3333333333333335,"echo_tps":164995.33333333334,"echo_mibs":90.36666666666667,"event_tps":761.6666666666666,"event_mibs":0.5,"req_tps":846.3333333333334,"req_mibs":2.333333333333333}},"run_id":"local-2026-03-18T22:22:12Z-agent-8c8d5a8","source":{"kind":"local","git_tag":"v0.3.0","git_commit":"8c8d5a8"},"infra":{"provider":"local"},"bench":{"runs":3,"targets":["parrhesia-pg","parrhesia-memory","strfry","nostr-rs-relay"]}} +{"schema_version":2,"timestamp":"2026-03-18T22:30:08Z","machine_id":"agent","git_tag":"v0.4.0","git_commit":"b86b5db","runs":3,"versions":{"parrhesia":"0.4.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":11.333333333333334,"connect_max_ms":20.666666666666668,"echo_tps":69139.33333333333,"echo_mibs":37.833333333333336,"event_tps":1938.6666666666667,"event_mibs":1.3,"req_tps":4619.666666666667,"req_mibs":26.266666666666666},"parrhesia-memory":{"connect_avg_ms":10,"connect_max_ms":17.333333333333332,"echo_tps":62715.333333333336,"echo_mibs":34.333333333333336,"event_tps":1573,"event_mibs":1.0333333333333334,"req_tps":4768,"req_mibs":23.733333333333334},"strfry":{"connect_avg_ms":3.3333333333333335,"connect_max_ms":6,"echo_tps":60956.666666666664,"echo_mibs":34.06666666666667,"event_tps":3380.6666666666665,"event_mibs":2.2,"req_tps":1820.3333333333333,"req_mibs":11.800000000000002},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.333333333333333,"echo_tps":161165.33333333334,"echo_mibs":88.26666666666665,"event_tps":768,"event_mibs":0.5,"req_tps":847.3333333333334,"req_mibs":2.3000000000000003}},"run_id":"local-2026-03-18T22:30:08Z-agent-b86b5db","source":{"kind":"local","git_tag":"v0.4.0","git_commit":"b86b5db"},"infra":{"provider":"local"},"bench":{"runs":3,"targets":["parrhesia-pg","parrhesia-memory","strfry","nostr-rs-relay"]}} +{"schema_version":2,"timestamp":"2026-03-18T22:36:37Z","machine_id":"agent","git_tag":"v0.5.0","git_commit":"e557eba","runs":3,"versions":{"parrhesia":"0.5.0","strfry":"strfry 1.0.4 (nixpkgs)","nostr-rs-relay":"nostr-rs-relay 0.9.0","nostr-bench":"nostr-bench 0.4.0"},"servers":{"parrhesia-pg":{"connect_avg_ms":34.666666666666664,"connect_max_ms":61.666666666666664,"echo_tps":72441,"echo_mibs":39.666666666666664,"event_tps":1897.3333333333333,"event_mibs":1.2333333333333334,"req_tps":13.333333333333334,"req_mibs":0.03333333333333333},"parrhesia-memory":{"connect_avg_ms":43.333333333333336,"connect_max_ms":74.66666666666667,"echo_tps":62704.666666666664,"echo_mibs":34.300000000000004,"event_tps":1370,"event_mibs":0.8666666666666667,"req_tps":47,"req_mibs":0.16666666666666666},"strfry":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4.666666666666667,"echo_tps":61189.333333333336,"echo_mibs":34.2,"event_tps":3426.6666666666665,"event_mibs":2.2,"req_tps":1811.3333333333333,"req_mibs":11.766666666666666},"nostr-rs-relay":{"connect_avg_ms":2.6666666666666665,"connect_max_ms":4,"echo_tps":152654.33333333334,"echo_mibs":83.63333333333333,"event_tps":772.6666666666666,"event_mibs":0.5,"req_tps":878.3333333333334,"req_mibs":2.4}},"run_id":"local-2026-03-18T22:36:37Z-agent-e557eba","source":{"kind":"local","git_tag":"v0.5.0","git_commit":"e557eba"},"infra":{"provider":"local"},"bench":{"runs":3,"targets":["parrhesia-pg","parrhesia-memory","strfry","nostr-rs-relay"]}} diff --git a/devenv.nix b/devenv.nix index 260fac7..06b0708 100644 --- a/devenv.nix +++ b/devenv.nix @@ -78,6 +78,7 @@ in { with pkgs; [ just + # Mix NIFs gcc git gnumake @@ -85,6 +86,8 @@ in { automake libtool pkg-config + # for tests + openssl # Nix code formatter alejandra # i18n @@ -103,8 +106,11 @@ in { nostr-rs-relay # Benchmark graph gnuplot + # Cloud benchmarks + hcloud ] ++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [ + # Nostr reference servers strfry ]; diff --git a/package-lock.json b/package-lock.json index 29336e1..6057349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@mariozechner/pi-coding-agent": "^0.57.1" + "@mariozechner/pi-coding-agent": "^0.60.0" } }, "node_modules/@anthropic-ai/sdk": { @@ -168,56 +168,56 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1007.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1007.0.tgz", - "integrity": "sha512-X7iWTQAZrCvQH2lfrZktVPfR3jdLPNtI4zkk4NA/vXzW5k8VNgdVuWUSm8cAzIXnhV3YThvDpLhEk87igNyGWQ==", + "version": "3.1012.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1012.0.tgz", + "integrity": "sha512-d++NOlkdsHwLJpyDbdpfaRBlyG0eytu3dY1G9p2ITt98BDJbUX5EgKFNJRDFLQvcWRJKvwnTAcuDLZZn6hg0VA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/credential-provider-node": "^3.972.19", - "@aws-sdk/eventstream-handler-node": "^3.972.10", - "@aws-sdk/middleware-eventstream": "^3.972.7", - "@aws-sdk/middleware-host-header": "^3.972.7", - "@aws-sdk/middleware-logger": "^3.972.7", - "@aws-sdk/middleware-recursion-detection": "^3.972.7", - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/middleware-websocket": "^3.972.12", - "@aws-sdk/region-config-resolver": "^3.972.7", - "@aws-sdk/token-providers": "3.1007.0", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@aws-sdk/util-user-agent-browser": "^3.972.7", - "@aws-sdk/util-user-agent-node": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/core": "^3.23.9", - "@smithy/eventstream-serde-browser": "^4.2.11", - "@smithy/eventstream-serde-config-resolver": "^4.3.11", - "@smithy/eventstream-serde-node": "^4.2.11", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/hash-node": "^4.2.11", - "@smithy/invalid-dependency": "^4.2.11", - "@smithy/middleware-content-length": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-retry": "^4.4.40", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/credential-provider-node": "^3.972.22", + "@aws-sdk/eventstream-handler-node": "^3.972.11", + "@aws-sdk/middleware-eventstream": "^3.972.8", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/middleware-websocket": "^3.972.13", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/token-providers": "3.1012.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.39", - "@smithy/util-defaults-mode-node": "^4.2.42", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -226,22 +226,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.19.tgz", - "integrity": "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==", + "version": "3.973.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.21.tgz", + "integrity": "sha512-OTUcDX9Yfz/FLKbHjiMaP9D4Hs44lYJzN7zBcrK2nDmBt0Wr8D6nYt12QoBkZsW0nVMFsTIGaZCrsU9zCcIMXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/xml-builder": "^3.972.10", - "@smithy/core": "^3.23.9", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.12", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -250,15 +250,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.17.tgz", - "integrity": "sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA==", + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.19.tgz", + "integrity": "sha512-33NpkQtmnsjLr9QdZvL3w8bjy+WoBJ+jY8JwuzxIq38rDNi1kwpBWW7Yjh+8bMlksd+ZAWW0fH4S/6OeoAdU5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -266,20 +266,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.19.tgz", - "integrity": "sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.21.tgz", + "integrity": "sha512-xFke7yjbON4unNOG0TApQwz+o1LH5VhVLgWlUuiLRWNDyBfeHIFje2ck8qHybvJ8Fkm5m3SsN+pvHtVo6PGWlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.17", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -287,24 +287,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.18.tgz", - "integrity": "sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.21.tgz", + "integrity": "sha512-fmJN7KhB7CoG65w9fC2LVOd2wZbR2d1yJIpZNe2J5CeDPu7nUHSmavuJAeGEoE3OL5UIBVPNhmK/fV/NQrs3Hw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/credential-provider-env": "^3.972.17", - "@aws-sdk/credential-provider-http": "^3.972.19", - "@aws-sdk/credential-provider-login": "^3.972.18", - "@aws-sdk/credential-provider-process": "^3.972.17", - "@aws-sdk/credential-provider-sso": "^3.972.18", - "@aws-sdk/credential-provider-web-identity": "^3.972.18", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/credential-provider-env": "^3.972.19", + "@aws-sdk/credential-provider-http": "^3.972.21", + "@aws-sdk/credential-provider-login": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.19", + "@aws-sdk/credential-provider-sso": "^3.972.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -312,18 +312,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.18.tgz", - "integrity": "sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.21.tgz", + "integrity": "sha512-ENU+YCiuQocQjfIf9bPxZ+ZY0wIBkl3SMH22optBQwy8UFpSfonHynXzGT27xQxer4cYTNOpwDqbfo57BusbpQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -331,22 +331,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.19.tgz", - "integrity": "sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.22.tgz", + "integrity": "sha512-VE6i8nkmrRyhKut7nnfCWRbdDf+CfyRr8ixSwdaPDguYlgvkAO2pHu9oK11XzbSuatB0io1ozI/vpYhelXn8Pg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.17", - "@aws-sdk/credential-provider-http": "^3.972.19", - "@aws-sdk/credential-provider-ini": "^3.972.18", - "@aws-sdk/credential-provider-process": "^3.972.17", - "@aws-sdk/credential-provider-sso": "^3.972.18", - "@aws-sdk/credential-provider-web-identity": "^3.972.18", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/credential-provider-env": "^3.972.19", + "@aws-sdk/credential-provider-http": "^3.972.21", + "@aws-sdk/credential-provider-ini": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.19", + "@aws-sdk/credential-provider-sso": "^3.972.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -354,16 +354,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.17.tgz", - "integrity": "sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg==", + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.19.tgz", + "integrity": "sha512-hjj5bFo4kf5/WzAMjDEFByVOMbq5gZiagIpJexf7Kp9nIDaGzhCphMsx03NCA8s9zUJzHlD1lXazd7MS+e03Lg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -371,36 +371,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.18.tgz", - "integrity": "sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.21.tgz", + "integrity": "sha512-9jWRCuMZpZKlqCZ46bvievqdfswsyB2yPAr9rOiN+FxaGgf8jrR5iYDqJgscvk1jrbAxiK4cIjHv3XjIAWAhzQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/token-providers": "3.1005.0", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1005.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1005.0.tgz", - "integrity": "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/token-providers": "3.1012.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -408,17 +390,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.18.tgz", - "integrity": "sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.21.tgz", + "integrity": "sha512-ShWQO/cQVZ+j3zUDK7Kj+m7grPzQCVA2iaZdJ+hJTGvVH5lR32Ip/rgZZ+zBdH6D6wczP9Upa4NMXoqJdGpK1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -426,14 +408,14 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.10.tgz", - "integrity": "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.11.tgz", + "integrity": "sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/eventstream-codec": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -441,14 +423,14 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.7.tgz", - "integrity": "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.8.tgz", + "integrity": "sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -456,14 +438,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", - "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -471,13 +453,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", - "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -485,15 +467,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", - "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -501,18 +483,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.20.tgz", - "integrity": "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.22.tgz", + "integrity": "sha512-pZPNGWZVQvgUIO/P9PXZNz7ciq9mLYb/wQEurg3phKTa3DiBIunIRcgA0eBNwmog6S3oy0KR1bv4EJ4ld9A5sQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@smithy/core": "^3.23.9", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-retry": "^4.2.11", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -520,19 +502,19 @@ } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.12.tgz", - "integrity": "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.13.tgz", + "integrity": "sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-format-url": "^3.972.7", - "@smithy/eventstream-codec": "^4.2.11", - "@smithy/eventstream-serde-browser": "^4.2.11", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-format-url": "^3.972.8", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", @@ -543,47 +525,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.8.tgz", - "integrity": "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==", + "version": "3.996.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.11.tgz", + "integrity": "sha512-i7SwoSR4JB/79JoGDUACnFUQOZwXGLWNX35lIb1Pq72nUGlVV+RFZp+BLa8S+mog2pbXU9+6Kc5YwGiMi5bKhQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/middleware-host-header": "^3.972.7", - "@aws-sdk/middleware-logger": "^3.972.7", - "@aws-sdk/middleware-recursion-detection": "^3.972.7", - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/region-config-resolver": "^3.972.7", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@aws-sdk/util-user-agent-browser": "^3.972.7", - "@aws-sdk/util-user-agent-node": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/core": "^3.23.9", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/hash-node": "^4.2.11", - "@smithy/invalid-dependency": "^4.2.11", - "@smithy/middleware-content-length": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-retry": "^4.4.40", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.39", - "@smithy/util-defaults-mode-node": "^4.2.42", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -592,15 +574,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", - "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -608,17 +590,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1007.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1007.0.tgz", - "integrity": "sha512-kKvVyr53vvVc5k6RbvI6jhafxufxO2SkEw8QeEzJqwOXH/IMY7Cm0IyhnBGdqj80iiIIiIM2jGe7Fn3TIdwdrw==", + "version": "3.1012.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1012.0.tgz", + "integrity": "sha512-vzKwy020zjuiF4WTJzejx5nYcXJnRhHpb6i3lyZHIwfFwXG1yX4bzBVNMWYWF+bz1i2Pp2VhJbPyzpqj4VuJXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.19", - "@aws-sdk/nested-clients": "^3.996.8", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -626,12 +608,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -639,15 +621,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", - "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-endpoints": "^3.3.2", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -655,14 +637,14 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.7.tgz", - "integrity": "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", + "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -682,27 +664,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", - "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.5.tgz", - "integrity": "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.8.tgz", + "integrity": "sha512-Kvb96TafGPLYo4Z2GRCzQTne77epXgiZEo0DDXwavzkWmgDV/1XD1tMA766gzRcHHFUraWsE+4T8DKtPTZUxgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.20", - "@aws-sdk/types": "^3.973.5", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -718,13 +701,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", - "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.13.tgz", + "integrity": "sha512-I/+BMxM4WE/6xL0tyV7tAUDOAXmyw/va1oGr/eSly43HmLUcD1G+v96vEKAA8VoLcZ03ZQo/PWzjmN9zQErqPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.4.1", + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.6", "tslib": "^2.6.2" }, "engines": { @@ -741,9 +724,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -760,9 +743,9 @@ } }, "node_modules/@google/genai": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.44.0.tgz", - "integrity": "sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.46.0.tgz", + "integrity": "sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==", "license": "Apache-2.0", "dependencies": { "google-auth-library": "^10.3.0", @@ -782,75 +765,6 @@ } } }, - "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/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/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/@isaacs/cliui/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/@isaacs/cliui/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/@mariozechner/clipboard": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz", @@ -1044,21 +958,21 @@ } }, "node_modules/@mariozechner/pi-agent-core": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.57.1.tgz", - "integrity": "sha512-WXsBbkNWOObFGHkhixaT8GXJpHDd3+fn8QntYF+4R8Sa9WB90ENXWidO6b7vcKX+JX0jjO5dIsQxmzosARJKlg==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.60.0.tgz", + "integrity": "sha512-1zQcfFp8r0iwZCxCBQ9/ccFJoagns68cndLPTJJXl1ZqkYirzSld1zBOPxLAgeAKWIz3OX8dB2WQwTJFhmEojQ==", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.57.1" + "@mariozechner/pi-ai": "^0.60.0" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@mariozechner/pi-ai": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.57.1.tgz", - "integrity": "sha512-Bd/J4a3YpdzJVyHLih0vDSdB0QPL4ti0XsAwtHOK/8eVhB0fHM1CpcgIrcBFJ23TMcKXMi0qamz18ERfp8tmgg==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.60.0.tgz", + "integrity": "sha512-OiMuXQturnEDPmA+ho7eLe4G8plO2z21yjNMs9niQREauoblWOz7Glv58I66KPzczLED4aZTlQLTRdU6t1rz8A==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -1083,15 +997,15 @@ } }, "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.57.1.tgz", - "integrity": "sha512-u5MQEduj68rwVIsRsqrWkJYiJCyPph/a6bMoJAQKo1sb+Pc17Y/ojwa+wGssnUMjEB38AQKofWTVe8NFEpSWNw==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.60.0.tgz", + "integrity": "sha512-IOv7cTU4nbznFNUE5ofi13k2dmSG39coBoGWIBQTVw3iVyl0HxuHbg0NiTx3ktrPIDNtkii+y7tWXzWqwoo4lw==", "license": "MIT", "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.57.1", - "@mariozechner/pi-ai": "^0.57.1", - "@mariozechner/pi-tui": "^0.57.1", + "@mariozechner/pi-agent-core": "^0.60.0", + "@mariozechner/pi-ai": "^0.60.0", + "@mariozechner/pi-tui": "^0.60.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", @@ -1119,9 +1033,9 @@ } }, "node_modules/@mariozechner/pi-tui": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.57.1.tgz", - "integrity": "sha512-cjoRghLbeAHV0tTJeHgZXaryUi5zzBZofeZ7uJun1gztnckLLRjoVeaPTujNlc5BIfyKvFqhh1QWCZng/MXlpg==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.60.0.tgz", + "integrity": "sha512-ZAK5gxYhGmfJqMjfWcRBjB8glITltDbTrYJXvcDtfengbKTZN0p39p5uO5pvUB8/PiAWKTRS06yaNMhf/LG26g==", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -1147,16 +1061,6 @@ "zod-to-json-schema": "^3.24.1" } }, - "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/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1264,9 +1168,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.10", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.10.tgz", - "integrity": "sha512-pn0HaJpxmdeCLdbAm79SUjX8IPiej9ANHNHec4K4u5Bkf5BqYCbAgK3c8NTCVf44DnlWJK7W1mimlgBPUQ3IlA==", + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -1275,7 +1179,7 @@ "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.18", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -1371,9 +1275,9 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.14.tgz", - "integrity": "sha512-Aswg1yMsujkikRVv+JIDw2ybTgx0cnTnv7pMee46OX6lTMwk/QpH1lbx3vN3feMwyNrFcSUbYBtbgwHXXn3CIA==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -1441,13 +1345,13 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.24", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.24.tgz", - "integrity": "sha512-k7SZG+7IbS4fVAI47p+QixmcjqliCoZ7T5ZtAJMHyViiv7AhMC9aXtgxvNQ8TQmbUe7kotsvW2XeEEqnTmdOXg==", + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.10", - "@smithy/middleware-serde": "^4.2.13", + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", @@ -1460,15 +1364,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.41", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.41.tgz", - "integrity": "sha512-qjeS0KGftfz2CL4/IziPmQurzemKRPh6sekt3IFbj1519nkj+JM+RcdjVrC1AQFFZhmW3zz7KqwOgN+qJZeVlQ==", + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.4", + "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -1480,12 +1384,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.13.tgz", - "integrity": "sha512-appEschlOmriCVGLYTTjKdbnXIZ55XT9TsV+aGuj5Jiw988gmEZwJwPkYqlZdwajMKgfxt5epjFTGriyYf4Kiw==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.10", + "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -1523,9 +1427,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.15", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.15.tgz", - "integrity": "sha512-2z3Z7Qfts2Eui5Oy+MLJjwKx1LT0Hm/b6W0XJXkUIFHP1W9D4BhdvxWW2W5xPP92CoXO+B4C/zSH67uIxMkWoA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", @@ -1636,17 +1540,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.4", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.4.tgz", - "integrity": "sha512-kbFGh3QrUj7Z9zYHCip+dGVyRGiFo6JK0A+9InOwmU4ZCkJs3HKhjLL/ABe5I8kp9uScqrftcWrDh7YxlWmmZA==", + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.10", - "@smithy/middleware-endpoint": "^4.4.24", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.18", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -1743,13 +1647,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.40", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.40.tgz", - "integrity": "sha512-TB++dVe/aHkhCw8+fVUiGEEyz70Drftze6uk5VGBDJAjEj2mqNFftkeY7Jyit3uui346NkZxzLMGM0yzD/S8og==", + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.4", + "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -1758,16 +1662,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.43", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.43.tgz", - "integrity": "sha512-cHmr8Q1BJstJC8ahvYrcyqjSIwrgLbpphOYmfMvF+EVsKUU52b3DDLb0SyiAzR16o7FR1r2IVUFfWWu7ADh1iw==", + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.11", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.4", + "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -1829,13 +1733,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.18", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.18.tgz", - "integrity": "sha512-o0hxsNp2rC7Kz93RNER/mv5G60kntYPPjV9e9Zoa3Mm455bCGHlFW6TywziCQRlLzvrQj/mmWJimAvJWF/wfjg==", + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.14", - "@smithy/node-http-handler": "^4.4.15", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", @@ -2210,20 +2114,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2273,12 +2163,6 @@ "node": ">=0.3.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2413,9 +2297,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", - "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", @@ -2428,9 +2312,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", + "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", "funding": [ { "type": "github", @@ -2439,7 +2323,8 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.0.0", + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", "strnum": "^2.1.2" }, "bin": { @@ -2479,9 +2364,9 @@ } }, "node_modules/file-type": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.1.tgz", - "integrity": "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA==", + "version": "21.3.3", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.3.tgz", + "integrity": "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg==", "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.4.1", @@ -2496,34 +2381,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "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/foreground-child/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/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2537,15 +2394,14 @@ } }, "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" + "node-fetch": "^3.3.2" }, "engines": { "node": ">=18" @@ -2642,14 +2498,14 @@ } }, "node_modules/google-auth-library": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", - "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "7.1.3", + "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" @@ -2776,27 +2632,6 @@ "node": ">=8" } }, - "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/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -2864,9 +2699,9 @@ "license": "Apache-2.0" }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -3081,12 +2916,6 @@ "node": ">= 14" } }, - "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/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -3129,15 +2958,6 @@ "node": ">=14.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": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -3275,94 +3095,6 @@ "node": ">= 4" } }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/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/rimraf/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/rimraf/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "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/rimraf/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/rimraf/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3383,27 +3115,6 @@ ], "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": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3478,42 +3189,6 @@ "node": ">=8" } }, - "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/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/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3550,32 +3225,10 @@ "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.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "funding": [ { "type": "github", @@ -3676,9 +3329,9 @@ } }, "node_modules/undici": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.23.0.tgz", - "integrity": "sha512-HVMxHKZKi+eL2mrUZDzDkKW3XvCjynhbtpSq20xQp4ePDFeSFuAfnvM0GIwZIv8fiKHjXFQ5WjxhCt15KRNj+g==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -3699,21 +3352,6 @@ "node": ">= 8" } }, - "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": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3731,45 +3369,6 @@ "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/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/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index e14ed19..7c3852e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@mariozechner/pi-coding-agent": "^0.57.1" + "@mariozechner/pi-coding-agent": "^0.60.0" } } diff --git a/scripts/cloud_bench_orchestrate.mjs b/scripts/cloud_bench_orchestrate.mjs new file mode 100755 index 0000000..219c85b --- /dev/null +++ b/scripts/cloud_bench_orchestrate.mjs @@ -0,0 +1,1192 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.resolve(__dirname, ".."); + +const DEFAULT_TARGETS = ["parrhesia-pg", "parrhesia-memory", "strfry", "nostr-rs-relay"]; + +const DEFAULTS = { + datacenter: "fsn1-dc14", + serverType: "cx23", + clientType: "cx23", + imageBase: "ubuntu-24.04", + clients: 3, + runs: 3, + targets: DEFAULT_TARGETS, + historyFile: "bench/history.jsonl", + artifactsDir: "bench/cloud_artifacts", + gitRef: "HEAD", + parrhesiaImage: null, + postgresImage: "postgres:17", + strfryImage: "ghcr.io/hoytech/strfry:latest", + nostrRsImage: "scsibug/nostr-rs-relay:latest", + keep: false, + bench: { + connectCount: 200, + connectRate: 100, + echoCount: 100, + echoRate: 50, + echoSize: 512, + eventCount: 100, + eventRate: 50, + reqCount: 100, + reqRate: 50, + reqLimit: 10, + keepaliveSeconds: 5, + }, +}; + +function usage() { + console.log(`usage: + node scripts/cloud_bench_orchestrate.mjs [options] + +Creates one server node + N client nodes on Hetzner Cloud, runs nostr-bench in +parallel from clients against selected relay targets, stores raw client logs in +bench/cloud_artifacts//, and appends metadata + pointers to +bench/history.jsonl. + +Options: + --datacenter (default: ${DEFAULTS.datacenter}) + --server-type (default: ${DEFAULTS.serverType}) + --client-type (default: ${DEFAULTS.clientType}) + --image-base (default: ${DEFAULTS.imageBase}) + --clients (default: ${DEFAULTS.clients}) + --runs (default: ${DEFAULTS.runs}) + --targets (default: ${DEFAULT_TARGETS.join(",")}) + + Source selection (choose one style): + --parrhesia-image Use remote image tag directly (e.g. ghcr.io/...) + --git-ref Build local nix docker archive from git ref (default: HEAD) + + Images for comparison targets: + --postgres-image (default: ${DEFAULTS.postgresImage}) + --strfry-image (default: ${DEFAULTS.strfryImage}) + --nostr-rs-image (default: ${DEFAULTS.nostrRsImage}) + + Benchmark knobs: + --connect-count (default: ${DEFAULTS.bench.connectCount}) + --connect-rate (default: ${DEFAULTS.bench.connectRate}) + --echo-count (default: ${DEFAULTS.bench.echoCount}) + --echo-rate (default: ${DEFAULTS.bench.echoRate}) + --echo-size (default: ${DEFAULTS.bench.echoSize}) + --event-count (default: ${DEFAULTS.bench.eventCount}) + --event-rate (default: ${DEFAULTS.bench.eventRate}) + --req-count (default: ${DEFAULTS.bench.reqCount}) + --req-rate (default: ${DEFAULTS.bench.reqRate}) + --req-limit (default: ${DEFAULTS.bench.reqLimit}) + --keepalive-seconds (default: ${DEFAULTS.bench.keepaliveSeconds}) + + Output + lifecycle: + --history-file (default: ${DEFAULTS.historyFile}) + --artifacts-dir (default: ${DEFAULTS.artifactsDir}) + --keep Keep cloud resources (no cleanup) + -h, --help + +Notes: + - Requires hcloud, ssh, scp, ssh-keygen, git. + - Requires docker locally to build portable nostr-bench binary. + - If --parrhesia-image is omitted, requires nix locally. +`); +} + +function parseArgs(argv) { + const opts = JSON.parse(JSON.stringify(DEFAULTS)); + + const intOpt = (name, value) => { + const n = Number(value); + if (!Number.isInteger(n) || n < 1) { + throw new Error(`${name} must be a positive integer, got: ${value}`); + } + return n; + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + switch (arg) { + case "-h": + case "--help": + usage(); + process.exit(0); + break; + case "--datacenter": + opts.datacenter = argv[++i]; + break; + case "--server-type": + opts.serverType = argv[++i]; + break; + case "--client-type": + opts.clientType = argv[++i]; + break; + case "--image-base": + opts.imageBase = argv[++i]; + break; + case "--clients": + opts.clients = intOpt(arg, argv[++i]); + break; + case "--runs": + opts.runs = intOpt(arg, argv[++i]); + break; + case "--targets": + opts.targets = argv[++i] + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + break; + case "--parrhesia-image": + opts.parrhesiaImage = argv[++i]; + break; + case "--git-ref": + opts.gitRef = argv[++i]; + break; + case "--postgres-image": + opts.postgresImage = argv[++i]; + break; + case "--strfry-image": + opts.strfryImage = argv[++i]; + break; + case "--nostr-rs-image": + opts.nostrRsImage = argv[++i]; + break; + case "--connect-count": + opts.bench.connectCount = intOpt(arg, argv[++i]); + break; + case "--connect-rate": + opts.bench.connectRate = intOpt(arg, argv[++i]); + break; + case "--echo-count": + opts.bench.echoCount = intOpt(arg, argv[++i]); + break; + case "--echo-rate": + opts.bench.echoRate = intOpt(arg, argv[++i]); + break; + case "--echo-size": + opts.bench.echoSize = intOpt(arg, argv[++i]); + break; + case "--event-count": + opts.bench.eventCount = intOpt(arg, argv[++i]); + break; + case "--event-rate": + opts.bench.eventRate = intOpt(arg, argv[++i]); + break; + case "--req-count": + opts.bench.reqCount = intOpt(arg, argv[++i]); + break; + case "--req-rate": + opts.bench.reqRate = intOpt(arg, argv[++i]); + break; + case "--req-limit": + opts.bench.reqLimit = intOpt(arg, argv[++i]); + break; + case "--keepalive-seconds": + opts.bench.keepaliveSeconds = intOpt(arg, argv[++i]); + break; + case "--history-file": + opts.historyFile = argv[++i]; + break; + case "--artifacts-dir": + opts.artifactsDir = argv[++i]; + break; + case "--keep": + opts.keep = true; + break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!opts.targets.length) { + throw new Error("--targets must include at least one target"); + } + + for (const t of opts.targets) { + if (!DEFAULT_TARGETS.includes(t)) { + throw new Error(`invalid target: ${t} (valid: ${DEFAULT_TARGETS.join(", ")})`); + } + } + + return opts; +} + +function shellEscape(value) { + return `'${String(value).replace(/'/g, `'"'"'`)}'`; +} + +function commandExists(cmd) { + const pathEnv = process.env.PATH || ""; + for (const dir of pathEnv.split(":")) { + if (!dir) continue; + const full = path.join(dir, cmd); + try { + fs.accessSync(full, fs.constants.X_OK); + return true; + } catch { + // ignore + } + } + return false; +} + +function runCommand(command, args = [], options = {}) { + const { cwd = ROOT_DIR, env = process.env, stdio = "pipe" } = options; + + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd, env, stdio }); + + let stdout = ""; + let stderr = ""; + + if (child.stdout) { + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + } + + if (child.stderr) { + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + } + + child.on("error", (error) => { + reject(error); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ code, stdout, stderr }); + } else { + const error = new Error( + `Command failed (${code}): ${command} ${args.map((a) => shellEscape(a)).join(" ")}`, + ); + error.code = code; + error.stdout = stdout; + error.stderr = stderr; + reject(error); + } + }); + }); +} + +async function sshExec(hostIp, keyPath, remoteCommand, options = {}) { + return runCommand( + "ssh", + [ + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "BatchMode=yes", + "-o", + "ConnectTimeout=8", + "-i", + keyPath, + `root@${hostIp}`, + remoteCommand, + ], + options, + ); +} + +async function scpToHost(hostIp, keyPath, localPath, remotePath) { + await runCommand("scp", [ + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-i", + keyPath, + localPath, + `root@${hostIp}:${remotePath}`, + ]); +} + +async function waitForSsh(hostIp, keyPath, attempts = 60) { + for (let i = 1; i <= attempts; i += 1) { + try { + await sshExec(hostIp, keyPath, "echo ready >/dev/null"); + return; + } catch { + await new Promise((r) => setTimeout(r, 2000)); + } + } + throw new Error(`SSH not ready after ${attempts} attempts: ${hostIp}`); +} + +async function ensureLocalPrereqs(opts) { + const required = ["hcloud", "ssh", "scp", "ssh-keygen", "git", "docker", "file"]; + const needsParrhesia = opts.targets.includes("parrhesia-pg") || opts.targets.includes("parrhesia-memory"); + + if (needsParrhesia && !opts.parrhesiaImage) { + required.push("nix"); + } + + for (const cmd of required) { + if (!commandExists(cmd)) { + throw new Error(`Required command not found in PATH: ${cmd}`); + } + } +} + +async function buildNostrBenchBinary(tmpDir) { + const srcDir = path.join(tmpDir, "nostr-bench-src"); + console.log("[local] cloning nostr-bench source..."); + await runCommand("git", ["clone", "--depth", "1", "https://github.com/rnostr/nostr-bench.git", srcDir], { + stdio: "inherit", + }); + + let binaryPath = path.join(srcDir, "target", "x86_64-unknown-linux-musl", "release", "nostr-bench"); + let buildMode = "nix-musl-static"; + + console.log("[local] building nostr-bench (attempt static musl via nix run cargo)..."); + let staticOk = false; + + if (commandExists("nix")) { + try { + await runCommand( + "nix", + ["run", "nixpkgs#cargo", "--", "build", "--release", "--target", "x86_64-unknown-linux-musl"], + { cwd: srcDir, stdio: "inherit" }, + ); + + const fileOut = await runCommand("file", [binaryPath]); + staticOk = fileOut.stdout.includes("statically linked"); + } catch { + staticOk = false; + } + } + + if (!staticOk) { + buildMode = "docker-glibc-portable"; + binaryPath = path.join(srcDir, "target", "release", "nostr-bench"); + + console.log("[local] static build unavailable, building portable glibc binary in rust:1-bookworm..."); + + await runCommand( + "docker", + [ + "run", + "--rm", + "-v", + `${srcDir}:/src`, + "-w", + "/src", + "rust:1-bookworm", + "bash", + "-lc", + "export PATH=/usr/local/cargo/bin:$PATH; apt-get update -qq >/dev/null; apt-get install -y -qq pkg-config build-essential >/dev/null; cargo build --release", + ], + { stdio: "inherit" }, + ); + + const fileOut = await runCommand("file", [binaryPath]); + if (!(fileOut.stdout.includes("/lib64/ld-linux-x86-64.so.2") || fileOut.stdout.includes("statically linked"))) { + throw new Error(`Built nostr-bench binary does not look portable: ${fileOut.stdout.trim()}`); + } + } + + const outPath = path.join(tmpDir, "nostr-bench"); + fs.copyFileSync(binaryPath, outPath); + fs.chmodSync(outPath, 0o755); + + const fileOut = await runCommand("file", [outPath]); + console.log(`[local] nostr-bench ready (${buildMode}): ${outPath}`); + console.log(`[local] ${fileOut.stdout.trim()}`); + + return { path: outPath, buildMode }; +} + +async function buildParrhesiaArchiveIfNeeded(opts, tmpDir) { + if (opts.parrhesiaImage) { + return { + mode: "remote-image", + image: opts.parrhesiaImage, + archivePath: null, + gitRef: null, + gitCommit: null, + }; + } + + const resolved = (await runCommand("git", ["rev-parse", "--verify", opts.gitRef], { cwd: ROOT_DIR })).stdout.trim(); + + let buildDir = ROOT_DIR; + let worktreeDir = null; + + if (opts.gitRef !== "HEAD") { + worktreeDir = path.join(tmpDir, "parrhesia-worktree"); + console.log(`[local] creating temporary worktree for ${opts.gitRef}...`); + await runCommand("git", ["worktree", "add", "--detach", worktreeDir, opts.gitRef], { + cwd: ROOT_DIR, + stdio: "inherit", + }); + buildDir = worktreeDir; + } + + try { + console.log(`[local] building parrhesia docker archive via nix at ${opts.gitRef}...`); + const archivePath = ( + await runCommand("nix", ["build", ".#dockerImage", "--print-out-paths", "--no-link"], { + cwd: buildDir, + }) + ).stdout.trim(); + + if (!archivePath) { + throw new Error("nix build did not return an archive path"); + } + + return { + mode: "local-git-ref", + image: "parrhesia:latest", + archivePath, + gitRef: opts.gitRef, + gitCommit: resolved, + }; + } finally { + if (worktreeDir) { + await runCommand("git", ["worktree", "remove", "--force", worktreeDir], { + cwd: ROOT_DIR, + }).catch(() => { + // ignore + }); + } + } +} + +function makeServerScript() { + return `#!/usr/bin/env bash +set -euo pipefail + +PARRHESIA_IMAGE="\${PARRHESIA_IMAGE:-parrhesia:latest}" +POSTGRES_IMAGE="\${POSTGRES_IMAGE:-postgres:17}" +STRFRY_IMAGE="\${STRFRY_IMAGE:-ghcr.io/hoytech/strfry:latest}" +NOSTR_RS_IMAGE="\${NOSTR_RS_IMAGE:-scsibug/nostr-rs-relay:latest}" + +cleanup_containers() { + docker rm -f parrhesia pg strfry nostr-rs >/dev/null 2>&1 || true +} + +wait_http() { + local url="\$1" + local timeout="\${2:-60}" + local log_container="\${3:-}" + + for _ in \$(seq 1 "\$timeout"); do + if curl -fsS "\$url" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + if [[ -n "\$log_container" ]]; then + docker logs --tail 200 "\$log_container" >&2 || true + fi + + echo "Timed out waiting for HTTP endpoint: \$url" >&2 + return 1 +} + +wait_pg() { + local timeout="\${1:-90}" + for _ in \$(seq 1 "\$timeout"); do + if docker exec pg pg_isready -U parrhesia -d parrhesia >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + docker logs --tail 200 pg >&2 || true + echo "Timed out waiting for Postgres" >&2 + return 1 +} + +wait_port() { + local port="\$1" + local timeout="\${2:-60}" + local log_container="\${3:-}" + + for _ in \$(seq 1 "\$timeout"); do + if ss -ltn | grep -q ":\${port} "; then + return 0 + fi + sleep 1 + done + + if [[ -n "\$log_container" ]]; then + docker logs --tail 200 "\$log_container" >&2 || true + fi + + echo "Timed out waiting for port: \$port" >&2 + return 1 +} + +common_parrhesia_env=() +common_parrhesia_env+=( -e PARRHESIA_ENABLE_EXPIRATION_WORKER=0 ) +common_parrhesia_env+=( -e PARRHESIA_ENABLE_PARTITION_RETENTION_WORKER=0 ) +common_parrhesia_env+=( -e PARRHESIA_PUBLIC_MAX_CONNECTIONS=infinity ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_FRAME_BYTES=16777216 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_EVENT_BYTES=4194304 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_FILTERS_PER_REQ=1024 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_FILTER_LIMIT=100000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_TAGS_PER_EVENT=4096 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_TAG_VALUES_PER_FILTER=4096 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_IP_MAX_EVENT_INGEST_PER_WINDOW=1000000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_RELAY_MAX_EVENT_INGEST_PER_WINDOW=1000000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_SUBSCRIPTIONS_PER_CONNECTION=4096 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_EVENT_FUTURE_SKEW_SECONDS=31536000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_EVENT_INGEST_PER_WINDOW=1000000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_AUTH_MAX_AGE_SECONDS=31536000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_OUTBOUND_QUEUE=65536 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_OUTBOUND_DRAIN_BATCH_SIZE=4096 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_NEGENTROPY_PAYLOAD_BYTES=1048576 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_NEGENTROPY_SESSIONS_PER_CONNECTION=256 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_NEGENTROPY_TOTAL_SESSIONS=100000 ) +common_parrhesia_env+=( -e PARRHESIA_LIMITS_MAX_NEGENTROPY_ITEMS_PER_SESSION=1000000 ) + +cmd="\${1:-}" +if [[ -z "\$cmd" ]]; then + echo "usage: cloud-bench-server.sh " >&2 + exit 1 +fi + +case "\$cmd" in + start-parrhesia-pg) + cleanup_containers + docker network create benchnet >/dev/null 2>&1 || true + + docker run -d --name pg --network benchnet \ + -e POSTGRES_DB=parrhesia \ + -e POSTGRES_USER=parrhesia \ + -e POSTGRES_PASSWORD=parrhesia \ + "\$POSTGRES_IMAGE" >/dev/null + + wait_pg 90 + + docker run --rm --network benchnet \ + -e DATABASE_URL=ecto://parrhesia:parrhesia@pg:5432/parrhesia \ + "\$PARRHESIA_IMAGE" \ + eval "Parrhesia.Release.migrate()" + + docker run -d --name parrhesia --network benchnet \ + -p 4413:4413 \ + -e DATABASE_URL=ecto://parrhesia:parrhesia@pg:5432/parrhesia \ + -e POOL_SIZE=20 \ + "\${common_parrhesia_env[@]}" \ + "\$PARRHESIA_IMAGE" >/dev/null + + wait_http "http://127.0.0.1:4413/health" 120 parrhesia + ;; + + start-parrhesia-memory) + cleanup_containers + + docker run -d --name parrhesia \ + -p 4413:4413 \ + -e PARRHESIA_STORAGE_BACKEND=memory \ + -e PARRHESIA_MODERATION_CACHE_ENABLED=0 \ + "\${common_parrhesia_env[@]}" \ + "\$PARRHESIA_IMAGE" >/dev/null + + wait_http "http://127.0.0.1:4413/health" 120 parrhesia + ;; + + start-strfry) + cleanup_containers + + mkdir -p /root/strfry-data/strfry + cat > /root/strfry.conf <<'EOF' +# generated by cloud bench script +db = "/data/strfry" +relay { + bind = "0.0.0.0" + port = 7777 + nofiles = 131072 +} +EOF + + docker run -d --name strfry \ + -p 7777:7777 \ + -v /root/strfry.conf:/etc/strfry.conf:ro \ + -v /root/strfry-data:/data \ + "\$STRFRY_IMAGE" \ + --config /etc/strfry.conf relay >/dev/null + + wait_port 7777 60 strfry + ;; + + start-nostr-rs-relay) + cleanup_containers + + cat > /root/nostr-rs.toml <<'EOF' +[database] +engine = "sqlite" + +[network] +ip = "0.0.0.0" +port = 8080 +EOF + + docker run -d --name nostr-rs \ + -p 8080:8080 \ + -v /root/nostr-rs.toml:/usr/src/app/config.toml:ro \ + "\$NOSTR_RS_IMAGE" >/dev/null + + wait_http "http://127.0.0.1:8080/" 60 nostr-rs + ;; + + cleanup) + cleanup_containers + ;; + + *) + echo "unknown command: \$cmd" >&2 + exit 1 + ;; +esac +`; +} + +function makeClientScript() { + return `#!/usr/bin/env bash +set -euo pipefail + +relay_url="\${1:-}" +if [[ -z "\$relay_url" ]]; then + echo "usage: cloud-bench-client.sh " >&2 + exit 1 +fi + +bench_bin="\${NOSTR_BENCH_BIN:-/usr/local/bin/nostr-bench}" + +echo "==> nostr-bench connect \${relay_url}" +"\$bench_bin" connect --json \ + -c "\${PARRHESIA_BENCH_CONNECT_COUNT:-200}" \ + -r "\${PARRHESIA_BENCH_CONNECT_RATE:-100}" \ + -k "\${PARRHESIA_BENCH_KEEPALIVE_SECONDS:-5}" \ + "\${relay_url}" + +echo +echo "==> nostr-bench echo \${relay_url}" +"\$bench_bin" echo --json \ + -c "\${PARRHESIA_BENCH_ECHO_COUNT:-100}" \ + -r "\${PARRHESIA_BENCH_ECHO_RATE:-50}" \ + -k "\${PARRHESIA_BENCH_KEEPALIVE_SECONDS:-5}" \ + --size "\${PARRHESIA_BENCH_ECHO_SIZE:-512}" \ + "\${relay_url}" + +echo +echo "==> nostr-bench event \${relay_url}" +"\$bench_bin" event --json \ + -c "\${PARRHESIA_BENCH_EVENT_COUNT:-100}" \ + -r "\${PARRHESIA_BENCH_EVENT_RATE:-50}" \ + -k "\${PARRHESIA_BENCH_KEEPALIVE_SECONDS:-5}" \ + "\${relay_url}" + +echo +echo "==> nostr-bench req \${relay_url}" +"\$bench_bin" req --json \ + -c "\${PARRHESIA_BENCH_REQ_COUNT:-100}" \ + -r "\${PARRHESIA_BENCH_REQ_RATE:-50}" \ + -k "\${PARRHESIA_BENCH_KEEPALIVE_SECONDS:-5}" \ + --limit "\${PARRHESIA_BENCH_REQ_LIMIT:-10}" \ + "\${relay_url}" +`; +} + +function parseNostrBenchSections(output) { + const lines = output.split(/\r?\n/); + let section = null; + const parsed = {}; + + for (const lineRaw of lines) { + const line = lineRaw.trim(); + const header = line.match(/^==>\s+nostr-bench\s+(connect|echo|event|req)\s+/); + if (header) { + section = header[1]; + continue; + } + + if (!line.startsWith("{")) continue; + + try { + const json = JSON.parse(line); + if (section) { + parsed[section] = json; + } + } catch { + // ignore noisy non-json lines + } + } + + return parsed; +} + +function mean(values) { + const valid = values.filter((v) => Number.isFinite(v)); + if (valid.length === 0) return NaN; + return valid.reduce((a, b) => a + b, 0) / valid.length; +} + +function metricFromSections(sections) { + const connect = sections?.connect?.connect_stats?.success_time || {}; + const echo = sections?.echo || {}; + const event = sections?.event || {}; + const req = sections?.req || {}; + + return { + connect_avg_ms: Number(connect.avg ?? NaN), + connect_max_ms: Number(connect.max ?? NaN), + echo_tps: Number(echo.tps ?? NaN), + echo_mibs: Number(echo.size ?? NaN), + event_tps: Number(event.tps ?? NaN), + event_mibs: Number(event.size ?? NaN), + req_tps: Number(req.tps ?? NaN), + req_mibs: Number(req.size ?? NaN), + }; +} + +function summariseServersFromResults(results) { + const byServer = new Map(); + + for (const runEntry of results) { + const serverName = runEntry.target; + if (!byServer.has(serverName)) { + byServer.set(serverName, []); + } + + const samples = byServer.get(serverName); + for (const clientResult of runEntry.clients || []) { + if (clientResult.status !== "ok") continue; + samples.push(metricFromSections(clientResult.sections || {})); + } + } + + const metricKeys = [ + "connect_avg_ms", + "connect_max_ms", + "echo_tps", + "echo_mibs", + "event_tps", + "event_mibs", + "req_tps", + "req_mibs", + ]; + + const out = {}; + for (const [serverName, samples] of byServer.entries()) { + const summary = {}; + for (const key of metricKeys) { + summary[key] = mean(samples.map((s) => s[key])); + } + out[serverName] = summary; + } + + return out; +} + +async function tryCommandStdout(command, args = [], options = {}) { + try { + const res = await runCommand(command, args, options); + return res.stdout.trim(); + } catch { + return ""; + } +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + await ensureLocalPrereqs(opts); + + const timestamp = new Date().toISOString(); + const runId = `cloudbench-${timestamp.replace(/[:.]/g, "-")}-${Math.floor(Math.random() * 100000)}`; + + const detectedGitTag = (await tryCommandStdout("git", ["describe", "--tags", "--abbrev=0"], { + cwd: ROOT_DIR, + })) || "untagged"; + const detectedGitCommit = await tryCommandStdout("git", ["rev-parse", "--short=7", "HEAD"], { + cwd: ROOT_DIR, + }); + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "parrhesia-cloud-bench-")); + const localServerScriptPath = path.join(tmpDir, "cloud-bench-server.sh"); + const localClientScriptPath = path.join(tmpDir, "cloud-bench-client.sh"); + + fs.writeFileSync(localServerScriptPath, makeServerScript(), "utf8"); + fs.writeFileSync(localClientScriptPath, makeClientScript(), "utf8"); + fs.chmodSync(localServerScriptPath, 0o755); + fs.chmodSync(localClientScriptPath, 0o755); + + const artifactsRoot = path.resolve(ROOT_DIR, opts.artifactsDir); + const artifactsDir = path.join(artifactsRoot, runId); + fs.mkdirSync(artifactsDir, { recursive: true }); + + const historyFile = path.resolve(ROOT_DIR, opts.historyFile); + fs.mkdirSync(path.dirname(historyFile), { recursive: true }); + + console.log(`[run] ${runId}`); + console.log("[phase] local preparation"); + + const nostrBench = await buildNostrBenchBinary(tmpDir); + const needsParrhesia = opts.targets.includes("parrhesia-pg") || opts.targets.includes("parrhesia-memory"); + const parrhesiaSource = needsParrhesia + ? await buildParrhesiaArchiveIfNeeded(opts, tmpDir) + : { + mode: "not-needed", + image: opts.parrhesiaImage, + archivePath: null, + gitRef: null, + gitCommit: null, + }; + + const keyName = `${runId}-ssh`; + const keyPath = path.join(tmpDir, "id_ed25519"); + const keyPubPath = `${keyPath}.pub`; + + const createdServers = []; + let sshKeyCreated = false; + + const cleanup = async () => { + if (opts.keep) { + console.log("[cleanup] --keep set, skipping cloud cleanup"); + return; + } + + if (createdServers.length > 0) { + console.log("[cleanup] deleting servers..."); + await Promise.all( + createdServers.map((name) => + runCommand("hcloud", ["server", "delete", name], { stdio: "inherit" }).catch(() => { + // ignore cleanup failures + }), + ), + ); + } + + if (sshKeyCreated) { + console.log("[cleanup] deleting ssh key..."); + await runCommand("hcloud", ["ssh-key", "delete", keyName], { stdio: "inherit" }).catch(() => { + // ignore cleanup failures + }); + } + }; + + try { + console.log("[phase] create ssh credentials"); + await runCommand("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", keyPath, "-C", keyName], { + stdio: "inherit", + }); + + await runCommand("hcloud", ["ssh-key", "create", "--name", keyName, "--public-key-from-file", keyPubPath], { + stdio: "inherit", + }); + sshKeyCreated = true; + + console.log("[phase] create cloud servers in parallel"); + + const serverName = `${runId}-server`; + const clientNames = Array.from({ length: opts.clients }, (_, i) => `${runId}-client-${i + 1}`); + + const createOne = (name, role, type) => + runCommand( + "hcloud", + [ + "server", + "create", + "--name", + name, + "--type", + type, + "--datacenter", + opts.datacenter, + "--image", + opts.imageBase, + "--ssh-key", + keyName, + "--label", + `bench_run=${runId}`, + "--label", + `bench_role=${role}`, + "-o", + "json", + ], + { stdio: "pipe" }, + ).then((res) => JSON.parse(res.stdout)); + + const [serverCreate, ...clientCreates] = await Promise.all([ + createOne(serverName, "server", opts.serverType), + ...clientNames.map((name) => createOne(name, "client", opts.clientType)), + ]); + + createdServers.push(serverName, ...clientNames); + + const serverIp = serverCreate.server.public_net.ipv4.ip; + const clientInfos = clientCreates.map((c) => ({ + name: c.server.name, + id: c.server.id, + ip: c.server.public_net.ipv4.ip, + })); + + console.log("[phase] wait for SSH"); + await Promise.all([ + waitForSsh(serverIp, keyPath), + ...clientInfos.map((client) => waitForSsh(client.ip, keyPath)), + ]); + + console.log("[phase] install runtime dependencies on nodes"); + const installCmd = [ + "set -euo pipefail", + "export DEBIAN_FRONTEND=noninteractive", + "apt-get update -y >/dev/null", + "apt-get install -y docker.io curl jq >/dev/null", + "systemctl enable --now docker >/dev/null", + "docker --version", + ].join("; "); + + await Promise.all([ + sshExec(serverIp, keyPath, installCmd, { stdio: "inherit" }), + ...clientInfos.map((client) => sshExec(client.ip, keyPath, installCmd, { stdio: "inherit" })), + ]); + + console.log("[phase] upload control scripts + nostr-bench binary"); + + await scpToHost(serverIp, keyPath, localServerScriptPath, "/root/cloud-bench-server.sh"); + await sshExec(serverIp, keyPath, "chmod +x /root/cloud-bench-server.sh"); + + for (const client of clientInfos) { + await scpToHost(client.ip, keyPath, localClientScriptPath, "/root/cloud-bench-client.sh"); + await scpToHost(client.ip, keyPath, nostrBench.path, "/usr/local/bin/nostr-bench"); + await sshExec(client.ip, keyPath, "chmod +x /root/cloud-bench-client.sh /usr/local/bin/nostr-bench"); + } + + console.log("[phase] server image setup"); + + let parrhesiaImageOnServer = parrhesiaSource.image; + + if (needsParrhesia) { + if (parrhesiaSource.archivePath) { + console.log("[server] uploading parrhesia docker archive..."); + await scpToHost(serverIp, keyPath, parrhesiaSource.archivePath, "/root/parrhesia.tar.gz"); + await sshExec(serverIp, keyPath, "docker load -i /root/parrhesia.tar.gz", { stdio: "inherit" }); + parrhesiaImageOnServer = "parrhesia:latest"; + } else { + console.log(`[server] pulling parrhesia image ${parrhesiaImageOnServer}...`); + await sshExec(serverIp, keyPath, `docker pull ${shellEscape(parrhesiaImageOnServer)}`, { + stdio: "inherit", + }); + } + } + + console.log("[server] pre-pulling comparison images..."); + for (const image of [opts.postgresImage, opts.strfryImage, opts.nostrRsImage]) { + await sshExec(serverIp, keyPath, `docker pull ${shellEscape(image)}`, { stdio: "inherit" }); + } + + const serverDescribe = JSON.parse( + (await runCommand("hcloud", ["server", "describe", serverName, "-o", "json"])).stdout, + ); + const clientDescribes = await Promise.all( + clientInfos.map(async (c) => + JSON.parse((await runCommand("hcloud", ["server", "describe", c.name, "-o", "json"])).stdout), + ), + ); + + const versions = { + nostr_bench: ( + await sshExec(clientInfos[0].ip, keyPath, "/usr/local/bin/nostr-bench --version") + ).stdout.trim(), + }; + + const startCommands = { + "parrhesia-pg": "start-parrhesia-pg", + "parrhesia-memory": "start-parrhesia-memory", + strfry: "start-strfry", + "nostr-rs-relay": "start-nostr-rs-relay", + }; + + const relayUrls = { + "parrhesia-pg": `ws://${serverIp}:4413/relay`, + "parrhesia-memory": `ws://${serverIp}:4413/relay`, + strfry: `ws://${serverIp}:7777`, + "nostr-rs-relay": `ws://${serverIp}:8080`, + }; + + const results = []; + + console.log("[phase] benchmark execution"); + + for (let runIndex = 1; runIndex <= opts.runs; runIndex += 1) { + for (const target of opts.targets) { + console.log(`[bench] run ${runIndex}/${opts.runs} target=${target}`); + + const serverEnvPrefix = [ + `PARRHESIA_IMAGE=${shellEscape(parrhesiaImageOnServer || "parrhesia:latest")}`, + `POSTGRES_IMAGE=${shellEscape(opts.postgresImage)}`, + `STRFRY_IMAGE=${shellEscape(opts.strfryImage)}`, + `NOSTR_RS_IMAGE=${shellEscape(opts.nostrRsImage)}`, + ].join(" "); + + await sshExec( + serverIp, + keyPath, + `${serverEnvPrefix} /root/cloud-bench-server.sh ${shellEscape(startCommands[target])}`, + { stdio: "inherit" }, + ); + + const relayUrl = relayUrls[target]; + const runTargetDir = path.join(artifactsDir, target, `run-${runIndex}`); + fs.mkdirSync(runTargetDir, { recursive: true }); + + const benchEnvPrefix = [ + `PARRHESIA_BENCH_CONNECT_COUNT=${opts.bench.connectCount}`, + `PARRHESIA_BENCH_CONNECT_RATE=${opts.bench.connectRate}`, + `PARRHESIA_BENCH_ECHO_COUNT=${opts.bench.echoCount}`, + `PARRHESIA_BENCH_ECHO_RATE=${opts.bench.echoRate}`, + `PARRHESIA_BENCH_ECHO_SIZE=${opts.bench.echoSize}`, + `PARRHESIA_BENCH_EVENT_COUNT=${opts.bench.eventCount}`, + `PARRHESIA_BENCH_EVENT_RATE=${opts.bench.eventRate}`, + `PARRHESIA_BENCH_REQ_COUNT=${opts.bench.reqCount}`, + `PARRHESIA_BENCH_REQ_RATE=${opts.bench.reqRate}`, + `PARRHESIA_BENCH_REQ_LIMIT=${opts.bench.reqLimit}`, + `PARRHESIA_BENCH_KEEPALIVE_SECONDS=${opts.bench.keepaliveSeconds}`, + ].join(" "); + + const clientRunResults = await Promise.all( + clientInfos.map(async (client) => { + const startedAt = new Date().toISOString(); + const startMs = Date.now(); + const stdoutPath = path.join(runTargetDir, `${client.name}.stdout.log`); + const stderrPath = path.join(runTargetDir, `${client.name}.stderr.log`); + + try { + const benchRes = await sshExec( + client.ip, + keyPath, + `${benchEnvPrefix} /root/cloud-bench-client.sh ${shellEscape(relayUrl)}`, + ); + + fs.writeFileSync(stdoutPath, benchRes.stdout, "utf8"); + fs.writeFileSync(stderrPath, benchRes.stderr, "utf8"); + + return { + client_name: client.name, + client_ip: client.ip, + status: "ok", + started_at: startedAt, + finished_at: new Date().toISOString(), + duration_ms: Date.now() - startMs, + stdout_path: path.relative(ROOT_DIR, stdoutPath), + stderr_path: path.relative(ROOT_DIR, stderrPath), + sections: parseNostrBenchSections(benchRes.stdout), + }; + } catch (error) { + const out = error.stdout || ""; + const err = error.stderr || String(error); + fs.writeFileSync(stdoutPath, out, "utf8"); + fs.writeFileSync(stderrPath, err, "utf8"); + + return { + client_name: client.name, + client_ip: client.ip, + status: "error", + started_at: startedAt, + finished_at: new Date().toISOString(), + duration_ms: Date.now() - startMs, + stdout_path: path.relative(ROOT_DIR, stdoutPath), + stderr_path: path.relative(ROOT_DIR, stderrPath), + error: String(error.message || error), + sections: parseNostrBenchSections(out), + }; + } + }), + ); + + results.push({ + run: runIndex, + target, + relay_url: relayUrl, + clients: clientRunResults, + }); + + const failed = clientRunResults.filter((r) => r.status !== "ok"); + if (failed.length > 0) { + throw new Error( + `Client benchmark failed for target=${target}, run=${runIndex}: ${failed + .map((f) => f.client_name) + .join(", ")}`, + ); + } + } + } + + console.log("[phase] final server cleanup (containers)"); + await sshExec(serverIp, keyPath, "/root/cloud-bench-server.sh cleanup"); + + const gitTag = detectedGitTag || "untagged"; + const gitCommit = parrhesiaSource.gitCommit || detectedGitCommit || "unknown"; + const servers = summariseServersFromResults(results); + + const entry = { + schema_version: 2, + timestamp, + run_id: runId, + machine_id: os.hostname(), + git_tag: gitTag, + git_commit: gitCommit, + runs: opts.runs, + source: { + kind: "cloud", + mode: parrhesiaSource.mode, + parrhesia_image: parrhesiaImageOnServer, + git_ref: parrhesiaSource.gitRef, + git_tag: gitTag, + git_commit: gitCommit, + }, + infra: { + provider: "hcloud", + datacenter: opts.datacenter, + server_type: opts.serverType, + client_type: opts.clientType, + image_base: opts.imageBase, + clients: opts.clients, + }, + bench: { + runs: opts.runs, + targets: opts.targets, + ...opts.bench, + }, + versions, + servers, + artifacts_dir: path.relative(ROOT_DIR, artifactsDir), + hcloud: { + server: serverDescribe, + clients: clientDescribes, + }, + results, + }; + + fs.appendFileSync(historyFile, `${JSON.stringify(entry)}\n`, "utf8"); + + console.log("[done] benchmark complete"); + console.log(`[done] history appended: ${path.relative(ROOT_DIR, historyFile)}`); + console.log(`[done] artifacts: ${path.relative(ROOT_DIR, artifactsDir)}`); + if (opts.keep) { + console.log(`[done] resources kept. server=${serverName} clients=${clientNames.join(",")}`); + console.log(`[done] ssh key kept: ${keyName}`); + } + } finally { + await cleanup(); + } +} + +main().catch((error) => { + console.error("[error]", error?.message || error); + if (error?.stderr) { + console.error(error.stderr); + } + process.exit(1); +}); diff --git a/scripts/run_bench_cloud.sh b/scripts/run_bench_cloud.sh new file mode 100755 index 0000000..c372bd5 --- /dev/null +++ b/scripts/run_bench_cloud.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +usage() { + cat <<'EOF' +usage: + ./scripts/run_bench_cloud.sh [options] [-- extra args for cloud_bench_orchestrate.mjs] + +Friendly wrapper around scripts/cloud_bench_orchestrate.mjs. + +Defaults (override via env or flags): + datacenter: fsn1-dc14 + server/client type: cx23 + clients: 3 + runs: 3 + targets: parrhesia-pg,parrhesia-memory,strfry,nostr-rs-relay + +Flags: + --quick Quick smoke profile (1 run, 1 client, lower load) + --clients N Override client count + --runs N Override run count + --targets CSV Override targets + --datacenter NAME Override datacenter + --server-type NAME Override server type + --client-type NAME Override client type + --image IMAGE Use remote Parrhesia image (e.g. ghcr.io/...) + --git-ref REF Build Parrhesia image from git ref (default: HEAD) + --keep Keep cloud resources after run + -h, --help + +Environment overrides: + PARRHESIA_CLOUD_DATACENTER (default: fsn1-dc14) + PARRHESIA_CLOUD_SERVER_TYPE (default: cx23) + PARRHESIA_CLOUD_CLIENT_TYPE (default: cx23) + PARRHESIA_CLOUD_CLIENTS (default: 3) + PARRHESIA_BENCH_RUNS (default: 3) + PARRHESIA_CLOUD_TARGETS (default: all 4) + PARRHESIA_CLOUD_PARRHESIA_IMAGE (optional) + PARRHESIA_CLOUD_GIT_REF (default: HEAD) + +Bench knobs (forwarded): + PARRHESIA_BENCH_CONNECT_COUNT + PARRHESIA_BENCH_CONNECT_RATE + PARRHESIA_BENCH_ECHO_COUNT + PARRHESIA_BENCH_ECHO_RATE + PARRHESIA_BENCH_ECHO_SIZE + PARRHESIA_BENCH_EVENT_COUNT + PARRHESIA_BENCH_EVENT_RATE + PARRHESIA_BENCH_REQ_COUNT + PARRHESIA_BENCH_REQ_RATE + PARRHESIA_BENCH_REQ_LIMIT + PARRHESIA_BENCH_KEEPALIVE_SECONDS + +Examples: + # Default full cloud run + ./scripts/run_bench_cloud.sh + + # Quick smoke + ./scripts/run_bench_cloud.sh --quick + + # Use a GHCR image + ./scripts/run_bench_cloud.sh --image ghcr.io/owner/parrhesia:latest +EOF +} + +DATACENTER="${PARRHESIA_CLOUD_DATACENTER:-fsn1-dc14}" +SERVER_TYPE="${PARRHESIA_CLOUD_SERVER_TYPE:-cx23}" +CLIENT_TYPE="${PARRHESIA_CLOUD_CLIENT_TYPE:-cx23}" +CLIENTS="${PARRHESIA_CLOUD_CLIENTS:-3}" +RUNS="${PARRHESIA_BENCH_RUNS:-3}" +TARGETS="${PARRHESIA_CLOUD_TARGETS:-parrhesia-pg,parrhesia-memory,strfry,nostr-rs-relay}" +PARRHESIA_IMAGE="${PARRHESIA_CLOUD_PARRHESIA_IMAGE:-}" +GIT_REF="${PARRHESIA_CLOUD_GIT_REF:-HEAD}" +KEEP=0 +QUICK=0 + +EXTRA_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --quick) + QUICK=1 + shift + ;; + --clients) + CLIENTS="$2" + shift 2 + ;; + --runs) + RUNS="$2" + shift 2 + ;; + --targets) + TARGETS="$2" + shift 2 + ;; + --datacenter) + DATACENTER="$2" + shift 2 + ;; + --server-type) + SERVER_TYPE="$2" + shift 2 + ;; + --client-type) + CLIENT_TYPE="$2" + shift 2 + ;; + --image) + PARRHESIA_IMAGE="$2" + shift 2 + ;; + --git-ref) + GIT_REF="$2" + shift 2 + ;; + --keep) + KEEP=1 + shift + ;; + --) + shift + EXTRA_ARGS+=("$@") + break + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ "$QUICK" == "1" ]]; then + RUNS=1 + CLIENTS=1 + : "${PARRHESIA_BENCH_CONNECT_COUNT:=20}" + : "${PARRHESIA_BENCH_CONNECT_RATE:=20}" + : "${PARRHESIA_BENCH_ECHO_COUNT:=20}" + : "${PARRHESIA_BENCH_ECHO_RATE:=20}" + : "${PARRHESIA_BENCH_ECHO_SIZE:=512}" + : "${PARRHESIA_BENCH_EVENT_COUNT:=20}" + : "${PARRHESIA_BENCH_EVENT_RATE:=20}" + : "${PARRHESIA_BENCH_REQ_COUNT:=20}" + : "${PARRHESIA_BENCH_REQ_RATE:=20}" + : "${PARRHESIA_BENCH_REQ_LIMIT:=10}" + : "${PARRHESIA_BENCH_KEEPALIVE_SECONDS:=2}" +fi + +CMD=( + node scripts/cloud_bench_orchestrate.mjs + --datacenter "$DATACENTER" + --server-type "$SERVER_TYPE" + --client-type "$CLIENT_TYPE" + --clients "$CLIENTS" + --runs "$RUNS" + --targets "$TARGETS" +) + +if [[ -n "$PARRHESIA_IMAGE" ]]; then + CMD+=(--parrhesia-image "$PARRHESIA_IMAGE") +else + CMD+=(--git-ref "$GIT_REF") +fi + +if [[ "$KEEP" == "1" ]]; then + CMD+=(--keep) +fi + +# Forward bench knob envs if set +for kv in \ + PARRHESIA_BENCH_CONNECT_COUNT \ + PARRHESIA_BENCH_CONNECT_RATE \ + PARRHESIA_BENCH_ECHO_COUNT \ + PARRHESIA_BENCH_ECHO_RATE \ + PARRHESIA_BENCH_ECHO_SIZE \ + PARRHESIA_BENCH_EVENT_COUNT \ + PARRHESIA_BENCH_EVENT_RATE \ + PARRHESIA_BENCH_REQ_COUNT \ + PARRHESIA_BENCH_REQ_RATE \ + PARRHESIA_BENCH_REQ_LIMIT \ + PARRHESIA_BENCH_KEEPALIVE_SECONDS + do + if [[ -n "${!kv:-}" ]]; then + flag="--$(echo "$kv" | tr '[:upper:]' '[:lower:]' | sed -E 's/^parrhesia_bench_//' | tr '_' '-')" + CMD+=("$flag" "${!kv}") + fi + done + +CMD+=("${EXTRA_ARGS[@]}") + +printf 'Running cloud bench:\n %q' "${CMD[0]}" +for ((i=1; i<${#CMD[@]}; i++)); do + printf ' %q' "${CMD[$i]}" +done +printf '\n\n' + +"${CMD[@]}" diff --git a/scripts/run_bench_collect.sh b/scripts/run_bench_collect.sh index 75223a3..ffcc201 100755 --- a/scripts/run_bench_collect.sh +++ b/scripts/run_bench_collect.sh @@ -78,11 +78,23 @@ const [, , jsonOut, timestamp, machineId, gitTag, gitCommit, runsStr, historyFil const { versions, ...servers } = JSON.parse(fs.readFileSync(jsonOut, "utf8")); const entry = { + schema_version: 2, timestamp, + run_id: `local-${timestamp}-${machineId}-${gitCommit}`, machine_id: machineId, git_tag: gitTag, git_commit: gitCommit, runs: Number(runsStr), + source: { + kind: "local", + mode: "run_bench_collect", + git_ref: gitTag, + git_tag: gitTag, + git_commit: gitCommit, + }, + infra: { + provider: "local", + }, versions: versions || {}, servers, }; diff --git a/scripts/run_bench_update.sh b/scripts/run_bench_update.sh index 40e2e54..5d6f34e 100755 --- a/scripts/run_bench_update.sh +++ b/scripts/run_bench_update.sh @@ -7,100 +7,178 @@ cd "$ROOT_DIR" usage() { cat <<'EOF' usage: - ./scripts/run_bench_update.sh [machine_id] + ./scripts/run_bench_update.sh [machine_id|all] + ./scripts/run_bench_update.sh --machine [--run-id ] + ./scripts/run_bench_update.sh --list Regenerates bench/chart.svg and updates the benchmark table in README.md from collected data in bench/history.jsonl. -Arguments: - machine_id Optional. Filter to a specific machine's data. - Default: current machine (hostname -s) - Use "all" to include all machines (will use latest entry per tag) - -Examples: - # Update chart for current machine - ./scripts/run_bench_update.sh - - # Update chart for specific machine - ./scripts/run_bench_update.sh my-server - - # Update chart using all machines (latest entry per tag wins) - ./scripts/run_bench_update.sh all +Options: + --machine Filter by machine_id (default: hostname -s) + --run-id Filter to an exact run_id + --history-file History JSONL file (default: bench/history.jsonl) + --list List available machines and runs, then exit + -h, --help EOF } -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -# --- Configuration ----------------------------------------------------------- - BENCH_DIR="$ROOT_DIR/bench" HISTORY_FILE="$BENCH_DIR/history.jsonl" CHART_FILE="$BENCH_DIR/chart.svg" GNUPLOT_TEMPLATE="$BENCH_DIR/chart.gnuplot" +README_FILE="$ROOT_DIR/README.md" -MACHINE_ID="${1:-$(hostname -s)}" +MACHINE_ID="$(hostname -s)" +RUN_ID="" +LIST_ONLY=0 +POSITIONAL_MACHINE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --machine) + MACHINE_ID="$2" + shift 2 + ;; + --run-id) + RUN_ID="$2" + shift 2 + ;; + --history-file) + HISTORY_FILE="$2" + shift 2 + ;; + --list) + LIST_ONLY=1 + shift + ;; + *) + if [[ -z "$POSITIONAL_MACHINE" ]]; then + POSITIONAL_MACHINE="$1" + shift + else + echo "Unexpected argument: $1" >&2 + usage + exit 1 + fi + ;; + esac +done + +if [[ -n "$POSITIONAL_MACHINE" ]]; then + MACHINE_ID="$POSITIONAL_MACHINE" +fi if [[ ! -f "$HISTORY_FILE" ]]; then echo "Error: No history file found at $HISTORY_FILE" >&2 - echo "Run ./scripts/run_bench_collect.sh first to collect benchmark data" >&2 + echo "Run ./scripts/run_bench_collect.sh or ./scripts/run_bench_cloud.sh first" >&2 exit 1 fi +if [[ "$LIST_ONLY" == "1" ]]; then + node - "$HISTORY_FILE" <<'NODE' +const fs = require("node:fs"); + +const [, , historyFile] = process.argv; + +const entries = fs.readFileSync(historyFile, "utf8") + .split("\n") + .filter((l) => l.trim().length > 0) + .map((l) => JSON.parse(l)); + +if (entries.length === 0) { + console.log("No entries in history file."); + process.exit(0); +} + +entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); + +const machines = new Map(); +for (const e of entries) { + const machineId = e.machine_id || "unknown"; + const prev = machines.get(machineId); + if (!prev) { + machines.set(machineId, { count: 1, latest: e }); + } else { + prev.count += 1; + if ((e.timestamp || "") > (prev.latest.timestamp || "")) prev.latest = e; + } +} + +console.log("Machines:"); +console.log(" machine_id entries latest_timestamp latest_tag"); +for (const [machineId, info] of [...machines.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { + const id = machineId.padEnd(34, " "); + const count = String(info.count).padStart(7, " "); + const ts = (info.latest.timestamp || "").padEnd(24, " "); + const tag = info.latest.git_tag || ""; + console.log(` ${id} ${count} ${ts} ${tag}`); +} + +console.log("\nRuns (newest first):"); +console.log(" timestamp run_id machine_id source git_tag targets"); +for (const e of entries) { + const ts = (e.timestamp || "").slice(0, 19).padEnd(24, " "); + const runId = (e.run_id || "").slice(0, 36).padEnd(36, " "); + const machineId = (e.machine_id || "").slice(0, 24).padEnd(24, " "); + const source = (e.source?.kind || "").padEnd(6, " "); + const tag = (e.git_tag || "").slice(0, 16).padEnd(16, " "); + const targets = (e.bench?.targets || Object.keys(e.servers || {})).join(","); + console.log(` ${ts} ${runId} ${machineId} ${source} ${tag} ${targets}`); +} +NODE + exit 0 +fi + WORK_DIR="$(mktemp -d)" trap 'rm -rf "$WORK_DIR"' EXIT -# --- Generate chart ---------------------------------------------------------- +echo "Generating chart (machine=$MACHINE_ID${RUN_ID:+, run_id=$RUN_ID})" -echo "Generating chart for machine: $MACHINE_ID" - -node - "$HISTORY_FILE" "$MACHINE_ID" "$WORK_DIR" <<'NODE' +if ! node - "$HISTORY_FILE" "$MACHINE_ID" "$RUN_ID" "$WORK_DIR" <<'NODE' const fs = require("node:fs"); const path = require("node:path"); -const [, , historyFile, machineId, workDir] = process.argv; - -if (!fs.existsSync(historyFile)) { - console.log(" no history file, skipping chart generation"); - process.exit(0); -} - -const lines = fs.readFileSync(historyFile, "utf8") - .split("\n") - .filter(l => l.trim().length > 0) - .map(l => JSON.parse(l)); - -// Filter to selected machine(s) -let entries; -if (machineId === "all") { - entries = lines; - console.log(" using all machines"); -} else { - entries = lines.filter(e => e.machine_id === machineId); - console.log(" filtered to machine: " + machineId); -} - -if (entries.length === 0) { - console.log(" no history entries for machine '" + machineId + "', skipping chart"); - process.exit(0); -} - -// Sort chronologically, deduplicate by tag (latest wins), -// then order the resulting series by git tag. -entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); -const byTag = new Map(); -for (const e of entries) { - byTag.set(e.git_tag, e); -} -const deduped = [...byTag.values()]; +const [, , historyFile, machineId, runId, workDir] = process.argv; function parseSemverTag(tag) { - const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag); + const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag || ""); return match ? match.slice(1).map(Number) : null; } +const all = fs.readFileSync(historyFile, "utf8") + .split("\n") + .filter((l) => l.trim().length > 0) + .map((l) => JSON.parse(l)); + +let selected = all; +if (runId && runId.length > 0) { + selected = all.filter((e) => e.run_id === runId); + console.log(` filtered by run_id: ${runId}`); +} else if (machineId !== "all") { + selected = all.filter((e) => e.machine_id === machineId); + console.log(` filtered to machine: ${machineId}`); +} else { + console.log(" using all machines"); +} + +if (selected.length === 0) { + console.error(" no matching history entries"); + process.exit(1); +} + +selected.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || "")); + +const byTag = new Map(); +for (const e of selected) { + byTag.set(e.git_tag || "untagged", e); +} +const deduped = [...byTag.values()]; + deduped.sort((a, b) => { const aTag = parseSemverTag(a.git_tag); const bTag = parseSemverTag(b.git_tag); @@ -109,24 +187,19 @@ deduped.sort((a, b) => { return aTag[0] - bTag[0] || aTag[1] - bTag[1] || aTag[2] - bTag[2]; } - return a.git_tag.localeCompare(b.git_tag, undefined, { numeric: true }); + return (a.git_tag || "").localeCompare(b.git_tag || "", undefined, { numeric: true }); }); -// Determine which non-parrhesia servers are present const baselineServerNames = ["strfry", "nostr-rs-relay"]; -const presentBaselines = baselineServerNames.filter(srv => - deduped.some(e => e.servers[srv]) -); +const presentBaselines = baselineServerNames.filter((srv) => deduped.some((e) => e.servers?.[srv])); -// Metrics to chart const chartMetrics = [ - { key: "event_tps", label: "Event Throughput (TPS) — higher is better", file: "event_tps.tsv", ylabel: "TPS" }, - { key: "req_tps", label: "Req Throughput (TPS) — higher is better", file: "req_tps.tsv", ylabel: "TPS" }, - { key: "echo_tps", label: "Echo Throughput (TPS) — higher is better", file: "echo_tps.tsv", ylabel: "TPS" }, - { key: "connect_avg_ms", label: "Connect Avg Latency (ms) — lower is better", file: "connect_avg_ms.tsv", ylabel: "ms" }, + { key: "event_tps", label: "Event Throughput (TPS) — higher is better", file: "event_tps.tsv", ylabel: "TPS" }, + { key: "req_tps", label: "Req Throughput (TPS) — higher is better", file: "req_tps.tsv", ylabel: "TPS" }, + { key: "echo_tps", label: "Echo Throughput (TPS) — higher is better", file: "echo_tps.tsv", ylabel: "TPS" }, + { key: "connect_avg_ms", label: "Connect Avg Latency (ms) — lower is better", file: "connect_avg_ms.tsv", ylabel: "ms" }, ]; -// Write per-metric TSV files for (const cm of chartMetrics) { const header = ["tag", "parrhesia-pg", "parrhesia-memory"]; for (const srv of presentBaselines) header.push(srv); @@ -134,12 +207,12 @@ for (const cm of chartMetrics) { const rows = [header.join("\t")]; for (const e of deduped) { const row = [ - e.git_tag, - e.servers["parrhesia-pg"]?.[cm.key] ?? "NaN", - e.servers["parrhesia-memory"]?.[cm.key] ?? "NaN", + e.git_tag || "untagged", + e.servers?.["parrhesia-pg"]?.[cm.key] ?? "NaN", + e.servers?.["parrhesia-memory"]?.[cm.key] ?? "NaN", ]; for (const srv of presentBaselines) { - row.push(e.servers[srv]?.[cm.key] ?? "NaN"); + row.push(e.servers?.[srv]?.[cm.key] ?? "NaN"); } rows.push(row.join("\t")); } @@ -147,7 +220,6 @@ for (const cm of chartMetrics) { fs.writeFileSync(path.join(workDir, cm.file), rows.join("\n") + "\n", "utf8"); } -// Generate gnuplot plot commands (handles variable column counts) const serverLabels = ["parrhesia-pg", "parrhesia-memory"]; for (const srv of presentBaselines) serverLabels.push(srv); @@ -158,10 +230,9 @@ for (const cm of chartMetrics) { plotLines.push(`set ylabel "${cm.ylabel}"`); const plotParts = []; - // Column 2 = parrhesia-pg, 3 = parrhesia-memory, 4+ = baselines plotParts.push(`${dataFile} using 0:2:xtic(1) lt 1 title "${serverLabels[0]}"`); plotParts.push(`'' using 0:3 lt 2 title "${serverLabels[1]}"`); - for (let i = 0; i < presentBaselines.length; i++) { + for (let i = 0; i < presentBaselines.length; i += 1) { plotParts.push(`'' using 0:${4 + i} lt ${3 + i} title "${serverLabels[2 + i]}"`); } @@ -169,14 +240,22 @@ for (const cm of chartMetrics) { plotLines.push(""); } -fs.writeFileSync( - path.join(workDir, "plot_commands.gnuplot"), - plotLines.join("\n") + "\n", - "utf8" -); +fs.writeFileSync(path.join(workDir, "plot_commands.gnuplot"), plotLines.join("\n") + "\n", "utf8"); -console.log(" " + deduped.length + " tag(s), " + presentBaselines.length + " baseline server(s)"); +const latestForReadme = [...selected] + .sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")) + .find((e) => e.servers?.["parrhesia-pg"] && e.servers?.["parrhesia-memory"]); + +if (latestForReadme) { + fs.writeFileSync(path.join(workDir, "latest_entry.json"), JSON.stringify(latestForReadme), "utf8"); +} + +console.log(` selected=${selected.length}, series_tags=${deduped.length}, baselines=${presentBaselines.length}`); NODE +then + echo "No matching data for chart/update" >&2 + exit 1 +fi if [[ -f "$WORK_DIR/plot_commands.gnuplot" ]]; then gnuplot \ @@ -185,52 +264,26 @@ if [[ -f "$WORK_DIR/plot_commands.gnuplot" ]]; then "$GNUPLOT_TEMPLATE" echo " chart written to $CHART_FILE" else - echo " chart generation skipped (no data for this machine)" - exit 0 + echo " chart generation skipped" fi -# --- Update README.md ------------------------------------------------------- - echo "Updating README.md with latest benchmark..." -# Find the most recent entry for this machine -LATEST_ENTRY=$(node - "$HISTORY_FILE" "$MACHINE_ID" <<'NODE' -const fs = require("node:fs"); -const [, , historyFile, machineId] = process.argv; - -const lines = fs.readFileSync(historyFile, "utf8") - .split("\n") - .filter(l => l.trim().length > 0) - .map(l => JSON.parse(l)); - -let entries; -if (machineId === "all") { - entries = lines; -} else { - entries = lines.filter(e => e.machine_id === machineId); -} - -if (entries.length === 0) { - console.error("No entries found for machine: " + machineId); - process.exit(1); -} - -// Get latest entry -entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); -console.log(JSON.stringify(entries[0])); -NODE -) - -if [[ -z "$LATEST_ENTRY" ]]; then - echo "Warning: Could not find latest entry, skipping README update" >&2 +if [[ ! -f "$WORK_DIR/latest_entry.json" ]]; then + echo "Warning: no selected entry contains both parrhesia-pg and parrhesia-memory; skipping README table update" >&2 + echo + echo "Benchmark rendering complete. Files updated:" + echo " $CHART_FILE" + echo exit 0 fi -node - "$LATEST_ENTRY" "$ROOT_DIR/README.md" <<'NODE' +LATEST_ENTRY="$(cat "$WORK_DIR/latest_entry.json")" + +node - "$LATEST_ENTRY" "$README_FILE" <<'NODE' const fs = require("node:fs"); const [, , entryJson, readmePath] = process.argv; - const entry = JSON.parse(entryJson); const servers = entry.servers || {}; @@ -240,11 +293,7 @@ const strfry = servers["strfry"]; const nostrRs = servers["nostr-rs-relay"]; if (!pg || !mem) { - const present = Object.keys(servers).sort().join(", ") || "(none)"; - console.error( - "Latest benchmark entry must include parrhesia-pg and parrhesia-memory. Present servers: " + - present - ); + console.error("Selected entry is missing parrhesia-pg or parrhesia-memory"); process.exit(1); } @@ -259,26 +308,26 @@ function ratio(base, other) { function boldIf(ratioStr, lowerIsBetter) { if (ratioStr === "n/a") return ratioStr; - const num = parseFloat(ratioStr); + const num = Number.parseFloat(ratioStr); + if (!Number.isFinite(num)) return ratioStr; const better = lowerIsBetter ? num < 1 : num > 1; - return better ? "**" + ratioStr + "**" : ratioStr; + return better ? `**${ratioStr}**` : ratioStr; } const metricRows = [ ["connect avg latency (ms) ↓", "connect_avg_ms", true], ["connect max latency (ms) ↓", "connect_max_ms", true], - ["echo throughput (TPS) ↑", "echo_tps", false], - ["echo throughput (MiB/s) ↑", "echo_mibs", false], - ["event throughput (TPS) ↑", "event_tps", false], - ["event throughput (MiB/s) ↑", "event_mibs", false], - ["req throughput (TPS) ↑", "req_tps", false], - ["req throughput (MiB/s) ↑", "req_mibs", false], + ["echo throughput (TPS) ↑", "echo_tps", false], + ["echo throughput (MiB/s) ↑", "echo_mibs", false], + ["event throughput (TPS) ↑", "event_tps", false], + ["event throughput (MiB/s) ↑", "event_mibs", false], + ["req throughput (TPS) ↑", "req_tps", false], + ["req throughput (MiB/s) ↑", "req_mibs", false], ]; const hasStrfry = !!strfry; const hasNostrRs = !!nostrRs; -// Build header const header = ["metric", "parrhesia-pg", "parrhesia-mem"]; if (hasStrfry) header.push("strfry"); if (hasNostrRs) header.push("nostr-rs-relay"); @@ -287,7 +336,7 @@ if (hasStrfry) header.push("strfry/pg"); if (hasNostrRs) header.push("nostr-rs/pg"); const alignRow = ["---"]; -for (let i = 1; i < header.length; i++) alignRow.push("---:"); +for (let i = 1; i < header.length; i += 1) alignRow.push("---:"); const rows = metricRows.map(([label, key, lowerIsBetter]) => { const row = [label, toFixed(pg[key]), toFixed(mem[key])]; @@ -304,13 +353,12 @@ const rows = metricRows.map(([label, key, lowerIsBetter]) => { const tableLines = [ "| " + header.join(" | ") + " |", "| " + alignRow.join(" | ") + " |", - ...rows.map(r => "| " + r.join(" | ") + " |"), + ...rows.map((r) => "| " + r.join(" | ") + " |"), ]; -// Replace the first markdown table in the ## Benchmark section const readme = fs.readFileSync(readmePath, "utf8"); -const readmeLines = readme.split("\n"); -const benchIdx = readmeLines.findIndex(l => /^## Benchmark/.test(l)); +const lines = readme.split("\n"); +const benchIdx = lines.findIndex((l) => /^## Benchmark/.test(l)); if (benchIdx === -1) { console.error("Could not find '## Benchmark' section in README.md"); process.exit(1); @@ -318,8 +366,8 @@ if (benchIdx === -1) { let tableStart = -1; let tableEnd = -1; -for (let i = benchIdx + 1; i < readmeLines.length; i++) { - if (readmeLines[i].startsWith("|")) { +for (let i = benchIdx + 1; i < lines.length; i += 1) { + if (lines[i].startsWith("|")) { if (tableStart === -1) tableStart = i; tableEnd = i; } else if (tableStart !== -1) { @@ -332,19 +380,19 @@ if (tableStart === -1) { process.exit(1); } -const before = readmeLines.slice(0, tableStart); -const after = readmeLines.slice(tableEnd + 1); -const updated = [...before, ...tableLines, ...after].join("\n"); +const updated = [ + ...lines.slice(0, tableStart), + ...tableLines, + ...lines.slice(tableEnd + 1), +].join("\n"); fs.writeFileSync(readmePath, updated, "utf8"); -console.log(" table updated (" + tableLines.length + " rows)"); +console.log(` table updated (${tableLines.length} rows)`); NODE -# --- Done --------------------------------------------------------------------- - echo echo "Benchmark rendering complete. Files updated:" echo " $CHART_FILE" -echo " $ROOT_DIR/README.md" +echo " $README_FILE" echo echo "Review with: git diff"