diff --git a/Cargo.lock b/Cargo.lock index 59fe882922..f6a40be2bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,9 @@ name = "anyhow" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +dependencies = [ + "backtrace", +] [[package]] name = "api-actor" @@ -1070,6 +1073,22 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-nats" version = "0.33.0" @@ -1117,6 +1136,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -1695,6 +1736,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -1712,6 +1762,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", + "regex-automata 0.4.9", + "serde", ] [[package]] @@ -1904,6 +1956,9 @@ name = "camino" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] [[package]] name = "captcha-hcaptcha-config-get" @@ -1982,6 +2037,29 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.2.1" @@ -2214,6 +2292,12 @@ dependencies = [ "upload-get", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cf-custom-hostname-get" version = "0.0.1" @@ -2275,6 +2359,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chirp-client" version = "0.0.1" @@ -2948,7 +3038,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.11", "windows-sys 0.52.0", ] @@ -3004,6 +3094,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -3020,6 +3130,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -3120,6 +3240,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot 0.12.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -3194,6 +3339,16 @@ dependencies = [ "sct 0.6.1", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix 0.29.0", + "windows-sys 0.59.0", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -3353,12 +3508,35 @@ dependencies = [ "sqlx", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + [[package]] name = "deflate64" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +[[package]] +name = "deno-embed" +version = "0.0.1" +dependencies = [ + "anyhow", + "dirs", + "reqwest 0.11.27", + "serde_json", + "tempfile", + "tokio", + "zip 0.5.13", +] + [[package]] name = "der" version = "0.6.1" @@ -3463,6 +3641,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -3516,6 +3700,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenvy" version = "0.15.7" @@ -3549,7 +3739,7 @@ dependencies = [ "mm-config-version-get", "mm-lobby-get", "mm-lobby-list-for-user-id", - "nix", + "nix 0.27.1", "nomad-client", "nomad-util", "nomad_client", @@ -3609,6 +3799,12 @@ dependencies = [ "serde", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "dynamic-config" version = "0.0.1" @@ -3806,6 +4002,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -4095,6 +4300,30 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -4170,6 +4399,12 @@ dependencies = [ "serde", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "1.1.0" @@ -4282,6 +4517,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "game-banner-upload-complete" version = "0.0.1" @@ -4670,6 +4923,19 @@ dependencies = [ "types-proto", ] +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + [[package]] name = "governor" version = "0.6.3" @@ -4928,6 +5194,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -4996,6 +5273,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + [[package]] name = "humantime" version = "2.1.0" @@ -5334,6 +5617,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "include_dir" version = "0.7.4" @@ -5375,6 +5674,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + [[package]] name = "indoc" version = "1.0.9" @@ -5396,6 +5708,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.11", +] + [[package]] name = "instant" version = "0.1.13" @@ -5486,6 +5815,28 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "job-gc" version = "0.0.1" @@ -5620,6 +5971,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonc-parser" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b558af6b49fd918e970471374e7a798b2c9bbcda624a210ffa3901ee5614bc8e" +dependencies = [ + "serde_json", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -5635,12 +5995,21 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +name = "kv-str" +version = "0.0.1" dependencies = [ - "spin 0.9.8", + "anyhow", + "envy", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", ] [[package]] @@ -5675,6 +6044,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.7", ] [[package]] @@ -6062,6 +6432,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" @@ -6607,6 +6989,21 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.27.1" @@ -6618,6 +7015,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nkeys" version = "0.3.2" @@ -6853,6 +7262,46 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "object" version = "0.36.5" @@ -6924,6 +7373,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_info" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "outref" version = "0.5.1" @@ -6995,7 +7455,7 @@ dependencies = [ "ansitok", "bytecount", "fnv", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -7073,7 +7533,7 @@ name = "pegboard" version = "0.0.1" dependencies = [ "chirp-workflow", - "nix", + "nix 0.27.1", "rivet-config", "rivet-util", "serde", @@ -7113,7 +7573,7 @@ version = "0.0.1" dependencies = [ "chirp-client", "chirp-workflow", - "nix", + "nix 0.27.1", "pegboard", "rivet-config", "rivet-connection", @@ -7293,6 +7753,25 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "pkg-version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e848f61ee4b2010345e65757e427a077213af1cee5d3e6a02e4a151dabca377" +dependencies = [ + "pkg-version-impl", + "proc-macro-hack", +] + +[[package]] +name = "pkg-version-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1564bf5d476bf4a5eac420b88c500454c000dca79cef0a2e4304a1fe34361a3b" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "portable-atomic" version = "1.9.0" @@ -7334,6 +7813,33 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -7377,6 +7883,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.89" @@ -7450,7 +7962,7 @@ dependencies = [ "regex", "syn 1.0.109", "tempfile", - "which", + "which 4.4.2", ] [[package]] @@ -7932,7 +8444,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ - "hostname", + "hostname 0.3.1", "quick-error", ] @@ -8093,6 +8605,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "rivet-cli" +version = "0.0.1" +dependencies = [ + "anyhow", + "async-posthog", + "base64 0.22.1", + "clap", + "ctrlc", + "inquire", + "kv-str", + "rivet-toolchain", + "sentry", + "serde", + "serde_json", + "sysinfo", + "tokio", + "url", + "uuid", + "vergen-git2", + "webbrowser", +] + [[package]] name = "rivet-cloud" version = "0.0.1" @@ -8240,7 +8775,7 @@ version = "0.0.1" dependencies = [ "include_dir", "reqwest 0.12.9", - "zip", + "zip 2.2.0", ] [[package]] @@ -8287,6 +8822,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rivet-js-utils-embed" +version = "0.0.1" +dependencies = [ + "anyhow", + "deno-embed", + "fs_extra", + "include_dir", + "merkle_hash", + "sha2 0.10.8", + "tempfile", + "tokio", + "walkdir", +] + [[package]] name = "rivet-kv" version = "0.0.8" @@ -8624,6 +9174,49 @@ dependencies = [ "testcontainers", ] +[[package]] +name = "rivet-toolchain" +version = "0.0.1" +dependencies = [ + "anyhow", + "assert_cmd", + "async-stream", + "console", + "const_format", + "deno-embed", + "dirs", + "futures-util", + "humansize", + "ignore", + "indicatif", + "jsonc-parser", + "kv-str", + "lazy_static", + "lz4", + "mime_guess", + "nix 0.27.1", + "pkg-version", + "regex", + "reqwest 0.11.27", + "rivet-api", + "rivet-js-utils-embed", + "serde", + "serde_json", + "sha1", + "strum 0.24.1", + "tar", + "tempfile", + "tokio", + "tokio-util 0.7.12", + "typed-path", + "url", + "uuid", + "vergen-git2", + "which 5.0.0", + "windows 0.48.0", + "zip 0.5.13", +] + [[package]] name = "rivet-traefik-provider-server" version = "0.0.1" @@ -8916,6 +9509,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.26" @@ -9008,7 +9610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -9029,6 +9631,129 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "sentry" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5484316556650182f03b43d4c746ce0e3e48074a21e2f51244b648b6542e1066" +dependencies = [ + "httpdate", + "native-tls", + "reqwest 0.12.9", + "sentry-anyhow", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-debug-images", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq", +] + +[[package]] +name = "sentry-anyhow" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d672bfd1ed4e90978435f3c0704edb71a7a9d86403657839d518cd6aa278aff5" +dependencies = [ + "anyhow", + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-backtrace" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40aa225bb41e2ec9d7c90886834367f560efc1af028f1c5478a6cce6a59c463a" +dependencies = [ + "backtrace", + "once_cell", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8dd746da3d16cb8c39751619cefd4fcdbd6df9610f3310fd646b55f6e39910" +dependencies = [ + "hostname 0.4.0", + "libc", + "os_info", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161283cfe8e99c8f6f236a402b9ccf726b201f365988b5bb637ebca0abbd4a30" +dependencies = [ + "once_cell", + "rand", + "sentry-types", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-debug-images" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc6b25e945fcaa5e97c43faee0267eebda9f18d4b09a251775d8fef1086238a" +dependencies = [ + "findshlibs", + "once_cell", + "sentry-core", +] + +[[package]] +name = "sentry-panic" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74f229c7186dd971a9491ffcbe7883544aa064d1589bd30b83fb856cd22d63" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c5faf2103cd01eeda779ea439b68c4ee15adcdb16600836e97feafab362ec" +dependencies = [ + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d68cdf6bc41b8ff3ae2a9c4671e97426dcdd154cc1d4b6b72813f285d6b163f" +dependencies = [ + "debugid", + "hex", + "rand", + "serde", + "serde_json", + "thiserror", + "time 0.3.36", + "url", + "uuid", +] [[package]] name = "serde" @@ -9240,6 +9965,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -9768,7 +10504,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows", + "windows 0.57.0", ] [[package]] @@ -9778,7 +10514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -9789,7 +10525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -9838,6 +10574,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tar" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "team-avatar-upload-complete" version = "0.0.1" @@ -10101,6 +10848,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "testcontainers" version = "0.12.0" @@ -10290,7 +11043,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", @@ -10747,6 +11500,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-path" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668404597c2c687647f6f8934f97c280fd500db28557f52b07c56b92d3dc500a" + [[package]] name = "typenum" version = "1.17.0" @@ -10788,6 +11547,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + [[package]] name = "unicase" version = "2.8.0" @@ -10833,6 +11601,18 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -10959,6 +11739,7 @@ dependencies = [ "base64 0.22.1", "flate2", "log", + "native-tls", "once_cell", "rustls 0.23.17", "rustls-pki-types", @@ -11232,8 +12013,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" dependencies = [ "anyhow", + "cargo_metadata", "derive_builder 0.20.2", + "regex", + "rustc_version", "rustversion", + "time 0.3.36", "vergen-lib", ] @@ -11296,6 +12081,25 @@ dependencies = [ "quote", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -11413,6 +12217,34 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" +dependencies = [ + "block2", + "core-foundation 0.10.0", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + [[package]] name = "webpki" version = "0.21.4" @@ -11469,6 +12301,19 @@ dependencies = [ "rustix", ] +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "whoami" version = "1.5.2" @@ -11501,12 +12346,40 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -11599,6 +12472,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -11626,6 +12508,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -11657,6 +12554,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -11669,6 +12572,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -11681,6 +12590,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -11699,6 +12614,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -11711,6 +12632,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -11723,6 +12650,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -11735,6 +12668,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -11813,6 +12752,17 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "xmlparser" version = "0.13.6" @@ -11944,6 +12894,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time 0.1.45", +] + [[package]] name = "zip" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 3fa0db96fe..6ec480540a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["packages/infra/legacy/job-runner","packages/infra/server","packages/common/operation/core","packages/common/operation/macros","packages/common/metrics","packages/common/pools","packages/common/s3-util","packages/common/cache/result","packages/common/cache/build","packages/common/test","packages/common/connection","packages/common/util/core","packages/common/util/search","packages/common/util/macros","packages/common/config","packages/common/nomad-util","packages/common/health-checks","packages/common/chirp/metrics","packages/common/chirp/types","packages/common/chirp/worker","packages/common/chirp/perf","packages/common/chirp/worker-attributes","packages/common/chirp/client","packages/common/runtime","packages/common/redis-util","packages/common/env","packages/common/types-proto/core","packages/common/types-proto/build","packages/common/smithy-output/api-status/rust-server","packages/common/smithy-output/api-status/rust","packages/common/smithy-output/api-matchmaker/rust-server","packages/common/smithy-output/api-matchmaker/rust","packages/common/smithy-output/api-cloud/rust-server","packages/common/smithy-output/api-cloud/rust","packages/common/smithy-output/api-portal/rust-server","packages/common/smithy-output/api-portal/rust","packages/common/smithy-output/api-cf-verification/rust-server","packages/common/smithy-output/api-cf-verification/rust","packages/common/smithy-output/api-party/rust-server","packages/common/smithy-output/api-party/rust","packages/common/smithy-output/api-traefik-provider/rust-server","packages/common/smithy-output/api-traefik-provider/rust","packages/common/smithy-output/api-group/rust-server","packages/common/smithy-output/api-group/rust","packages/common/smithy-output/api-identity/rust-server","packages/common/smithy-output/api-identity/rust","packages/common/smithy-output/api-job/rust-server","packages/common/smithy-output/api-job/rust","packages/common/smithy-output/api-auth/rust-server","packages/common/smithy-output/api-auth/rust","packages/common/smithy-output/api-kv/rust-server","packages/common/smithy-output/api-kv/rust","packages/common/migrate","packages/common/api-helper/macros","packages/common/api-helper/build","packages/common/global-error","packages/common/formatted-error","packages/common/test-images","packages/common/hub-embed","packages/common/service-manager","packages/common/schemac","packages/common/convert","packages/common/chirp-workflow/core","packages/common/chirp-workflow/macros","packages/common/claims","packages/api/provision","packages/api/ui","packages/api/matchmaker","packages/api/portal","packages/api/traefik-provider","packages/api/identity","packages/api/auth","packages/api/group","packages/api/monolith-edge","packages/api/cf-verification","packages/api/status","packages/api/cloud","packages/api/job","packages/api/games","packages/api/actor","packages/api/monolith-public","packages/services/token/ops/exchange","packages/services/token/ops/revoke","packages/services/token/ops/get","packages/services/token/ops/create","packages/services/cluster","packages/services/cluster/standalone/datacenter-tls-renew","packages/services/cluster/standalone/default-update","packages/services/cluster/standalone/gc","packages/services/cluster/standalone/metrics-publish","packages/services/email-verification/ops/complete","packages/services/email-verification/ops/create","packages/services/user-identity/ops/delete","packages/services/user-identity/ops/get","packages/services/user-identity/ops/create","packages/services/pegboard","packages/services/pegboard/standalone/dc-init","packages/services/pegboard/standalone/gc","packages/services/pegboard/standalone/ws","packages/services/monolith/standalone/workflow-worker","packages/services/monolith/standalone/worker","packages/services/ds","packages/services/linode","packages/services/linode/standalone/gc","packages/services/load-test/standalone/api-cloud","packages/services/load-test/standalone/watch-requests","packages/services/load-test/standalone/sqlx","packages/services/load-test/standalone/mm","packages/services/load-test/standalone/mm-sustain","packages/services/faker/ops/job-template","packages/services/faker/ops/mm-player","packages/services/faker/ops/user","packages/services/faker/ops/game","packages/services/faker/ops/game-version","packages/services/faker/ops/region","packages/services/faker/ops/team","packages/services/faker/ops/job-run","packages/services/faker/ops/game-namespace","packages/services/faker/ops/mm-lobby-row","packages/services/faker/ops/build","packages/services/faker/ops/mm-lobby","packages/services/faker/ops/cdn-site","packages/services/user","packages/services/user/standalone/delete-pending","packages/services/user/ops/resolve-email","packages/services/user/ops/get","packages/services/user/ops/profile-validate","packages/services/user/ops/pending-delete-toggle","packages/services/user/ops/token-create","packages/services/user/ops/team-list","packages/services/user/ops/avatar-upload-complete","packages/services/user/worker","packages/services/game/ops/namespace-version-set","packages/services/game/ops/version-validate","packages/services/game/ops/validate","packages/services/game/ops/resolve-namespace-id","packages/services/game/ops/namespace-version-history-list","packages/services/game/ops/namespace-resolve-name-id","packages/services/game/ops/recommend","packages/services/game/ops/version-list","packages/services/game/ops/namespace-validate","packages/services/game/ops/list-for-team","packages/services/game/ops/namespace-get","packages/services/game/ops/get","packages/services/game/ops/version-get","packages/services/game/ops/list-all","packages/services/game/ops/logo-upload-complete","packages/services/game/ops/namespace-list","packages/services/game/ops/namespace-create","packages/services/game/ops/resolve-name-id","packages/services/game/ops/namespace-resolve-url","packages/services/game/ops/create","packages/services/game/ops/version-create","packages/services/game/ops/banner-upload-complete","packages/services/game/ops/token-development-validate","packages/services/team-invite/ops/get","packages/services/team-invite/worker","packages/services/captcha/util","packages/services/captcha/ops/hcaptcha-config-get","packages/services/captcha/ops/verify","packages/services/captcha/ops/turnstile-verify","packages/services/captcha/ops/turnstile-config-get","packages/services/captcha/ops/request","packages/services/captcha/ops/hcaptcha-verify","packages/services/mm-config/ops/namespace-config-validate","packages/services/mm-config/ops/version-publish","packages/services/mm-config/ops/lobby-group-get","packages/services/mm-config/ops/namespace-get","packages/services/mm-config/ops/lobby-group-resolve-version","packages/services/mm-config/ops/version-get","packages/services/mm-config/ops/version-prepare","packages/services/mm-config/ops/lobby-group-resolve-name-id","packages/services/mm-config/ops/namespace-create","packages/services/mm-config/ops/game-upsert","packages/services/mm-config/ops/game-get","packages/services/mm-config/ops/namespace-config-set","packages/services/region/ops/resolve","packages/services/region/ops/recommend","packages/services/region/ops/get","packages/services/region/ops/resolve-for-game","packages/services/region/ops/list-for-game","packages/services/region/ops/list","packages/services/nomad/standalone/monitor","packages/services/cdn/util","packages/services/cdn/ops/site-get","packages/services/cdn/ops/site-list-for-game","packages/services/cdn/ops/namespace-resolve-domain","packages/services/cdn/ops/version-publish","packages/services/cdn/ops/namespace-domain-create","packages/services/cdn/ops/namespace-auth-user-remove","packages/services/cdn/ops/namespace-get","packages/services/cdn/ops/ns-enable-domain-public-auth-set","packages/services/cdn/ops/ns-auth-type-set","packages/services/cdn/ops/site-create","packages/services/cdn/ops/version-get","packages/services/cdn/ops/namespace-auth-user-update","packages/services/cdn/ops/version-prepare","packages/services/cdn/ops/namespace-domain-remove","packages/services/cdn/ops/namespace-create","packages/services/cdn/worker","packages/services/team/util","packages/services/team/ops/validate","packages/services/team/ops/user-ban-list","packages/services/team/ops/recommend","packages/services/team/ops/resolve-display-name","packages/services/team/ops/member-list","packages/services/team/ops/get","packages/services/team/ops/member-count","packages/services/team/ops/member-relationship-get","packages/services/team/ops/member-get","packages/services/team/ops/profile-validate","packages/services/team/ops/user-ban-get","packages/services/team/ops/join-request-list","packages/services/team/ops/avatar-upload-complete","packages/services/team/worker","packages/services/ds-log/ops/read","packages/services/ds-log/ops/export","packages/services/job-run","packages/services/workflow/standalone/gc","packages/services/workflow/standalone/metrics-publish","packages/services/cloud/standalone/default-create","packages/services/cloud/ops/version-publish","packages/services/cloud/ops/game-config-get","packages/services/cloud/ops/namespace-token-public-create","packages/services/cloud/ops/game-config-create","packages/services/cloud/ops/namespace-get","packages/services/cloud/ops/version-get","packages/services/cloud/ops/game-token-create","packages/services/cloud/ops/device-link-create","packages/services/cloud/ops/namespace-create","packages/services/cloud/ops/namespace-token-development-create","packages/services/cloud/worker","packages/services/job/util","packages/services/job/standalone/gc","packages/services/mm/util","packages/services/mm/standalone/gc","packages/services/mm/ops/player-get","packages/services/mm/ops/dev-player-token-create","packages/services/mm/ops/lobby-idle-update","packages/services/mm/ops/lobby-find-lobby-query-list","packages/services/mm/ops/lobby-find-fail","packages/services/mm/ops/lobby-list-for-user-id","packages/services/mm/ops/lobby-history","packages/services/mm/ops/lobby-list-for-namespace","packages/services/mm/ops/lobby-state-get","packages/services/mm/ops/lobby-player-count","packages/services/mm/ops/lobby-find-try-complete","packages/services/mm/ops/lobby-for-run-id","packages/services/mm/ops/player-count-for-namespace","packages/services/mm/ops/lobby-runtime-aggregate","packages/services/mm/ops/lobby-get","packages/services/mm/worker","packages/services/telemetry/standalone/beacon","packages/services/external/ops/request-validate","packages/services/external/worker","packages/services/build","packages/services/build/util","packages/services/build/standalone/default-create","packages/services/build/ops/get","packages/services/build/ops/list-for-game","packages/services/build/ops/list-for-env","packages/services/build/ops/create","packages/services/server-spec","packages/services/cf-custom-hostname/ops/get","packages/services/cf-custom-hostname/ops/resolve-hostname","packages/services/cf-custom-hostname/ops/list-for-namespace-id","packages/services/cf-custom-hostname/worker","packages/services/ip/ops/info","packages/services/dynamic-config","packages/services/email/ops/send","packages/services/job-log/ops/read","packages/services/job-log/worker","packages/services/upload/ops/prepare","packages/services/upload/ops/complete","packages/services/upload/ops/get","packages/services/upload/ops/file-list","packages/services/upload/ops/list-for-user","packages/services/upload/worker","packages/services/custom-user-avatar/ops/list-for-game","packages/services/custom-user-avatar/ops/upload-complete","packages/services/debug/ops/email-res","packages/services/tier","sdks/api/full/rust"] +members = ["packages/api/actor","packages/api/auth","packages/api/cf-verification","packages/api/cloud","packages/api/games","packages/api/group","packages/api/identity","packages/api/job","packages/api/matchmaker","packages/api/monolith-edge","packages/api/monolith-public","packages/api/portal","packages/api/provision","packages/api/status","packages/api/traefik-provider","packages/api/ui","packages/common/api-helper/build","packages/common/api-helper/macros","packages/common/cache/build","packages/common/cache/result","packages/common/chirp-workflow/core","packages/common/chirp-workflow/macros","packages/common/chirp/client","packages/common/chirp/metrics","packages/common/chirp/perf","packages/common/chirp/types","packages/common/chirp/worker","packages/common/chirp/worker-attributes","packages/common/claims","packages/common/config","packages/common/connection","packages/common/convert","packages/common/deno-embed","packages/common/env","packages/common/formatted-error","packages/common/global-error","packages/common/health-checks","packages/common/hub-embed","packages/common/kv-str","packages/common/metrics","packages/common/migrate","packages/common/nomad-util","packages/common/operation/core","packages/common/operation/macros","packages/common/pools","packages/common/redis-util","packages/common/runtime","packages/common/s3-util","packages/common/schemac","packages/common/service-manager","packages/common/smithy-output/api-auth/rust","packages/common/smithy-output/api-auth/rust-server","packages/common/smithy-output/api-cf-verification/rust","packages/common/smithy-output/api-cf-verification/rust-server","packages/common/smithy-output/api-cloud/rust","packages/common/smithy-output/api-cloud/rust-server","packages/common/smithy-output/api-group/rust","packages/common/smithy-output/api-group/rust-server","packages/common/smithy-output/api-identity/rust","packages/common/smithy-output/api-identity/rust-server","packages/common/smithy-output/api-job/rust","packages/common/smithy-output/api-job/rust-server","packages/common/smithy-output/api-kv/rust","packages/common/smithy-output/api-kv/rust-server","packages/common/smithy-output/api-matchmaker/rust","packages/common/smithy-output/api-matchmaker/rust-server","packages/common/smithy-output/api-party/rust","packages/common/smithy-output/api-party/rust-server","packages/common/smithy-output/api-portal/rust","packages/common/smithy-output/api-portal/rust-server","packages/common/smithy-output/api-status/rust","packages/common/smithy-output/api-status/rust-server","packages/common/smithy-output/api-traefik-provider/rust","packages/common/smithy-output/api-traefik-provider/rust-server","packages/common/test","packages/common/test-images","packages/common/types-proto/build","packages/common/types-proto/core","packages/common/util/core","packages/common/util/macros","packages/common/util/search","packages/infra/legacy/job-runner","packages/infra/server","packages/services/build","packages/services/build/ops/create","packages/services/build/ops/get","packages/services/build/ops/list-for-env","packages/services/build/ops/list-for-game","packages/services/build/standalone/default-create","packages/services/build/util","packages/services/captcha/ops/hcaptcha-config-get","packages/services/captcha/ops/hcaptcha-verify","packages/services/captcha/ops/request","packages/services/captcha/ops/turnstile-config-get","packages/services/captcha/ops/turnstile-verify","packages/services/captcha/ops/verify","packages/services/captcha/util","packages/services/cdn/ops/namespace-auth-user-remove","packages/services/cdn/ops/namespace-auth-user-update","packages/services/cdn/ops/namespace-create","packages/services/cdn/ops/namespace-domain-create","packages/services/cdn/ops/namespace-domain-remove","packages/services/cdn/ops/namespace-get","packages/services/cdn/ops/namespace-resolve-domain","packages/services/cdn/ops/ns-auth-type-set","packages/services/cdn/ops/ns-enable-domain-public-auth-set","packages/services/cdn/ops/site-create","packages/services/cdn/ops/site-get","packages/services/cdn/ops/site-list-for-game","packages/services/cdn/ops/version-get","packages/services/cdn/ops/version-prepare","packages/services/cdn/ops/version-publish","packages/services/cdn/util","packages/services/cdn/worker","packages/services/cf-custom-hostname/ops/get","packages/services/cf-custom-hostname/ops/list-for-namespace-id","packages/services/cf-custom-hostname/ops/resolve-hostname","packages/services/cf-custom-hostname/worker","packages/services/cloud/ops/device-link-create","packages/services/cloud/ops/game-config-create","packages/services/cloud/ops/game-config-get","packages/services/cloud/ops/game-token-create","packages/services/cloud/ops/namespace-create","packages/services/cloud/ops/namespace-get","packages/services/cloud/ops/namespace-token-development-create","packages/services/cloud/ops/namespace-token-public-create","packages/services/cloud/ops/version-get","packages/services/cloud/ops/version-publish","packages/services/cloud/standalone/default-create","packages/services/cloud/worker","packages/services/cluster","packages/services/cluster/standalone/datacenter-tls-renew","packages/services/cluster/standalone/default-update","packages/services/cluster/standalone/gc","packages/services/cluster/standalone/metrics-publish","packages/services/custom-user-avatar/ops/list-for-game","packages/services/custom-user-avatar/ops/upload-complete","packages/services/debug/ops/email-res","packages/services/ds","packages/services/ds-log/ops/export","packages/services/ds-log/ops/read","packages/services/dynamic-config","packages/services/email-verification/ops/complete","packages/services/email-verification/ops/create","packages/services/email/ops/send","packages/services/external/ops/request-validate","packages/services/external/worker","packages/services/faker/ops/build","packages/services/faker/ops/cdn-site","packages/services/faker/ops/game","packages/services/faker/ops/game-namespace","packages/services/faker/ops/game-version","packages/services/faker/ops/job-run","packages/services/faker/ops/job-template","packages/services/faker/ops/mm-lobby","packages/services/faker/ops/mm-lobby-row","packages/services/faker/ops/mm-player","packages/services/faker/ops/region","packages/services/faker/ops/team","packages/services/faker/ops/user","packages/services/game/ops/banner-upload-complete","packages/services/game/ops/create","packages/services/game/ops/get","packages/services/game/ops/list-all","packages/services/game/ops/list-for-team","packages/services/game/ops/logo-upload-complete","packages/services/game/ops/namespace-create","packages/services/game/ops/namespace-get","packages/services/game/ops/namespace-list","packages/services/game/ops/namespace-resolve-name-id","packages/services/game/ops/namespace-resolve-url","packages/services/game/ops/namespace-validate","packages/services/game/ops/namespace-version-history-list","packages/services/game/ops/namespace-version-set","packages/services/game/ops/recommend","packages/services/game/ops/resolve-name-id","packages/services/game/ops/resolve-namespace-id","packages/services/game/ops/token-development-validate","packages/services/game/ops/validate","packages/services/game/ops/version-create","packages/services/game/ops/version-get","packages/services/game/ops/version-list","packages/services/game/ops/version-validate","packages/services/ip/ops/info","packages/services/job-log/ops/read","packages/services/job-log/worker","packages/services/job-run","packages/services/job/standalone/gc","packages/services/job/util","packages/services/linode","packages/services/linode/standalone/gc","packages/services/load-test/standalone/api-cloud","packages/services/load-test/standalone/mm","packages/services/load-test/standalone/mm-sustain","packages/services/load-test/standalone/sqlx","packages/services/load-test/standalone/watch-requests","packages/services/mm-config/ops/game-get","packages/services/mm-config/ops/game-upsert","packages/services/mm-config/ops/lobby-group-get","packages/services/mm-config/ops/lobby-group-resolve-name-id","packages/services/mm-config/ops/lobby-group-resolve-version","packages/services/mm-config/ops/namespace-config-set","packages/services/mm-config/ops/namespace-config-validate","packages/services/mm-config/ops/namespace-create","packages/services/mm-config/ops/namespace-get","packages/services/mm-config/ops/version-get","packages/services/mm-config/ops/version-prepare","packages/services/mm-config/ops/version-publish","packages/services/mm/ops/dev-player-token-create","packages/services/mm/ops/lobby-find-fail","packages/services/mm/ops/lobby-find-lobby-query-list","packages/services/mm/ops/lobby-find-try-complete","packages/services/mm/ops/lobby-for-run-id","packages/services/mm/ops/lobby-get","packages/services/mm/ops/lobby-history","packages/services/mm/ops/lobby-idle-update","packages/services/mm/ops/lobby-list-for-namespace","packages/services/mm/ops/lobby-list-for-user-id","packages/services/mm/ops/lobby-player-count","packages/services/mm/ops/lobby-runtime-aggregate","packages/services/mm/ops/lobby-state-get","packages/services/mm/ops/player-count-for-namespace","packages/services/mm/ops/player-get","packages/services/mm/standalone/gc","packages/services/mm/util","packages/services/mm/worker","packages/services/monolith/standalone/worker","packages/services/monolith/standalone/workflow-worker","packages/services/nomad/standalone/monitor","packages/services/pegboard","packages/services/pegboard/standalone/dc-init","packages/services/pegboard/standalone/gc","packages/services/pegboard/standalone/ws","packages/services/region/ops/get","packages/services/region/ops/list","packages/services/region/ops/list-for-game","packages/services/region/ops/recommend","packages/services/region/ops/resolve","packages/services/region/ops/resolve-for-game","packages/services/server-spec","packages/services/team-invite/ops/get","packages/services/team-invite/worker","packages/services/team/ops/avatar-upload-complete","packages/services/team/ops/get","packages/services/team/ops/join-request-list","packages/services/team/ops/member-count","packages/services/team/ops/member-get","packages/services/team/ops/member-list","packages/services/team/ops/member-relationship-get","packages/services/team/ops/profile-validate","packages/services/team/ops/recommend","packages/services/team/ops/resolve-display-name","packages/services/team/ops/user-ban-get","packages/services/team/ops/user-ban-list","packages/services/team/ops/validate","packages/services/team/util","packages/services/team/worker","packages/services/telemetry/standalone/beacon","packages/services/tier","packages/services/token/ops/create","packages/services/token/ops/exchange","packages/services/token/ops/get","packages/services/token/ops/revoke","packages/services/upload/ops/complete","packages/services/upload/ops/file-list","packages/services/upload/ops/get","packages/services/upload/ops/list-for-user","packages/services/upload/ops/prepare","packages/services/upload/worker","packages/services/user","packages/services/user-identity/ops/create","packages/services/user-identity/ops/delete","packages/services/user-identity/ops/get","packages/services/user/ops/avatar-upload-complete","packages/services/user/ops/get","packages/services/user/ops/pending-delete-toggle","packages/services/user/ops/profile-validate","packages/services/user/ops/resolve-email","packages/services/user/ops/team-list","packages/services/user/ops/token-create","packages/services/user/standalone/delete-pending","packages/services/user/worker","packages/services/workflow/standalone/gc","packages/services/workflow/standalone/metrics-publish","packages/toolchain/cli","packages/toolchain/js-utils-embed","packages/toolchain/toolchain","sdks/api/full/rust"] [workspace.package] version = "0.0.1" @@ -42,883 +42,898 @@ rev = "ac3e27f" git = "https://github.com/rivet-gg/serde_array_query" rev = "b9f8bfa" -[workspace.dependencies.job-runner] -path = "packages/infra/legacy/job-runner" +[workspace.dependencies.api-actor] +path = "packages/api/actor" -[workspace.dependencies.rivet-server] -path = "packages/infra/server" +[workspace.dependencies.api-auth] +path = "packages/api/auth" -[workspace.dependencies.rivet-operation] -path = "packages/common/operation/core" +[workspace.dependencies.api-cf-verification] +path = "packages/api/cf-verification" -[workspace.dependencies.rivet-operation-macros] -path = "packages/common/operation/macros" +[workspace.dependencies.api-cloud] +path = "packages/api/cloud" -[workspace.dependencies.rivet-metrics] -path = "packages/common/metrics" +[workspace.dependencies.api-games] +path = "packages/api/games" -[workspace.dependencies.rivet-pools] -path = "packages/common/pools" +[workspace.dependencies.api-group] +path = "packages/api/group" -[workspace.dependencies.s3-util] -path = "packages/common/s3-util" +[workspace.dependencies.api-identity] +path = "packages/api/identity" -[workspace.dependencies.rivet-cache-result] -path = "packages/common/cache/result" +[workspace.dependencies.api-job] +path = "packages/api/job" -[workspace.dependencies.rivet-cache] -path = "packages/common/cache/build" +[workspace.dependencies.api-matchmaker] +path = "packages/api/matchmaker" -[workspace.dependencies.rivet-test] -path = "packages/common/test" +[workspace.dependencies.api-monolith-edge] +path = "packages/api/monolith-edge" -[workspace.dependencies.rivet-connection] -path = "packages/common/connection" +[workspace.dependencies.api-monolith-public] +path = "packages/api/monolith-public" -[workspace.dependencies.rivet-util] -path = "packages/common/util/core" +[workspace.dependencies.api-portal] +path = "packages/api/portal" -[workspace.dependencies.util] -package = "rivet-util" -path = "packages/common/util/core" +[workspace.dependencies.api-provision] +path = "packages/api/provision" -[workspace.dependencies.rivet-util-search] -path = "packages/common/util/search" +[workspace.dependencies.api-status] +path = "packages/api/status" -[workspace.dependencies.util-search] -package = "rivet-util-search" -path = "packages/common/util/search" +[workspace.dependencies.api-traefik-provider] +path = "packages/api/traefik-provider" -[workspace.dependencies.rivet-util-macros] -path = "packages/common/util/macros" +[workspace.dependencies.api-ui] +path = "packages/api/ui" -[workspace.dependencies.rivet-config] -path = "packages/common/config" +[workspace.dependencies.api-helper] +path = "packages/common/api-helper/build" -[workspace.dependencies.nomad-util] -path = "packages/common/nomad-util" +[workspace.dependencies.api-helper-macros] +path = "packages/common/api-helper/macros" -[workspace.dependencies.rivet-health-checks] -path = "packages/common/health-checks" +[workspace.dependencies.rivet-cache] +path = "packages/common/cache/build" + +[workspace.dependencies.rivet-cache-result] +path = "packages/common/cache/result" + +[workspace.dependencies.chirp-workflow] +path = "packages/common/chirp-workflow/core" + +[workspace.dependencies.chirp-workflow-macros] +path = "packages/common/chirp-workflow/macros" + +[workspace.dependencies.chirp-client] +path = "packages/common/chirp/client" [workspace.dependencies.chirp-metrics] path = "packages/common/chirp/metrics" +[workspace.dependencies.chirp-perf] +path = "packages/common/chirp/perf" + [workspace.dependencies.chirp-types] path = "packages/common/chirp/types" [workspace.dependencies.chirp-worker] path = "packages/common/chirp/worker" -[workspace.dependencies.chirp-perf] -path = "packages/common/chirp/perf" - [workspace.dependencies.chirp-worker-attributes] path = "packages/common/chirp/worker-attributes" -[workspace.dependencies.chirp-client] -path = "packages/common/chirp/client" +[workspace.dependencies.rivet-claims] +path = "packages/common/claims" -[workspace.dependencies.rivet-runtime] -path = "packages/common/runtime" +[workspace.dependencies.rivet-config] +path = "packages/common/config" -[workspace.dependencies.redis-util] -path = "packages/common/redis-util" +[workspace.dependencies.rivet-connection] +path = "packages/common/connection" + +[workspace.dependencies.rivet-convert] +path = "packages/common/convert" + +[workspace.dependencies.deno-embed] +path = "packages/common/deno-embed" [workspace.dependencies.rivet-env] path = "packages/common/env" -[workspace.dependencies.types-proto] -path = "packages/common/types-proto/core" +[workspace.dependencies.formatted-error] +path = "packages/common/formatted-error" -[workspace.dependencies.types-proto-build] -path = "packages/common/types-proto/build" +[workspace.dependencies.global-error] +path = "packages/common/global-error" -[workspace.dependencies.rivet-status-server] -path = "packages/common/smithy-output/api-status/rust-server" +[workspace.dependencies.rivet-health-checks] +path = "packages/common/health-checks" -[workspace.dependencies.rivet-status] -path = "packages/common/smithy-output/api-status/rust" +[workspace.dependencies.rivet-hub-embed] +path = "packages/common/hub-embed" -[workspace.dependencies.rivet-matchmaker-server] -path = "packages/common/smithy-output/api-matchmaker/rust-server" +[workspace.dependencies.kv-str] +path = "packages/common/kv-str" -[workspace.dependencies.rivet-matchmaker] -path = "packages/common/smithy-output/api-matchmaker/rust" +[workspace.dependencies.rivet-metrics] +path = "packages/common/metrics" -[workspace.dependencies.rivet-cloud-server] -path = "packages/common/smithy-output/api-cloud/rust-server" +[workspace.dependencies.rivet-migrate] +path = "packages/common/migrate" -[workspace.dependencies.rivet-cloud] -path = "packages/common/smithy-output/api-cloud/rust" +[workspace.dependencies.nomad-util] +path = "packages/common/nomad-util" -[workspace.dependencies.rivet-portal-server] -path = "packages/common/smithy-output/api-portal/rust-server" +[workspace.dependencies.rivet-operation] +path = "packages/common/operation/core" -[workspace.dependencies.rivet-portal] -path = "packages/common/smithy-output/api-portal/rust" +[workspace.dependencies.rivet-operation-macros] +path = "packages/common/operation/macros" -[workspace.dependencies.rivet-cf-verification-server] -path = "packages/common/smithy-output/api-cf-verification/rust-server" +[workspace.dependencies.rivet-pools] +path = "packages/common/pools" -[workspace.dependencies.rivet-cf-verification] -path = "packages/common/smithy-output/api-cf-verification/rust" +[workspace.dependencies.redis-util] +path = "packages/common/redis-util" -[workspace.dependencies.rivet-party-server] -path = "packages/common/smithy-output/api-party/rust-server" +[workspace.dependencies.rivet-runtime] +path = "packages/common/runtime" -[workspace.dependencies.rivet-party] -path = "packages/common/smithy-output/api-party/rust" +[workspace.dependencies.s3-util] +path = "packages/common/s3-util" -[workspace.dependencies.rivet-traefik-provider-server] -path = "packages/common/smithy-output/api-traefik-provider/rust-server" +[workspace.dependencies.schemac] +path = "packages/common/schemac" -[workspace.dependencies.rivet-route] -path = "packages/common/smithy-output/api-traefik-provider/rust" +[workspace.dependencies.rivet-service-manager] +path = "packages/common/service-manager" -[workspace.dependencies.rivet-group-server] -path = "packages/common/smithy-output/api-group/rust-server" +[workspace.dependencies.rivet-auth] +path = "packages/common/smithy-output/api-auth/rust" + +[workspace.dependencies.rivet-auth-server] +path = "packages/common/smithy-output/api-auth/rust-server" + +[workspace.dependencies.rivet-cf-verification] +path = "packages/common/smithy-output/api-cf-verification/rust" + +[workspace.dependencies.rivet-cf-verification-server] +path = "packages/common/smithy-output/api-cf-verification/rust-server" + +[workspace.dependencies.rivet-cloud] +path = "packages/common/smithy-output/api-cloud/rust" + +[workspace.dependencies.rivet-cloud-server] +path = "packages/common/smithy-output/api-cloud/rust-server" [workspace.dependencies.rivet-group] path = "packages/common/smithy-output/api-group/rust" -[workspace.dependencies.rivet-identity-server] -path = "packages/common/smithy-output/api-identity/rust-server" +[workspace.dependencies.rivet-group-server] +path = "packages/common/smithy-output/api-group/rust-server" [workspace.dependencies.rivet-identity] path = "packages/common/smithy-output/api-identity/rust" -[workspace.dependencies.rivet-job-server] -path = "packages/common/smithy-output/api-job/rust-server" +[workspace.dependencies.rivet-identity-server] +path = "packages/common/smithy-output/api-identity/rust-server" [workspace.dependencies.rivet-job] path = "packages/common/smithy-output/api-job/rust" -[workspace.dependencies.rivet-auth-server] -path = "packages/common/smithy-output/api-auth/rust-server" +[workspace.dependencies.rivet-job-server] +path = "packages/common/smithy-output/api-job/rust-server" -[workspace.dependencies.rivet-auth] -path = "packages/common/smithy-output/api-auth/rust" +[workspace.dependencies.rivet-kv] +path = "packages/common/smithy-output/api-kv/rust" [workspace.dependencies.rivet-kv-server] path = "packages/common/smithy-output/api-kv/rust-server" -[workspace.dependencies.rivet-kv] -path = "packages/common/smithy-output/api-kv/rust" - -[workspace.dependencies.rivet-migrate] -path = "packages/common/migrate" +[workspace.dependencies.rivet-matchmaker] +path = "packages/common/smithy-output/api-matchmaker/rust" -[workspace.dependencies.api-helper-macros] -path = "packages/common/api-helper/macros" +[workspace.dependencies.rivet-matchmaker-server] +path = "packages/common/smithy-output/api-matchmaker/rust-server" -[workspace.dependencies.api-helper] -path = "packages/common/api-helper/build" +[workspace.dependencies.rivet-party] +path = "packages/common/smithy-output/api-party/rust" -[workspace.dependencies.global-error] -path = "packages/common/global-error" +[workspace.dependencies.rivet-party-server] +path = "packages/common/smithy-output/api-party/rust-server" -[workspace.dependencies.formatted-error] -path = "packages/common/formatted-error" +[workspace.dependencies.rivet-portal] +path = "packages/common/smithy-output/api-portal/rust" -[workspace.dependencies.rivet-test-images] -path = "packages/common/test-images" +[workspace.dependencies.rivet-portal-server] +path = "packages/common/smithy-output/api-portal/rust-server" -[workspace.dependencies.rivet-hub-embed] -path = "packages/common/hub-embed" +[workspace.dependencies.rivet-status] +path = "packages/common/smithy-output/api-status/rust" -[workspace.dependencies.rivet-service-manager] -path = "packages/common/service-manager" +[workspace.dependencies.rivet-status-server] +path = "packages/common/smithy-output/api-status/rust-server" -[workspace.dependencies.schemac] -path = "packages/common/schemac" +[workspace.dependencies.rivet-route] +path = "packages/common/smithy-output/api-traefik-provider/rust" -[workspace.dependencies.rivet-convert] -path = "packages/common/convert" +[workspace.dependencies.rivet-traefik-provider-server] +path = "packages/common/smithy-output/api-traefik-provider/rust-server" -[workspace.dependencies.chirp-workflow] -path = "packages/common/chirp-workflow/core" +[workspace.dependencies.rivet-test] +path = "packages/common/test" -[workspace.dependencies.chirp-workflow-macros] -path = "packages/common/chirp-workflow/macros" - -[workspace.dependencies.rivet-claims] -path = "packages/common/claims" - -[workspace.dependencies.api-provision] -path = "packages/api/provision" - -[workspace.dependencies.api-ui] -path = "packages/api/ui" - -[workspace.dependencies.api-matchmaker] -path = "packages/api/matchmaker" - -[workspace.dependencies.api-portal] -path = "packages/api/portal" - -[workspace.dependencies.api-traefik-provider] -path = "packages/api/traefik-provider" - -[workspace.dependencies.api-identity] -path = "packages/api/identity" - -[workspace.dependencies.api-auth] -path = "packages/api/auth" - -[workspace.dependencies.api-group] -path = "packages/api/group" - -[workspace.dependencies.api-monolith-edge] -path = "packages/api/monolith-edge" - -[workspace.dependencies.api-cf-verification] -path = "packages/api/cf-verification" - -[workspace.dependencies.api-status] -path = "packages/api/status" - -[workspace.dependencies.api-cloud] -path = "packages/api/cloud" - -[workspace.dependencies.api-job] -path = "packages/api/job" - -[workspace.dependencies.api-games] -path = "packages/api/games" - -[workspace.dependencies.api-actor] -path = "packages/api/actor" - -[workspace.dependencies.api-monolith-public] -path = "packages/api/monolith-public" - -[workspace.dependencies.token-exchange] -path = "packages/services/token/ops/exchange" - -[workspace.dependencies.token-revoke] -path = "packages/services/token/ops/revoke" - -[workspace.dependencies.token-get] -path = "packages/services/token/ops/get" - -[workspace.dependencies.token-create] -path = "packages/services/token/ops/create" - -[workspace.dependencies.cluster] -path = "packages/services/cluster" - -[workspace.dependencies.cluster-datacenter-tls-renew] -path = "packages/services/cluster/standalone/datacenter-tls-renew" - -[workspace.dependencies.cluster-default-update] -path = "packages/services/cluster/standalone/default-update" - -[workspace.dependencies.cluster-gc] -path = "packages/services/cluster/standalone/gc" - -[workspace.dependencies.cluster-metrics-publish] -path = "packages/services/cluster/standalone/metrics-publish" - -[workspace.dependencies.email-verification-complete] -path = "packages/services/email-verification/ops/complete" - -[workspace.dependencies.email-verification-create] -path = "packages/services/email-verification/ops/create" - -[workspace.dependencies.user-identity-delete] -path = "packages/services/user-identity/ops/delete" - -[workspace.dependencies.user-identity-get] -path = "packages/services/user-identity/ops/get" - -[workspace.dependencies.user-identity-create] -path = "packages/services/user-identity/ops/create" - -[workspace.dependencies.pegboard] -path = "packages/services/pegboard" - -[workspace.dependencies.pegboard-dc-init] -path = "packages/services/pegboard/standalone/dc-init" - -[workspace.dependencies.pegboard-gc] -path = "packages/services/pegboard/standalone/gc" - -[workspace.dependencies.pegboard-ws] -path = "packages/services/pegboard/standalone/ws" - -[workspace.dependencies.monolith-workflow-worker] -path = "packages/services/monolith/standalone/workflow-worker" - -[workspace.dependencies.monolith-worker] -path = "packages/services/monolith/standalone/worker" - -[workspace.dependencies.ds] -path = "packages/services/ds" - -[workspace.dependencies.linode] -path = "packages/services/linode" - -[workspace.dependencies.linode-gc] -path = "packages/services/linode/standalone/gc" - -[workspace.dependencies.load-test-api-cloud] -path = "packages/services/load-test/standalone/api-cloud" +[workspace.dependencies.rivet-test-images] +path = "packages/common/test-images" -[workspace.dependencies.load-test-watch-requests] -path = "packages/services/load-test/standalone/watch-requests" +[workspace.dependencies.types-proto-build] +path = "packages/common/types-proto/build" -[workspace.dependencies.load-test-sqlx] -path = "packages/services/load-test/standalone/sqlx" +[workspace.dependencies.types-proto] +path = "packages/common/types-proto/core" -[workspace.dependencies.load-test-mm] -path = "packages/services/load-test/standalone/mm" +[workspace.dependencies.rivet-util] +path = "packages/common/util/core" -[workspace.dependencies.load-test-mm-sustain] -path = "packages/services/load-test/standalone/mm-sustain" +[workspace.dependencies.util] +package = "rivet-util" +path = "packages/common/util/core" -[workspace.dependencies.faker-job-template] -path = "packages/services/faker/ops/job-template" +[workspace.dependencies.rivet-util-macros] +path = "packages/common/util/macros" -[workspace.dependencies.faker-mm-player] -path = "packages/services/faker/ops/mm-player" +[workspace.dependencies.rivet-util-search] +path = "packages/common/util/search" -[workspace.dependencies.faker-user] -path = "packages/services/faker/ops/user" +[workspace.dependencies.util-search] +package = "rivet-util-search" +path = "packages/common/util/search" -[workspace.dependencies.faker-game] -path = "packages/services/faker/ops/game" +[workspace.dependencies.job-runner] +path = "packages/infra/legacy/job-runner" -[workspace.dependencies.faker-game-version] -path = "packages/services/faker/ops/game-version" +[workspace.dependencies.rivet-server] +path = "packages/infra/server" -[workspace.dependencies.faker-region] -path = "packages/services/faker/ops/region" +[workspace.dependencies.build] +path = "packages/services/build" -[workspace.dependencies.faker-team] -path = "packages/services/faker/ops/team" +[workspace.dependencies.build-create] +path = "packages/services/build/ops/create" -[workspace.dependencies.faker-job-run] -path = "packages/services/faker/ops/job-run" +[workspace.dependencies.build-get] +path = "packages/services/build/ops/get" -[workspace.dependencies.faker-game-namespace] -path = "packages/services/faker/ops/game-namespace" +[workspace.dependencies.build-list-for-env] +path = "packages/services/build/ops/list-for-env" -[workspace.dependencies.faker-mm-lobby-row] -path = "packages/services/faker/ops/mm-lobby-row" +[workspace.dependencies.build-list-for-game] +path = "packages/services/build/ops/list-for-game" -[workspace.dependencies.faker-build] -path = "packages/services/faker/ops/build" +[workspace.dependencies.build-default-create] +path = "packages/services/build/standalone/default-create" -[workspace.dependencies.faker-mm-lobby] -path = "packages/services/faker/ops/mm-lobby" +[workspace.dependencies.rivet-util-build] +path = "packages/services/build/util" -[workspace.dependencies.faker-cdn-site] -path = "packages/services/faker/ops/cdn-site" +[workspace.dependencies.util-build] +package = "rivet-util-build" +path = "packages/services/build/util" -[workspace.dependencies.user] -path = "packages/services/user" +[workspace.dependencies.captcha-hcaptcha-config-get] +path = "packages/services/captcha/ops/hcaptcha-config-get" -[workspace.dependencies.user-delete-pending] -path = "packages/services/user/standalone/delete-pending" +[workspace.dependencies.captcha-hcaptcha-verify] +path = "packages/services/captcha/ops/hcaptcha-verify" -[workspace.dependencies.user-resolve-email] -path = "packages/services/user/ops/resolve-email" +[workspace.dependencies.captcha-request] +path = "packages/services/captcha/ops/request" -[workspace.dependencies.user-get] -path = "packages/services/user/ops/get" +[workspace.dependencies.captcha-turnstile-config-get] +path = "packages/services/captcha/ops/turnstile-config-get" -[workspace.dependencies.user-profile-validate] -path = "packages/services/user/ops/profile-validate" +[workspace.dependencies.captcha-turnstile-verify] +path = "packages/services/captcha/ops/turnstile-verify" -[workspace.dependencies.user-pending-delete-toggle] -path = "packages/services/user/ops/pending-delete-toggle" +[workspace.dependencies.captcha-verify] +path = "packages/services/captcha/ops/verify" -[workspace.dependencies.user-token-create] -path = "packages/services/user/ops/token-create" +[workspace.dependencies.rivet-util-captcha] +path = "packages/services/captcha/util" -[workspace.dependencies.user-team-list] -path = "packages/services/user/ops/team-list" +[workspace.dependencies.util-captcha] +package = "rivet-util-captcha" +path = "packages/services/captcha/util" -[workspace.dependencies.user-avatar-upload-complete] -path = "packages/services/user/ops/avatar-upload-complete" +[workspace.dependencies.cdn-namespace-auth-user-remove] +path = "packages/services/cdn/ops/namespace-auth-user-remove" -[workspace.dependencies.user-worker] -path = "packages/services/user/worker" +[workspace.dependencies.cdn-namespace-auth-user-update] +path = "packages/services/cdn/ops/namespace-auth-user-update" -[workspace.dependencies.game-namespace-version-set] -path = "packages/services/game/ops/namespace-version-set" +[workspace.dependencies.cdn-namespace-create] +path = "packages/services/cdn/ops/namespace-create" -[workspace.dependencies.game-version-validate] -path = "packages/services/game/ops/version-validate" +[workspace.dependencies.cdn-namespace-domain-create] +path = "packages/services/cdn/ops/namespace-domain-create" -[workspace.dependencies.game-validate] -path = "packages/services/game/ops/validate" +[workspace.dependencies.cdn-namespace-domain-remove] +path = "packages/services/cdn/ops/namespace-domain-remove" -[workspace.dependencies.game-resolve-namespace-id] -path = "packages/services/game/ops/resolve-namespace-id" +[workspace.dependencies.cdn-namespace-get] +path = "packages/services/cdn/ops/namespace-get" -[workspace.dependencies.game-namespace-version-history-list] -path = "packages/services/game/ops/namespace-version-history-list" +[workspace.dependencies.cdn-namespace-resolve-domain] +path = "packages/services/cdn/ops/namespace-resolve-domain" -[workspace.dependencies.game-namespace-resolve-name-id] -path = "packages/services/game/ops/namespace-resolve-name-id" +[workspace.dependencies.cdn-ns-auth-type-set] +path = "packages/services/cdn/ops/ns-auth-type-set" -[workspace.dependencies.game-recommend] -path = "packages/services/game/ops/recommend" +[workspace.dependencies.cdn-ns-enable-domain-public-auth-set] +path = "packages/services/cdn/ops/ns-enable-domain-public-auth-set" -[workspace.dependencies.game-version-list] -path = "packages/services/game/ops/version-list" +[workspace.dependencies.cdn-site-create] +path = "packages/services/cdn/ops/site-create" -[workspace.dependencies.game-namespace-validate] -path = "packages/services/game/ops/namespace-validate" +[workspace.dependencies.cdn-site-get] +path = "packages/services/cdn/ops/site-get" -[workspace.dependencies.game-list-for-team] -path = "packages/services/game/ops/list-for-team" +[workspace.dependencies.cdn-site-list-for-game] +path = "packages/services/cdn/ops/site-list-for-game" -[workspace.dependencies.game-namespace-get] -path = "packages/services/game/ops/namespace-get" +[workspace.dependencies.cdn-version-get] +path = "packages/services/cdn/ops/version-get" -[workspace.dependencies.game-get] -path = "packages/services/game/ops/get" +[workspace.dependencies.cdn-version-prepare] +path = "packages/services/cdn/ops/version-prepare" -[workspace.dependencies.game-version-get] -path = "packages/services/game/ops/version-get" +[workspace.dependencies.cdn-version-publish] +path = "packages/services/cdn/ops/version-publish" -[workspace.dependencies.game-list-all] -path = "packages/services/game/ops/list-all" +[workspace.dependencies.rivet-util-cdn] +path = "packages/services/cdn/util" -[workspace.dependencies.game-logo-upload-complete] -path = "packages/services/game/ops/logo-upload-complete" +[workspace.dependencies.util-cdn] +package = "rivet-util-cdn" +path = "packages/services/cdn/util" -[workspace.dependencies.game-namespace-list] -path = "packages/services/game/ops/namespace-list" +[workspace.dependencies.cdn-worker] +path = "packages/services/cdn/worker" -[workspace.dependencies.game-namespace-create] -path = "packages/services/game/ops/namespace-create" +[workspace.dependencies.cf-custom-hostname-get] +path = "packages/services/cf-custom-hostname/ops/get" -[workspace.dependencies.game-resolve-name-id] -path = "packages/services/game/ops/resolve-name-id" +[workspace.dependencies.cf-custom-hostname-list-for-namespace-id] +path = "packages/services/cf-custom-hostname/ops/list-for-namespace-id" -[workspace.dependencies.game-namespace-resolve-url] -path = "packages/services/game/ops/namespace-resolve-url" +[workspace.dependencies.cf-custom-hostname-resolve-hostname] +path = "packages/services/cf-custom-hostname/ops/resolve-hostname" -[workspace.dependencies.game-create] -path = "packages/services/game/ops/create" +[workspace.dependencies.cf-custom-hostname-worker] +path = "packages/services/cf-custom-hostname/worker" -[workspace.dependencies.game-version-create] -path = "packages/services/game/ops/version-create" +[workspace.dependencies.cloud-device-link-create] +path = "packages/services/cloud/ops/device-link-create" -[workspace.dependencies.game-banner-upload-complete] -path = "packages/services/game/ops/banner-upload-complete" +[workspace.dependencies.cloud-game-config-create] +path = "packages/services/cloud/ops/game-config-create" -[workspace.dependencies.game-token-development-validate] -path = "packages/services/game/ops/token-development-validate" +[workspace.dependencies.cloud-game-config-get] +path = "packages/services/cloud/ops/game-config-get" -[workspace.dependencies.team-invite-get] -path = "packages/services/team-invite/ops/get" +[workspace.dependencies.cloud-game-token-create] +path = "packages/services/cloud/ops/game-token-create" -[workspace.dependencies.team-invite-worker] -path = "packages/services/team-invite/worker" +[workspace.dependencies.cloud-namespace-create] +path = "packages/services/cloud/ops/namespace-create" -[workspace.dependencies.rivet-util-captcha] -path = "packages/services/captcha/util" +[workspace.dependencies.cloud-namespace-get] +path = "packages/services/cloud/ops/namespace-get" -[workspace.dependencies.util-captcha] -package = "rivet-util-captcha" -path = "packages/services/captcha/util" +[workspace.dependencies.cloud-namespace-token-development-create] +path = "packages/services/cloud/ops/namespace-token-development-create" -[workspace.dependencies.captcha-hcaptcha-config-get] -path = "packages/services/captcha/ops/hcaptcha-config-get" +[workspace.dependencies.cloud-namespace-token-public-create] +path = "packages/services/cloud/ops/namespace-token-public-create" -[workspace.dependencies.captcha-verify] -path = "packages/services/captcha/ops/verify" +[workspace.dependencies.cloud-version-get] +path = "packages/services/cloud/ops/version-get" -[workspace.dependencies.captcha-turnstile-verify] -path = "packages/services/captcha/ops/turnstile-verify" +[workspace.dependencies.cloud-version-publish] +path = "packages/services/cloud/ops/version-publish" -[workspace.dependencies.captcha-turnstile-config-get] -path = "packages/services/captcha/ops/turnstile-config-get" +[workspace.dependencies.cloud-default-create] +path = "packages/services/cloud/standalone/default-create" -[workspace.dependencies.captcha-request] -path = "packages/services/captcha/ops/request" +[workspace.dependencies.cloud-worker] +path = "packages/services/cloud/worker" -[workspace.dependencies.captcha-hcaptcha-verify] -path = "packages/services/captcha/ops/hcaptcha-verify" +[workspace.dependencies.cluster] +path = "packages/services/cluster" -[workspace.dependencies.mm-config-namespace-config-validate] -path = "packages/services/mm-config/ops/namespace-config-validate" +[workspace.dependencies.cluster-datacenter-tls-renew] +path = "packages/services/cluster/standalone/datacenter-tls-renew" -[workspace.dependencies.mm-config-version-publish] -path = "packages/services/mm-config/ops/version-publish" +[workspace.dependencies.cluster-default-update] +path = "packages/services/cluster/standalone/default-update" -[workspace.dependencies.mm-config-lobby-group-get] -path = "packages/services/mm-config/ops/lobby-group-get" +[workspace.dependencies.cluster-gc] +path = "packages/services/cluster/standalone/gc" -[workspace.dependencies.mm-config-namespace-get] -path = "packages/services/mm-config/ops/namespace-get" +[workspace.dependencies.cluster-metrics-publish] +path = "packages/services/cluster/standalone/metrics-publish" -[workspace.dependencies.mm-config-lobby-group-resolve-version] -path = "packages/services/mm-config/ops/lobby-group-resolve-version" +[workspace.dependencies.custom-user-avatar-list-for-game] +path = "packages/services/custom-user-avatar/ops/list-for-game" -[workspace.dependencies.mm-config-version-get] -path = "packages/services/mm-config/ops/version-get" +[workspace.dependencies.custom-user-avatar-upload-complete] +path = "packages/services/custom-user-avatar/ops/upload-complete" -[workspace.dependencies.mm-config-version-prepare] -path = "packages/services/mm-config/ops/version-prepare" +[workspace.dependencies.debug-email-res] +path = "packages/services/debug/ops/email-res" -[workspace.dependencies.mm-config-lobby-group-resolve-name-id] -path = "packages/services/mm-config/ops/lobby-group-resolve-name-id" +[workspace.dependencies.ds] +path = "packages/services/ds" -[workspace.dependencies.mm-config-namespace-create] -path = "packages/services/mm-config/ops/namespace-create" +[workspace.dependencies.ds-log-export] +path = "packages/services/ds-log/ops/export" -[workspace.dependencies.mm-config-game-upsert] -path = "packages/services/mm-config/ops/game-upsert" +[workspace.dependencies.ds-log-read] +path = "packages/services/ds-log/ops/read" -[workspace.dependencies.mm-config-game-get] -path = "packages/services/mm-config/ops/game-get" +[workspace.dependencies.dynamic-config] +path = "packages/services/dynamic-config" -[workspace.dependencies.mm-config-namespace-config-set] -path = "packages/services/mm-config/ops/namespace-config-set" +[workspace.dependencies.email-verification-complete] +path = "packages/services/email-verification/ops/complete" -[workspace.dependencies.region-resolve] -path = "packages/services/region/ops/resolve" +[workspace.dependencies.email-verification-create] +path = "packages/services/email-verification/ops/create" -[workspace.dependencies.region-recommend] -path = "packages/services/region/ops/recommend" +[workspace.dependencies.email-send] +path = "packages/services/email/ops/send" -[workspace.dependencies.region-get] -path = "packages/services/region/ops/get" +[workspace.dependencies.external-request-validate] +path = "packages/services/external/ops/request-validate" -[workspace.dependencies.region-resolve-for-game] -path = "packages/services/region/ops/resolve-for-game" +[workspace.dependencies.external-worker] +path = "packages/services/external/worker" -[workspace.dependencies.region-list-for-game] -path = "packages/services/region/ops/list-for-game" +[workspace.dependencies.faker-build] +path = "packages/services/faker/ops/build" -[workspace.dependencies.region-list] -path = "packages/services/region/ops/list" +[workspace.dependencies.faker-cdn-site] +path = "packages/services/faker/ops/cdn-site" -[workspace.dependencies.nomad-monitor] -path = "packages/services/nomad/standalone/monitor" +[workspace.dependencies.faker-game] +path = "packages/services/faker/ops/game" -[workspace.dependencies.rivet-util-cdn] -path = "packages/services/cdn/util" +[workspace.dependencies.faker-game-namespace] +path = "packages/services/faker/ops/game-namespace" -[workspace.dependencies.util-cdn] -package = "rivet-util-cdn" -path = "packages/services/cdn/util" +[workspace.dependencies.faker-game-version] +path = "packages/services/faker/ops/game-version" -[workspace.dependencies.cdn-site-get] -path = "packages/services/cdn/ops/site-get" +[workspace.dependencies.faker-job-run] +path = "packages/services/faker/ops/job-run" -[workspace.dependencies.cdn-site-list-for-game] -path = "packages/services/cdn/ops/site-list-for-game" +[workspace.dependencies.faker-job-template] +path = "packages/services/faker/ops/job-template" -[workspace.dependencies.cdn-namespace-resolve-domain] -path = "packages/services/cdn/ops/namespace-resolve-domain" +[workspace.dependencies.faker-mm-lobby] +path = "packages/services/faker/ops/mm-lobby" -[workspace.dependencies.cdn-version-publish] -path = "packages/services/cdn/ops/version-publish" +[workspace.dependencies.faker-mm-lobby-row] +path = "packages/services/faker/ops/mm-lobby-row" -[workspace.dependencies.cdn-namespace-domain-create] -path = "packages/services/cdn/ops/namespace-domain-create" +[workspace.dependencies.faker-mm-player] +path = "packages/services/faker/ops/mm-player" -[workspace.dependencies.cdn-namespace-auth-user-remove] -path = "packages/services/cdn/ops/namespace-auth-user-remove" +[workspace.dependencies.faker-region] +path = "packages/services/faker/ops/region" -[workspace.dependencies.cdn-namespace-get] -path = "packages/services/cdn/ops/namespace-get" +[workspace.dependencies.faker-team] +path = "packages/services/faker/ops/team" -[workspace.dependencies.cdn-ns-enable-domain-public-auth-set] -path = "packages/services/cdn/ops/ns-enable-domain-public-auth-set" +[workspace.dependencies.faker-user] +path = "packages/services/faker/ops/user" -[workspace.dependencies.cdn-ns-auth-type-set] -path = "packages/services/cdn/ops/ns-auth-type-set" +[workspace.dependencies.game-banner-upload-complete] +path = "packages/services/game/ops/banner-upload-complete" -[workspace.dependencies.cdn-site-create] -path = "packages/services/cdn/ops/site-create" +[workspace.dependencies.game-create] +path = "packages/services/game/ops/create" -[workspace.dependencies.cdn-version-get] -path = "packages/services/cdn/ops/version-get" +[workspace.dependencies.game-get] +path = "packages/services/game/ops/get" -[workspace.dependencies.cdn-namespace-auth-user-update] -path = "packages/services/cdn/ops/namespace-auth-user-update" +[workspace.dependencies.game-list-all] +path = "packages/services/game/ops/list-all" -[workspace.dependencies.cdn-version-prepare] -path = "packages/services/cdn/ops/version-prepare" +[workspace.dependencies.game-list-for-team] +path = "packages/services/game/ops/list-for-team" -[workspace.dependencies.cdn-namespace-domain-remove] -path = "packages/services/cdn/ops/namespace-domain-remove" +[workspace.dependencies.game-logo-upload-complete] +path = "packages/services/game/ops/logo-upload-complete" -[workspace.dependencies.cdn-namespace-create] -path = "packages/services/cdn/ops/namespace-create" +[workspace.dependencies.game-namespace-create] +path = "packages/services/game/ops/namespace-create" -[workspace.dependencies.cdn-worker] -path = "packages/services/cdn/worker" +[workspace.dependencies.game-namespace-get] +path = "packages/services/game/ops/namespace-get" -[workspace.dependencies.rivet-util-team] -path = "packages/services/team/util" +[workspace.dependencies.game-namespace-list] +path = "packages/services/game/ops/namespace-list" -[workspace.dependencies.util-team] -package = "rivet-util-team" -path = "packages/services/team/util" +[workspace.dependencies.game-namespace-resolve-name-id] +path = "packages/services/game/ops/namespace-resolve-name-id" -[workspace.dependencies.team-validate] -path = "packages/services/team/ops/validate" +[workspace.dependencies.game-namespace-resolve-url] +path = "packages/services/game/ops/namespace-resolve-url" -[workspace.dependencies.team-user-ban-list] -path = "packages/services/team/ops/user-ban-list" +[workspace.dependencies.game-namespace-validate] +path = "packages/services/game/ops/namespace-validate" -[workspace.dependencies.team-recommend] -path = "packages/services/team/ops/recommend" +[workspace.dependencies.game-namespace-version-history-list] +path = "packages/services/game/ops/namespace-version-history-list" -[workspace.dependencies.team-resolve-display-name] -path = "packages/services/team/ops/resolve-display-name" +[workspace.dependencies.game-namespace-version-set] +path = "packages/services/game/ops/namespace-version-set" -[workspace.dependencies.team-member-list] -path = "packages/services/team/ops/member-list" +[workspace.dependencies.game-recommend] +path = "packages/services/game/ops/recommend" -[workspace.dependencies.team-get] -path = "packages/services/team/ops/get" +[workspace.dependencies.game-resolve-name-id] +path = "packages/services/game/ops/resolve-name-id" -[workspace.dependencies.team-member-count] -path = "packages/services/team/ops/member-count" +[workspace.dependencies.game-resolve-namespace-id] +path = "packages/services/game/ops/resolve-namespace-id" -[workspace.dependencies.team-member-relationship-get] -path = "packages/services/team/ops/member-relationship-get" +[workspace.dependencies.game-token-development-validate] +path = "packages/services/game/ops/token-development-validate" -[workspace.dependencies.team-member-get] -path = "packages/services/team/ops/member-get" +[workspace.dependencies.game-validate] +path = "packages/services/game/ops/validate" -[workspace.dependencies.team-profile-validate] -path = "packages/services/team/ops/profile-validate" +[workspace.dependencies.game-version-create] +path = "packages/services/game/ops/version-create" -[workspace.dependencies.team-user-ban-get] -path = "packages/services/team/ops/user-ban-get" +[workspace.dependencies.game-version-get] +path = "packages/services/game/ops/version-get" -[workspace.dependencies.team-join-request-list] -path = "packages/services/team/ops/join-request-list" +[workspace.dependencies.game-version-list] +path = "packages/services/game/ops/version-list" -[workspace.dependencies.team-avatar-upload-complete] -path = "packages/services/team/ops/avatar-upload-complete" +[workspace.dependencies.game-version-validate] +path = "packages/services/game/ops/version-validate" -[workspace.dependencies.team-worker] -path = "packages/services/team/worker" +[workspace.dependencies.ip-info] +path = "packages/services/ip/ops/info" -[workspace.dependencies.ds-log-read] -path = "packages/services/ds-log/ops/read" +[workspace.dependencies.job-log-read] +path = "packages/services/job-log/ops/read" -[workspace.dependencies.ds-log-export] -path = "packages/services/ds-log/ops/export" +[workspace.dependencies.job-log-worker] +path = "packages/services/job-log/worker" [workspace.dependencies.job-run] path = "packages/services/job-run" -[workspace.dependencies.workflow-gc] -path = "packages/services/workflow/standalone/gc" +[workspace.dependencies.job-gc] +path = "packages/services/job/standalone/gc" -[workspace.dependencies.workflow-metrics-publish] -path = "packages/services/workflow/standalone/metrics-publish" +[workspace.dependencies.rivet-util-job] +path = "packages/services/job/util" -[workspace.dependencies.cloud-default-create] -path = "packages/services/cloud/standalone/default-create" +[workspace.dependencies.util-job] +package = "rivet-util-job" +path = "packages/services/job/util" -[workspace.dependencies.cloud-version-publish] -path = "packages/services/cloud/ops/version-publish" +[workspace.dependencies.linode] +path = "packages/services/linode" -[workspace.dependencies.cloud-game-config-get] -path = "packages/services/cloud/ops/game-config-get" +[workspace.dependencies.linode-gc] +path = "packages/services/linode/standalone/gc" -[workspace.dependencies.cloud-namespace-token-public-create] -path = "packages/services/cloud/ops/namespace-token-public-create" +[workspace.dependencies.load-test-api-cloud] +path = "packages/services/load-test/standalone/api-cloud" -[workspace.dependencies.cloud-game-config-create] -path = "packages/services/cloud/ops/game-config-create" +[workspace.dependencies.load-test-mm] +path = "packages/services/load-test/standalone/mm" -[workspace.dependencies.cloud-namespace-get] -path = "packages/services/cloud/ops/namespace-get" +[workspace.dependencies.load-test-mm-sustain] +path = "packages/services/load-test/standalone/mm-sustain" -[workspace.dependencies.cloud-version-get] -path = "packages/services/cloud/ops/version-get" +[workspace.dependencies.load-test-sqlx] +path = "packages/services/load-test/standalone/sqlx" -[workspace.dependencies.cloud-game-token-create] -path = "packages/services/cloud/ops/game-token-create" +[workspace.dependencies.load-test-watch-requests] +path = "packages/services/load-test/standalone/watch-requests" -[workspace.dependencies.cloud-device-link-create] -path = "packages/services/cloud/ops/device-link-create" +[workspace.dependencies.mm-config-game-get] +path = "packages/services/mm-config/ops/game-get" -[workspace.dependencies.cloud-namespace-create] -path = "packages/services/cloud/ops/namespace-create" +[workspace.dependencies.mm-config-game-upsert] +path = "packages/services/mm-config/ops/game-upsert" -[workspace.dependencies.cloud-namespace-token-development-create] -path = "packages/services/cloud/ops/namespace-token-development-create" +[workspace.dependencies.mm-config-lobby-group-get] +path = "packages/services/mm-config/ops/lobby-group-get" -[workspace.dependencies.cloud-worker] -path = "packages/services/cloud/worker" +[workspace.dependencies.mm-config-lobby-group-resolve-name-id] +path = "packages/services/mm-config/ops/lobby-group-resolve-name-id" -[workspace.dependencies.rivet-util-job] -path = "packages/services/job/util" +[workspace.dependencies.mm-config-lobby-group-resolve-version] +path = "packages/services/mm-config/ops/lobby-group-resolve-version" -[workspace.dependencies.util-job] -package = "rivet-util-job" -path = "packages/services/job/util" +[workspace.dependencies.mm-config-namespace-config-set] +path = "packages/services/mm-config/ops/namespace-config-set" -[workspace.dependencies.job-gc] -path = "packages/services/job/standalone/gc" +[workspace.dependencies.mm-config-namespace-config-validate] +path = "packages/services/mm-config/ops/namespace-config-validate" -[workspace.dependencies.rivet-util-mm] -path = "packages/services/mm/util" +[workspace.dependencies.mm-config-namespace-create] +path = "packages/services/mm-config/ops/namespace-create" -[workspace.dependencies.util-mm] -package = "rivet-util-mm" -path = "packages/services/mm/util" +[workspace.dependencies.mm-config-namespace-get] +path = "packages/services/mm-config/ops/namespace-get" -[workspace.dependencies.mm-gc] -path = "packages/services/mm/standalone/gc" +[workspace.dependencies.mm-config-version-get] +path = "packages/services/mm-config/ops/version-get" -[workspace.dependencies.mm-player-get] -path = "packages/services/mm/ops/player-get" +[workspace.dependencies.mm-config-version-prepare] +path = "packages/services/mm-config/ops/version-prepare" + +[workspace.dependencies.mm-config-version-publish] +path = "packages/services/mm-config/ops/version-publish" [workspace.dependencies.mm-dev-player-token-create] path = "packages/services/mm/ops/dev-player-token-create" -[workspace.dependencies.mm-lobby-idle-update] -path = "packages/services/mm/ops/lobby-idle-update" +[workspace.dependencies.mm-lobby-find-fail] +path = "packages/services/mm/ops/lobby-find-fail" [workspace.dependencies.mm-lobby-find-lobby-query-list] path = "packages/services/mm/ops/lobby-find-lobby-query-list" -[workspace.dependencies.mm-lobby-find-fail] -path = "packages/services/mm/ops/lobby-find-fail" +[workspace.dependencies.mm-lobby-find-try-complete] +path = "packages/services/mm/ops/lobby-find-try-complete" -[workspace.dependencies.mm-lobby-list-for-user-id] -path = "packages/services/mm/ops/lobby-list-for-user-id" +[workspace.dependencies.mm-lobby-for-run-id] +path = "packages/services/mm/ops/lobby-for-run-id" + +[workspace.dependencies.mm-lobby-get] +path = "packages/services/mm/ops/lobby-get" [workspace.dependencies.mm-lobby-history] path = "packages/services/mm/ops/lobby-history" +[workspace.dependencies.mm-lobby-idle-update] +path = "packages/services/mm/ops/lobby-idle-update" + [workspace.dependencies.mm-lobby-list-for-namespace] path = "packages/services/mm/ops/lobby-list-for-namespace" -[workspace.dependencies.mm-lobby-state-get] -path = "packages/services/mm/ops/lobby-state-get" +[workspace.dependencies.mm-lobby-list-for-user-id] +path = "packages/services/mm/ops/lobby-list-for-user-id" [workspace.dependencies.mm-lobby-player-count] path = "packages/services/mm/ops/lobby-player-count" -[workspace.dependencies.mm-lobby-find-try-complete] -path = "packages/services/mm/ops/lobby-find-try-complete" +[workspace.dependencies.mm-lobby-runtime-aggregate] +path = "packages/services/mm/ops/lobby-runtime-aggregate" -[workspace.dependencies.mm-lobby-for-run-id] -path = "packages/services/mm/ops/lobby-for-run-id" +[workspace.dependencies.mm-lobby-state-get] +path = "packages/services/mm/ops/lobby-state-get" [workspace.dependencies.mm-player-count-for-namespace] path = "packages/services/mm/ops/player-count-for-namespace" -[workspace.dependencies.mm-lobby-runtime-aggregate] -path = "packages/services/mm/ops/lobby-runtime-aggregate" +[workspace.dependencies.mm-player-get] +path = "packages/services/mm/ops/player-get" -[workspace.dependencies.mm-lobby-get] -path = "packages/services/mm/ops/lobby-get" +[workspace.dependencies.mm-gc] +path = "packages/services/mm/standalone/gc" + +[workspace.dependencies.rivet-util-mm] +path = "packages/services/mm/util" + +[workspace.dependencies.util-mm] +package = "rivet-util-mm" +path = "packages/services/mm/util" [workspace.dependencies.mm-worker] path = "packages/services/mm/worker" -[workspace.dependencies.telemetry-beacon] -path = "packages/services/telemetry/standalone/beacon" +[workspace.dependencies.monolith-worker] +path = "packages/services/monolith/standalone/worker" -[workspace.dependencies.external-request-validate] -path = "packages/services/external/ops/request-validate" +[workspace.dependencies.monolith-workflow-worker] +path = "packages/services/monolith/standalone/workflow-worker" -[workspace.dependencies.external-worker] -path = "packages/services/external/worker" +[workspace.dependencies.nomad-monitor] +path = "packages/services/nomad/standalone/monitor" -[workspace.dependencies.build] -path = "packages/services/build" +[workspace.dependencies.pegboard] +path = "packages/services/pegboard" -[workspace.dependencies.rivet-util-build] -path = "packages/services/build/util" +[workspace.dependencies.pegboard-dc-init] +path = "packages/services/pegboard/standalone/dc-init" -[workspace.dependencies.util-build] -package = "rivet-util-build" -path = "packages/services/build/util" +[workspace.dependencies.pegboard-gc] +path = "packages/services/pegboard/standalone/gc" -[workspace.dependencies.build-default-create] -path = "packages/services/build/standalone/default-create" +[workspace.dependencies.pegboard-ws] +path = "packages/services/pegboard/standalone/ws" -[workspace.dependencies.build-get] -path = "packages/services/build/ops/get" +[workspace.dependencies.region-get] +path = "packages/services/region/ops/get" -[workspace.dependencies.build-list-for-game] -path = "packages/services/build/ops/list-for-game" +[workspace.dependencies.region-list] +path = "packages/services/region/ops/list" -[workspace.dependencies.build-list-for-env] -path = "packages/services/build/ops/list-for-env" +[workspace.dependencies.region-list-for-game] +path = "packages/services/region/ops/list-for-game" -[workspace.dependencies.build-create] -path = "packages/services/build/ops/create" +[workspace.dependencies.region-recommend] +path = "packages/services/region/ops/recommend" + +[workspace.dependencies.region-resolve] +path = "packages/services/region/ops/resolve" + +[workspace.dependencies.region-resolve-for-game] +path = "packages/services/region/ops/resolve-for-game" [workspace.dependencies.server-spec] path = "packages/services/server-spec" -[workspace.dependencies.cf-custom-hostname-get] -path = "packages/services/cf-custom-hostname/ops/get" +[workspace.dependencies.team-invite-get] +path = "packages/services/team-invite/ops/get" -[workspace.dependencies.cf-custom-hostname-resolve-hostname] -path = "packages/services/cf-custom-hostname/ops/resolve-hostname" +[workspace.dependencies.team-invite-worker] +path = "packages/services/team-invite/worker" -[workspace.dependencies.cf-custom-hostname-list-for-namespace-id] -path = "packages/services/cf-custom-hostname/ops/list-for-namespace-id" +[workspace.dependencies.team-avatar-upload-complete] +path = "packages/services/team/ops/avatar-upload-complete" -[workspace.dependencies.cf-custom-hostname-worker] -path = "packages/services/cf-custom-hostname/worker" +[workspace.dependencies.team-get] +path = "packages/services/team/ops/get" -[workspace.dependencies.ip-info] -path = "packages/services/ip/ops/info" +[workspace.dependencies.team-join-request-list] +path = "packages/services/team/ops/join-request-list" -[workspace.dependencies.dynamic-config] -path = "packages/services/dynamic-config" +[workspace.dependencies.team-member-count] +path = "packages/services/team/ops/member-count" -[workspace.dependencies.email-send] -path = "packages/services/email/ops/send" +[workspace.dependencies.team-member-get] +path = "packages/services/team/ops/member-get" -[workspace.dependencies.job-log-read] -path = "packages/services/job-log/ops/read" +[workspace.dependencies.team-member-list] +path = "packages/services/team/ops/member-list" -[workspace.dependencies.job-log-worker] -path = "packages/services/job-log/worker" +[workspace.dependencies.team-member-relationship-get] +path = "packages/services/team/ops/member-relationship-get" -[workspace.dependencies.upload-prepare] -path = "packages/services/upload/ops/prepare" +[workspace.dependencies.team-profile-validate] +path = "packages/services/team/ops/profile-validate" + +[workspace.dependencies.team-recommend] +path = "packages/services/team/ops/recommend" + +[workspace.dependencies.team-resolve-display-name] +path = "packages/services/team/ops/resolve-display-name" + +[workspace.dependencies.team-user-ban-get] +path = "packages/services/team/ops/user-ban-get" + +[workspace.dependencies.team-user-ban-list] +path = "packages/services/team/ops/user-ban-list" + +[workspace.dependencies.team-validate] +path = "packages/services/team/ops/validate" + +[workspace.dependencies.rivet-util-team] +path = "packages/services/team/util" + +[workspace.dependencies.util-team] +package = "rivet-util-team" +path = "packages/services/team/util" + +[workspace.dependencies.team-worker] +path = "packages/services/team/worker" + +[workspace.dependencies.telemetry-beacon] +path = "packages/services/telemetry/standalone/beacon" + +[workspace.dependencies.tier] +path = "packages/services/tier" + +[workspace.dependencies.token-create] +path = "packages/services/token/ops/create" + +[workspace.dependencies.token-exchange] +path = "packages/services/token/ops/exchange" + +[workspace.dependencies.token-get] +path = "packages/services/token/ops/get" + +[workspace.dependencies.token-revoke] +path = "packages/services/token/ops/revoke" [workspace.dependencies.upload-complete] path = "packages/services/upload/ops/complete" -[workspace.dependencies.upload-get] -path = "packages/services/upload/ops/get" - [workspace.dependencies.upload-file-list] path = "packages/services/upload/ops/file-list" +[workspace.dependencies.upload-get] +path = "packages/services/upload/ops/get" + [workspace.dependencies.upload-list-for-user] path = "packages/services/upload/ops/list-for-user" +[workspace.dependencies.upload-prepare] +path = "packages/services/upload/ops/prepare" + [workspace.dependencies.upload-worker] path = "packages/services/upload/worker" -[workspace.dependencies.custom-user-avatar-list-for-game] -path = "packages/services/custom-user-avatar/ops/list-for-game" +[workspace.dependencies.user] +path = "packages/services/user" -[workspace.dependencies.custom-user-avatar-upload-complete] -path = "packages/services/custom-user-avatar/ops/upload-complete" +[workspace.dependencies.user-identity-create] +path = "packages/services/user-identity/ops/create" -[workspace.dependencies.debug-email-res] -path = "packages/services/debug/ops/email-res" +[workspace.dependencies.user-identity-delete] +path = "packages/services/user-identity/ops/delete" -[workspace.dependencies.tier] -path = "packages/services/tier" +[workspace.dependencies.user-identity-get] +path = "packages/services/user-identity/ops/get" + +[workspace.dependencies.user-avatar-upload-complete] +path = "packages/services/user/ops/avatar-upload-complete" + +[workspace.dependencies.user-get] +path = "packages/services/user/ops/get" + +[workspace.dependencies.user-pending-delete-toggle] +path = "packages/services/user/ops/pending-delete-toggle" + +[workspace.dependencies.user-profile-validate] +path = "packages/services/user/ops/profile-validate" + +[workspace.dependencies.user-resolve-email] +path = "packages/services/user/ops/resolve-email" + +[workspace.dependencies.user-team-list] +path = "packages/services/user/ops/team-list" + +[workspace.dependencies.user-token-create] +path = "packages/services/user/ops/token-create" + +[workspace.dependencies.user-delete-pending] +path = "packages/services/user/standalone/delete-pending" + +[workspace.dependencies.user-worker] +path = "packages/services/user/worker" + +[workspace.dependencies.workflow-gc] +path = "packages/services/workflow/standalone/gc" + +[workspace.dependencies.workflow-metrics-publish] +path = "packages/services/workflow/standalone/metrics-publish" + +[workspace.dependencies.rivet-cli] +path = "packages/toolchain/cli" + +[workspace.dependencies.rivet-js-utils-embed] +path = "packages/toolchain/js-utils-embed" + +[workspace.dependencies.rivet-toolchain] +path = "packages/toolchain/toolchain" [workspace.dependencies.rivet-api] path = "sdks/api/full/rust" diff --git a/examples/actor-layer/index.ts b/examples/actor-layer/index.ts new file mode 100644 index 0000000000..62f44a43d7 --- /dev/null +++ b/examples/actor-layer/index.ts @@ -0,0 +1,44 @@ +// Without Rivet actor class + +const portStr = Deno.env.get("PORT_http") ?? Deno.env.get("HTTP_PORT"); +if (!portStr) throw "Missing port"; +const port = parseInt(portStr); +if (!isFinite(port)) throw "Invalid port"; + +const server = Deno.serve({ + handler, + port, + hostname: "0.0.0.0", +}); + +await server.finished; + +async function handler(req: Request) { + console.log("Received request"); + + let newCount = (await Rivet.kv.get("count") ?? 0) + 1; + await Rivet.kv.put("count", newCount); + + return new Response(newCount.toString()); +} + +// With Rivet actor class + +import { Actor } from "@rivet-gg/rivet"; + +interface State { + count: number; +} + +class Counter extends Actor { + initialize(): State { + return { count: 0 }; + } + + async onRequest(req) { + this.count += 1; + return new Response(this.count.toString()); + } +} + +Rivet.run(Counter); diff --git a/examples/basic/clean_run.sh b/examples/basic/clean_run.sh new file mode 100644 index 0000000000..144bdb8cba --- /dev/null +++ b/examples/basic/clean_run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +(cd ../.. && docker build -f cli.Dockerfile -t opengb .) && docker run --privileged -it --add-host=host.docker.internal:host-gateway -e DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres?sslmode=disable -e VERBOSE=1 -v ./:/backend -w /backend opengb dev +# (cd ../.. && deno task cli:install) && opengb clean && opengb dev + diff --git a/examples/basic/fetch_count.sh b/examples/basic/fetch_count.sh new file mode 100755 index 0000000000..86979c31c8 --- /dev/null +++ b/examples/basic/fetch_count.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# curl -X POST -d '{"key":"test"}' 'http://localhost:6420/modules/actors_test/scripts/fetch_counter/call' +curl -X POST -d '{"key":"test"}' 'https://test-game-cir.backend.rivet.gg/modules/actors_test/scripts/fetch_counter/call' | jq + diff --git a/examples/basic/lookup_sourcemap.sh b/examples/basic/lookup_sourcemap.sh new file mode 100644 index 0000000000..ba1fa87b7a --- /dev/null +++ b/examples/basic/lookup_sourcemap.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +npx source-map-cli resolve .rivet/backend/output.js $1 $2 + diff --git a/examples/basic/modules/actors_test/actors/counter.ts b/examples/basic/modules/actors_test/actors/counter.ts new file mode 100644 index 0000000000..37db6074ac --- /dev/null +++ b/examples/basic/modules/actors_test/actors/counter.ts @@ -0,0 +1,38 @@ +import { ActorBase, ActorContext, Empty } from "../module.gen.ts"; + +interface Input { +} + +interface State { + count: number; + lastTick: number; +} + +export interface FetchResponse { + count: number; + lastTick: number; + schedule: any; +} + +export const TICK_INTERVAL = 200; + +export class Actor extends ActorBase { + public initialize(_input: Input): State { + this.schedule.after(TICK_INTERVAL, "tick", undefined); + return { count: 0, lastTick: 0 }; + } + + async rpcFetchCount(_ctx: ActorContext, _req: Empty): Promise { + return { + count: this.state.count, + lastTick: this.state.lastTick, + schedule: await this.schedule.__inspect(), + }; + } + + private tick(ctx: ActorContext) { + this.schedule.after(TICK_INTERVAL, "tick", undefined); + this.state.count += 1; + this.state.lastTick = Date.now(); + } +} diff --git a/examples/basic/modules/actors_test/module.json b/examples/basic/modules/actors_test/module.json new file mode 100644 index 0000000000..3e3acd45d2 --- /dev/null +++ b/examples/basic/modules/actors_test/module.json @@ -0,0 +1,11 @@ +{ + "scripts": { + "fetch_counter": { + "public": true + } + }, + "errors": {}, + "actors": { + "counter": {} + } +} diff --git a/examples/basic/modules/actors_test/scripts/fetch_counter.ts b/examples/basic/modules/actors_test/scripts/fetch_counter.ts new file mode 100644 index 0000000000..87a29ee036 --- /dev/null +++ b/examples/basic/modules/actors_test/scripts/fetch_counter.ts @@ -0,0 +1,18 @@ +import { ScriptContext, Empty } from "../module.gen.ts"; +import { FetchResponse } from "../actors/counter.ts"; + +export interface Request { + key: string; +} + +export type Response = FetchResponse; + +export async function run(ctx: ScriptContext, req: Request): Promise { + return await ctx.actors.counter + .getOrCreateAndCall( + req.key, + undefined, + "rpcFetchCount", + {}, + ); +} diff --git a/examples/basic/modules/actors_test/tests/counter.ts b/examples/basic/modules/actors_test/tests/counter.ts new file mode 100644 index 0000000000..fe54547c63 --- /dev/null +++ b/examples/basic/modules/actors_test/tests/counter.ts @@ -0,0 +1,16 @@ +import { test, TestContext } from "../module.gen.ts"; +import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { delay } from "jsr:@std/async"; +import { TICK_INTERVAL } from "../actors/counter.ts"; + +test("counter", async (ctx: TestContext) => { + const key = `${Math.floor(Math.random() * 100000)}`; + + for (let i = 0; i < 5; i++) { + const res = await ctx.modules.actorsTest.fetchCounter({ key }); + assertEquals(res.count, i); + + await delay(TICK_INTERVAL); + } +}); + diff --git a/examples/basic/modules/config_test/config.ts b/examples/basic/modules/config_test/config.ts new file mode 100644 index 0000000000..a1047c55eb --- /dev/null +++ b/examples/basic/modules/config_test/config.ts @@ -0,0 +1,4 @@ +export interface Config { + foo: string; + bar: number; +} diff --git a/examples/basic/modules/config_test/module.json b/examples/basic/modules/config_test/module.json new file mode 100644 index 0000000000..dd56b418f0 --- /dev/null +++ b/examples/basic/modules/config_test/module.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "read_config": {} + }, + "errors": {} +} \ No newline at end of file diff --git a/examples/basic/modules/config_test/scripts/read_config.ts b/examples/basic/modules/config_test/scripts/read_config.ts new file mode 100644 index 0000000000..473b757d19 --- /dev/null +++ b/examples/basic/modules/config_test/scripts/read_config.ts @@ -0,0 +1,20 @@ +import { RuntimeError, ScriptContext } from "../module.gen.ts"; + +export interface Request { +} + +export interface Response { + config: { + foo: string; + bar: number; + }; +} + +export async function run( + ctx: ScriptContext, + req: Request, +): Promise { + return { + config: ctx.config, + }; +} diff --git a/examples/basic/modules/config_test/tests/read_config.ts b/examples/basic/modules/config_test/tests/read_config.ts new file mode 100644 index 0000000000..8144c1b7e1 --- /dev/null +++ b/examples/basic/modules/config_test/tests/read_config.ts @@ -0,0 +1,8 @@ +import { test, TestContext } from "../module.gen.ts"; +import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; + +test("e2e", async (ctx: TestContext) => { + const res = await ctx.call("config_test", "read_config", {}) as any; + assertEquals(res.config.foo, "hello world"); + assertEquals(res.config.bar, 1234); +}); diff --git a/examples/basic/modules/deno.jsonc b/examples/basic/modules/deno.jsonc new file mode 100644 index 0000000000..58bd1a3ca3 --- /dev/null +++ b/examples/basic/modules/deno.jsonc @@ -0,0 +1,21 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +{ + "lint": { + "rules": { + "exclude": [ + "no-empty-interface", + "no-explicit-any", + "require-await" + ] + } + }, + "fmt": { + "useTabs": true + }, + "imports": { + "cloudflare:workers": "npm:@cloudflare/workers-types" + } +} \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/1724448780_minor_winter_soldier.sql b/examples/basic/modules/foo/db/migrations/1724448780_minor_winter_soldier.sql new file mode 100644 index 0000000000..2a267e8e65 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/1724448780_minor_winter_soldier.sql @@ -0,0 +1,13 @@ +CREATE SCHEMA "module_foo"; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "module_foo"."db_entry" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "test2" text, + "test3" text, + "test5" boolean +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "module_foo"."table6" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "hello2" text +); diff --git a/examples/basic/modules/foo/db/migrations/1724448812_fine_rage.sql b/examples/basic/modules/foo/db/migrations/1724448812_fine_rage.sql new file mode 100644 index 0000000000..c85eed5005 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/1724448812_fine_rage.sql @@ -0,0 +1 @@ +ALTER TABLE "module_foo"."table6" RENAME TO "table7"; \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/1724448902_sleepy_purple_man.sql b/examples/basic/modules/foo/db/migrations/1724448902_sleepy_purple_man.sql new file mode 100644 index 0000000000..900fed2504 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/1724448902_sleepy_purple_man.sql @@ -0,0 +1 @@ +ALTER TABLE "module_foo"."table7" RENAME TO "table8"; \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/meta/1724448780_snapshot.json b/examples/basic/modules/foo/db/migrations/meta/1724448780_snapshot.json new file mode 100644 index 0000000000..cfa701d762 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/meta/1724448780_snapshot.json @@ -0,0 +1,76 @@ +{ + "id": "23839314-5d54-47b8-a9a0-a1048bd2676d", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "module_foo.db_entry": { + "name": "db_entry", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "test2": { + "name": "test2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test3": { + "name": "test3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test5": { + "name": "test5", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "module_foo.table6": { + "name": "table6", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hello2": { + "name": "hello2", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": { + "module_foo": "module_foo" + }, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/meta/1724448812_snapshot.json b/examples/basic/modules/foo/db/migrations/meta/1724448812_snapshot.json new file mode 100644 index 0000000000..e316cbddfb --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/meta/1724448812_snapshot.json @@ -0,0 +1,76 @@ +{ + "id": "9ed1e095-c1ca-4c43-afdc-37f39cd2c9fc", + "prevId": "23839314-5d54-47b8-a9a0-a1048bd2676d", + "version": "7", + "dialect": "postgresql", + "tables": { + "module_foo.db_entry": { + "name": "db_entry", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "test2": { + "name": "test2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test3": { + "name": "test3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test5": { + "name": "test5", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "module_foo.table7": { + "name": "table7", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hello2": { + "name": "hello2", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": { + "module_foo": "module_foo" + }, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/meta/1724448902_snapshot.json b/examples/basic/modules/foo/db/migrations/meta/1724448902_snapshot.json new file mode 100644 index 0000000000..78d70353c5 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/meta/1724448902_snapshot.json @@ -0,0 +1,76 @@ +{ + "id": "83e9489e-bfb3-4738-a238-4a76102b1e7c", + "prevId": "9ed1e095-c1ca-4c43-afdc-37f39cd2c9fc", + "version": "7", + "dialect": "postgresql", + "tables": { + "module_foo.db_entry": { + "name": "db_entry", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "test2": { + "name": "test2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test3": { + "name": "test3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test5": { + "name": "test5", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "module_foo.table8": { + "name": "table8", + "schema": "module_foo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hello2": { + "name": "hello2", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": { + "module_foo": "module_foo" + }, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/examples/basic/modules/foo/db/migrations/meta/_journal.json b/examples/basic/modules/foo/db/migrations/meta/_journal.json new file mode 100644 index 0000000000..1c75105620 --- /dev/null +++ b/examples/basic/modules/foo/db/migrations/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1724448780247, + "tag": "1724448780_minor_winter_soldier", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1724448812897, + "tag": "1724448812_fine_rage", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1724448902426, + "tag": "1724448902_sleepy_purple_man", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/examples/basic/modules/foo/db/schema.ts b/examples/basic/modules/foo/db/schema.ts new file mode 100644 index 0000000000..5cd0ed61dc --- /dev/null +++ b/examples/basic/modules/foo/db/schema.ts @@ -0,0 +1,14 @@ +import { schema, Query } from "./schema.gen.ts"; + +export const dbEntry = schema.table("db_entry", { + id: Query.uuid("id").primaryKey().defaultRandom(), + test2: Query.text("test2"), + test3: Query.text("test3"), + test4: Query.boolean("test5"), +}); + +export const table2 = schema.table("table8", { + id: Query.uuid("id").primaryKey().defaultRandom(), + hello: Query.text("hello2"), +}); + diff --git a/examples/basic/modules/foo/module.json b/examples/basic/modules/foo/module.json new file mode 100644 index 0000000000..b79ea82adc --- /dev/null +++ b/examples/basic/modules/foo/module.json @@ -0,0 +1,23 @@ +{ + "status": "preview", + "authors": [ + "NathanFlurry" + ], + "scripts": { + "ping": { + "public": true + }, + "create_entry": { + "public": true + }, + "call_self": { + "public": true + }, + "actor": { + "public": true + } + }, + "errors": { + "db_error": {} + } +} diff --git a/examples/basic/modules/foo/scripts/actor.ts b/examples/basic/modules/foo/scripts/actor.ts new file mode 100644 index 0000000000..94c6520b72 --- /dev/null +++ b/examples/basic/modules/foo/scripts/actor.ts @@ -0,0 +1,22 @@ +import { ScriptContext } from "../module.gen.ts"; + +export interface Request { + id?: string; +} + +export interface Response { + pongs: number; +} + +export async function run( + ctx: ScriptContext, + req: Request, +): Promise { + let id = req.id ?? "me"; + + return { pongs: 1 }; + + // let pongs = await ctx.actors.ponger.getOrCreateAndCall<{}, number, number>(id, {}, "addPong", 5); + // + // return { pongs }; +} diff --git a/examples/basic/modules/foo/scripts/call_self.ts b/examples/basic/modules/foo/scripts/call_self.ts new file mode 100644 index 0000000000..05d37e3c74 --- /dev/null +++ b/examples/basic/modules/foo/scripts/call_self.ts @@ -0,0 +1,18 @@ +import { ScriptContext } from "../module.gen.ts"; + +export interface Request extends Record {} + +export interface Response { + response: { + pong: string; + }; +} + +export async function run( + ctx: ScriptContext, + _req: Request, +): Promise { + return { + response: await ctx.modules.foo.ping({}), + }; +} diff --git a/examples/basic/modules/foo/scripts/create_entry.ts b/examples/basic/modules/foo/scripts/create_entry.ts new file mode 100644 index 0000000000..ed395a4985 --- /dev/null +++ b/examples/basic/modules/foo/scripts/create_entry.ts @@ -0,0 +1,29 @@ +import { Query, Database, ScriptContext } from "../module.gen.ts"; + +export interface Request extends Record {} + +export interface Response { + id: string; +} + +export type IdentityType = { guest: IdentityTypeGuest }; + +export interface IdentityTypeGuest { +} + +export async function run( + ctx: ScriptContext, + _req: Request, +): Promise { + // Create entry + await ctx.db.insert(Database.dbEntry).values({ test2: "abc" }); + + // Get entry + const entry = await ctx.db.query.dbEntry.findFirst({ + where: Query.eq(Database.dbEntry.test2, "abc"), + }); + + return { + id: entry!.id, + }; +} diff --git a/examples/basic/modules/foo/scripts/ping.ts b/examples/basic/modules/foo/scripts/ping.ts new file mode 100644 index 0000000000..03a3ff3525 --- /dev/null +++ b/examples/basic/modules/foo/scripts/ping.ts @@ -0,0 +1,14 @@ +import { ScriptContext } from "../module.gen.ts"; + +export interface Request extends Record {} + +export interface Response { + pong: string; +} + +export async function run( + _ctx: ScriptContext, + _req: Request, +): Promise { + return { pong: "pong" }; +} diff --git a/examples/basic/modules/foo/tests/e2e.ts b/examples/basic/modules/foo/tests/e2e.ts new file mode 100644 index 0000000000..bb894d5374 --- /dev/null +++ b/examples/basic/modules/foo/tests/e2e.ts @@ -0,0 +1,17 @@ +import { test, TestContext } from "../module.gen.ts"; +import { assertEquals, assertExists } from "https://deno.land/std@0.224.0/assert/mod.ts"; + +test("ping-pong", async (ctx: TestContext) => { + const { pong } = await ctx.modules.foo.ping({}); + assertEquals("pong", pong); +}); + +test("call-self", async (ctx: TestContext) => { + const { response } = await ctx.modules.foo.callSelf({}); + assertEquals("pong", response.pong); +}); + +test("create-entry", async (ctx: TestContext) => { + const { id } = await ctx.modules.foo.createEntry({}); + assertExists(id); +}); diff --git a/examples/basic/modules/foo/types/common.ts b/examples/basic/modules/foo/types/common.ts new file mode 100644 index 0000000000..6028496ef3 --- /dev/null +++ b/examples/basic/modules/foo/types/common.ts @@ -0,0 +1,3 @@ +export interface Entry { + id: string; +} diff --git a/examples/basic/ping.sh b/examples/basic/ping.sh new file mode 100644 index 0000000000..6ea05c29da --- /dev/null +++ b/examples/basic/ping.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +curl -X POST -d '{}' 'http://localhost:6420/modules/foo/scripts/ping/call' + diff --git a/examples/basic/rivet.json b/examples/basic/rivet.json new file mode 100644 index 0000000000..54efaccee9 --- /dev/null +++ b/examples/basic/rivet.json @@ -0,0 +1,37 @@ +{ + "registries": { + "local": { + "local": { + "directory": "./modules" + } + } + }, + "modules": { + "users": {}, + "rate_limit": {}, + "tokens": {}, + "foo": { + "registry": "local" + }, + "config_test": { + "registry": "local", + "config": { + "foo": "hello world", + "bar": 1234 + } + }, + "friends": {}, + "rivet": { + "config": { + "apiEndpoint": "https://api.rivet.gg", + "apiEndpointVariable": "RIVET_API_ENDPOINT", + "serviceTokenVariable": "RIVET_SERVICE_TOKEN", + "gameIdVariable": "RIVET_GAME_ID", + "environmentIdVariable": "RIVET_ENVIRONMENT_ID" + } + }, + "actors_test": { + "registry": "local" + } + } +} \ No newline at end of file diff --git a/examples/docker/Dockerfile b/examples/docker/Dockerfile new file mode 100644 index 0000000000..70b2dbcca0 --- /dev/null +++ b/examples/docker/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine:latest +RUN apk add --no-cache shadow +RUN useradd -m -s /bin/sh rivet +USER rivet +CMD while true; do echo "Hello, World!"; sleep 10; done diff --git a/examples/docker/rivet.jsonc b/examples/docker/rivet.jsonc new file mode 100644 index 0000000000..760424f4da --- /dev/null +++ b/examples/docker/rivet.jsonc @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "builds": [ + { + "tags": { "name": "hello-world" }, + "runtime": "docker", + "dockerfile": "Dockerfile" + } + ] +} + diff --git a/examples/html5-node/.dockerignore b/examples/html5-node/.dockerignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/examples/html5-node/.dockerignore @@ -0,0 +1 @@ +node_modules/ diff --git a/examples/html5-node/.gitignore b/examples/html5-node/.gitignore new file mode 100644 index 0000000000..cf7098890e --- /dev/null +++ b/examples/html5-node/.gitignore @@ -0,0 +1 @@ +**/node_modules diff --git a/examples/html5-node/README.md b/examples/html5-node/README.md new file mode 100644 index 0000000000..34fe01e592 --- /dev/null +++ b/examples/html5-node/README.md @@ -0,0 +1,37 @@ +# Sandbox + +## Quickstart Tutorial + +View the documentation [here](https://rivet.gg/docs/html5/tutorials/quickstart). + +## Prerequisites + +- Rivet CLI +- NodeJS +- Open ports: 8080 (client), 7777 (game server), 6420 (Rivet) + +## Instructions + +### Develop + +``` +npm install +npm run dev +``` + +Open [http://127.0.0.1:8080](http://127.0.0.1:8080). + +### Deploy + +``` +rivet login +rivet deploy prod +``` + +#### Connecting + +In the client served by `npm run dev`: + +1. Run `rivet backend get-endpoint prod` and copy this value to the _Endpoint_ field. +2. Run `rivet backend get-current-version prod` and copy this value to the _Game Version_ field. + diff --git a/examples/html5-node/game_server.Dockerfile b/examples/html5-node/game_server.Dockerfile new file mode 100644 index 0000000000..e02b7889e0 --- /dev/null +++ b/examples/html5-node/game_server.Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-alpine +RUN adduser -D server +WORKDIR /app + +COPY package.json package.json +COPY packages/rivet-sdk ./packages/rivet-sdk +COPY packages/game-server ./packages/game-server +WORKDIR /app/packages/game-server +RUN npm install --frozen-lockfile && npm run build + +RUN chown -R server:server /app +USER server +EXPOSE 7777 + +CMD ["node", "dist/index.js"] diff --git a/examples/html5-node/package-lock.json b/examples/html5-node/package-lock.json new file mode 100644 index 0000000000..6c1c347d25 --- /dev/null +++ b/examples/html5-node/package-lock.json @@ -0,0 +1,4350 @@ +{ + "name": "rivet-example-html5", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rivet-example-html5", + "version": "1.0.0", + "license": "Apache-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "wait-on": "^8.0.1" + }, + "devDependencies": { + "concurrently": "^6.2.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "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==", + "dev": true, + "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-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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==", + "dev": true, + "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/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", + "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", + "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", + "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", + "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", + "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", + "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", + "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", + "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", + "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", + "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", + "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", + "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", + "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", + "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", + "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", + "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-require": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.0.0.tgz", + "integrity": "sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", + "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "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==", + "dev": true, + "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/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "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/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rivet-example-html5-client": { + "resolved": "packages/client", + "link": true + }, + "node_modules/rivet-example-html5-game-server": { + "resolved": "packages/game-server", + "link": true + }, + "node_modules/rivet-sdk": { + "resolved": "packages/rivet-sdk", + "link": true + }, + "node_modules/rollup": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", + "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.23.0", + "@rollup/rollup-android-arm64": "4.23.0", + "@rollup/rollup-darwin-arm64": "4.23.0", + "@rollup/rollup-darwin-x64": "4.23.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", + "@rollup/rollup-linux-arm-musleabihf": "4.23.0", + "@rollup/rollup-linux-arm64-gnu": "4.23.0", + "@rollup/rollup-linux-arm64-musl": "4.23.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", + "@rollup/rollup-linux-riscv64-gnu": "4.23.0", + "@rollup/rollup-linux-s390x-gnu": "4.23.0", + "@rollup/rollup-linux-x64-gnu": "4.23.0", + "@rollup/rollup-linux-x64-musl": "4.23.0", + "@rollup/rollup-win32-arm64-msvc": "4.23.0", + "@rollup/rollup-win32-ia32-msvc": "4.23.0", + "@rollup/rollup-win32-x64-msvc": "4.23.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/source-map/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/source-map/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz", + "integrity": "sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.0.0", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "consola": "^3.2.3", + "debug": "^4.3.5", + "esbuild": "^0.23.0", + "execa": "^5.1.1", + "joycon": "^3.1.1", + "picocolors": "^1.0.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.19.0", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyglobby": "^0.2.1", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/wait-on": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz", + "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wait-on/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/wait-on/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "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", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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": { + "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==", + "dev": true, + "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/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/client": { + "name": "rivet-example-html5-client", + "version": "1.0.0", + "devDependencies": { + "rivet-sdk": "*", + "vite": "^4.3.9" + } + }, + "packages/game_server": { + "name": "rivet-example-html5-game-server", + "extraneous": true, + "dependencies": { + "express": "^4.18.2", + "node-fetch": "2", + "rivet-sdk": "*", + "ws": "^7.5.4" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.14.10", + "@types/node-fetch": "^2.6.11", + "@types/ws": "^8.5.12", + "nodemon": "^2.0.20", + "ts-node": "^10.9.2", + "typescript": "^5.5.3" + } + }, + "packages/game-server": { + "name": "rivet-example-html5-game-server", + "dependencies": { + "express": "^4.18.2", + "node-fetch": "2", + "rivet-sdk": "*", + "ws": "^7.5.4" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.14.10", + "@types/node-fetch": "^2.6.11", + "@types/ws": "^8.5.12", + "nodemon": "^2.0.20", + "ts-node": "^10.9.2", + "typescript": "^5.5.3" + } + }, + "packages/rivet_sdk": { + "name": "rivet-sdk", + "version": "1.0.0", + "extraneous": true, + "devDependencies": { + "tsup": "^8.3.0", + "typescript": "^4.0 || ^5.0" + } + }, + "packages/rivet-sdk": { + "version": "1.0.0", + "devDependencies": { + "tsup": "^8.3.0", + "typescript": "^4.0 || ^5.0" + } + } + } +} diff --git a/examples/html5-node/package.json b/examples/html5-node/package.json new file mode 100644 index 0000000000..a2b51b3006 --- /dev/null +++ b/examples/html5-node/package.json @@ -0,0 +1,21 @@ +{ + "name": "rivet-example-html5", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "scripts": { + "rivet:dev": "rivet dev --project rivet.dev.json", + "client:dev": "cd packages/client && npm run dev", + "game-server:dev": "npx wait-on tcp:6420 && cd packages/game-server && npm run dev", + "dev": "concurrently \"npm:rivet:dev\" \"npm:game-server:dev\" \"npm:client:dev\"" + }, + "devDependencies": { + "concurrently": "^6.2.0" + }, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "wait-on": "^8.0.1" + } +} diff --git a/examples/html5-node/packages/client/package.json b/examples/html5-node/packages/client/package.json new file mode 100644 index 0000000000..cb3dbb96ee --- /dev/null +++ b/examples/html5-node/packages/client/package.json @@ -0,0 +1,13 @@ +{ + "name": "rivet-example-html5-client", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^4.3.9", + "rivet-sdk": "*" + } +} diff --git a/examples/html5-node/packages/client/src/index.html b/examples/html5-node/packages/client/src/index.html new file mode 100644 index 0000000000..a3d9f29dd0 --- /dev/null +++ b/examples/html5-node/packages/client/src/index.html @@ -0,0 +1,122 @@ + + + + + + Rivet Lobbies Example + + + + +
+

Rivet Lobbies Example

+ + + + + + + + +
+ Status + Idle +
+
+ Lobby ID + +
+
+ Origin + +
+
+ Region + +
+
+ Public IP + +
+
+ Counter + +
+
+ + + diff --git a/examples/html5-node/packages/client/src/index.ts b/examples/html5-node/packages/client/src/index.ts new file mode 100644 index 0000000000..e4b1ae6bbe --- /dev/null +++ b/examples/html5-node/packages/client/src/index.ts @@ -0,0 +1,210 @@ +import { Rivet } from "rivet-sdk"; + +const urlParams = new URLSearchParams(window.location.search); + +let rivet: Rivet; +let currentWebSocket: WebSocket | null = null; + +/** + * Sets the endpoint for the Rivet client and updates the UI + */ +function setEndpoint(newEndpoint: string): void { + rivet = new Rivet({ endpoint: newEndpoint }); + (document.getElementById("endpointInput") as HTMLInputElement).value = newEndpoint; + localStorage.setItem("endpoint", newEndpoint); +} + +/** + * Updates the status message in the UI + */ +function updateStatus(status: string): void { + console.log("Status update", status); + document.getElementById("status")!.textContent = status; +} + +/** + * Fetches and populates the available regions in the UI + */ +async function listRegions(): Promise { + updateStatus("Fetching regions..."); + try { + const regions = await rivet.lobbies.listRegions({}); + const regionSelect = document.getElementById("regionSelect") as HTMLSelectElement; + // Clear existing options + regionSelect.innerHTML = ""; + // Add new options + regionSelect.innerHTML = regions.regions.map((r) => + `` + ).join(""); + updateStatus("Idle"); + } catch (error) { + console.error("Error fetching regions:", error); + updateStatus(`Error fetching regions: ${(error as Error).message}`); + (document.getElementById("regionSelect") as HTMLSelectElement).innerHTML = + ''; + } +} + +/** + * Establishes a WebSocket connection to the game server + */ +function connect(lobby: any, players: any[]): void { + let protocol: string; + let hostname: string; + let port: number; + if (lobby.backend.server) { + protocol = lobby.backend.server.ports["game"].protocol; + hostname = lobby.backend.server.ports["game"].publicHostname; + port = lobby.backend.server.ports["game"].publicPort; + } else if (lobby.backend.localDevelopment) { + protocol = "http"; + hostname = lobby.backend.localDevelopment.ports["game"].hostname; + port = lobby.backend.localDevelopment.ports["game"].port; + } else { + throw new Error("unknown backend"); + } + + // Update status fields + document.getElementById("origin")!.textContent = `${protocol}://${hostname}:${port}`; + + updateStatus("Connecting"); + + let pingSend: number | null = null; + + // Close the existing WebSocket connection if it exists + if (currentWebSocket) { + currentWebSocket.close(); + } + + const ws = new WebSocket( + `${protocol}://${hostname}:${port}?token=${players[0].token}`, + ); + currentWebSocket = ws; + + ws.onopen = () => { + updateStatus("Initiating"); + }; + ws.onerror = (err: Event) => { + const errorMessage = `WebSocket error: ${ + (err as ErrorEvent).message || "Unknown error" + }`; + updateStatus(errorMessage); + console.error(errorMessage, err); + }; + ws.onclose = () => { + updateStatus("Closed"); + }; + ws.onmessage = (ev: MessageEvent) => { + try { + let [event, data] = JSON.parse(ev.data as string); + if (event == "init") { + updateStatus("Ready"); + + document.getElementById("publicIp")!.textContent = data.publicIp ?? "[not available locally]"; + setInterval(() => { + ws.send(JSON.stringify(["ping", 1])); + pingSend = performance.now(); + }, 1000); + } else if (event == "pong") { + console.log( + "ping rtt", + `${(performance.now() - pingSend!).toFixed(2)}ms`, + ); + } else if (event == "counter") { + document.getElementById("counter")!.textContent = data; + } + } catch (error) { + const parseErrorMessage = `Error parsing WebSocket message: ${ + (error as Error).message + }`; + updateStatus(parseErrorMessage); + console.error(parseErrorMessage, error); + } + }; +} + +/** + * Finds or creates a lobby and connects to it + */ +(globalThis as any).findOrCreateLobby = async function (): Promise { + const endpoint = (document.getElementById("endpointInput") as HTMLInputElement).value; + + const region = (document.getElementById("regionSelect") as HTMLSelectElement).value; + const tags = {}; + updateStatus("Waiting for lobby"); + try { + let res = await rivet.lobbies.findOrCreate({ + version: (document.getElementById("gameVersionInput") as HTMLInputElement).value, + regions: [region], + tags, + players: [{}], + + createConfig: { + region, + tags, + maxPlayers: 8, + maxPlayersDirect: 8, + }, + }); + + let { lobby, players } = res; + + // Update status fields + document.getElementById("lobbyId")!.textContent = lobby.id; + document.getElementById("region")!.textContent = lobby.region.name; + + // Test lobby connection + updateStatus("Connecting"); + try { + connect(lobby, players); + } catch (err) { + updateStatus(`Failed to connect: ${(err as Error).message}`); + console.warn("connection failed", err); + } + } catch (error) { + console.error("Error finding or creating lobby:", error); + updateStatus(`Error finding or creating lobby: ${(error as Error).message}`); + } +}; + +function setGameVersion(newGameVersion: string): void { + (document.getElementById("gameVersionInput") as HTMLInputElement).value = newGameVersion; + localStorage.setItem("gameVersion", newGameVersion); +} + +globalThis.addEventListener("load", () => { + // Load endpoint + const storedEndpoint = localStorage.getItem("endpoint") ?? "http://127.0.0.1:6420"; + if (storedEndpoint) { + setEndpoint(storedEndpoint); + } else { + updateStatus("Please enter an endpoint"); + } + + // Load game version + const storedGameVersion = localStorage.getItem("gameVersion") ?? "default"; + if (storedGameVersion) { + setGameVersion(storedGameVersion); + } else { + setGameVersion("default"); + } + + // Update the event listener to use 'input' instead of 'change' + (document.getElementById("endpointInput") as HTMLInputElement).addEventListener("input", async function () { + const newEndpoint = (document.getElementById("endpointInput") as HTMLInputElement).value; + setEndpoint(newEndpoint); + + // Clear regions before refetching + (document.getElementById("regionSelect") as HTMLSelectElement).innerHTML = ""; + + // Refetch regions with the new endpoint + await listRegions(); + }); + + (document.getElementById("gameVersionInput") as HTMLInputElement).addEventListener("input", function () { + const newGameVersion = (document.getElementById("gameVersionInput") as HTMLInputElement).value; + setGameVersion(newGameVersion); + }); + + listRegions(); +}); diff --git a/examples/html5-node/packages/client/tsconfig.json b/examples/html5-node/packages/client/tsconfig.json new file mode 100644 index 0000000000..3d5e339bd5 --- /dev/null +++ b/examples/html5-node/packages/client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "moduleResolution": "bundler", + "noImplicitAny": false, + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/html5-node/packages/client/vite.config.js b/examples/html5-node/packages/client/vite.config.js new file mode 100644 index 0000000000..f184187f5d --- /dev/null +++ b/examples/html5-node/packages/client/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + root: 'src', + build: { + outDir: '../dist', + }, + server: { + port: 8080, + }, +}) diff --git a/examples/html5-node/packages/game-server/.gitignore b/examples/html5-node/packages/game-server/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/examples/html5-node/packages/game-server/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/examples/html5-node/packages/game-server/package.json b/examples/html5-node/packages/game-server/package.json new file mode 100644 index 0000000000..58f2068fa5 --- /dev/null +++ b/examples/html5-node/packages/game-server/package.json @@ -0,0 +1,23 @@ +{ + "name": "rivet-example-html5-game-server", + "scripts": { + "start": "npx ts-node src/index.ts", + "dev": "npx nodemon --watch 'src/**/*.ts' --exec 'npx ts-node' src/index.ts", + "build": "npx tsc --project tsconfig.json" + }, + "dependencies": { + "express": "^4.18.2", + "node-fetch": "2", + "rivet-sdk": "*", + "ws": "^7.5.4" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.14.10", + "@types/node-fetch": "^2.6.11", + "@types/ws": "^8.5.12", + "ts-node": "^10.9.2", + "typescript": "^5.5.3", + "nodemon": "^2.0.20" + } +} diff --git a/examples/html5-node/packages/game-server/src/index.ts b/examples/html5-node/packages/game-server/src/index.ts new file mode 100644 index 0000000000..01a888c3b2 --- /dev/null +++ b/examples/html5-node/packages/game-server/src/index.ts @@ -0,0 +1,88 @@ +import { Rivet } from "rivet-sdk"; +import { Server as WebSocketServer, WebSocket } from "ws"; + +const LOBBY_ID = process.env.LOBBY_ID ?? "00000000-0000-0000-0000-000000000000"; +const LOBBY_TOKEN = process.env.LOBBY_TOKEN; + +const rivet = new Rivet(); + +// Setup WebSocket server +const wss = new WebSocketServer({ port: parseInt(process.env.PORT!) || 7777 }); +console.log(`WebSocket server listening on port ${wss.options.port}`); + +// Handle connections +const connectedClients: WebSocket[] = []; +wss.on("connection", async (ws: WebSocket, req: any) => { + connectedClients.push(ws); + + const searchParams = new URL(req.url!, `ws://${req.headers.host}`).searchParams; + const playerToken = searchParams.get("token"); + + ws.on("close", async () => { + // Remove client from connectedClients array + const index = connectedClients.indexOf(ws); + if (index > -1) { + connectedClients.splice(index, 1); + } + + // Rivet disconnection + try { + await rivet.lobbies.setPlayerDisconnected({ + lobbyId: LOBBY_ID, + lobbyToken: LOBBY_TOKEN, + playerTokens: [playerToken], + client: ws, + }); + } catch (err) { + console.error("Failed to disconnect player", err); + } + }); + + // Rivet connection + try { + await rivet.lobbies.setPlayerConnected({ + lobbyId: LOBBY_ID, + lobbyToken: LOBBY_TOKEN, + playerTokens: [playerToken], + }); + } catch (err) { + console.error("Failed to connect player", err); + ws.close(); + return; + } + + ws.on("message", (rawData: string) => { + let [event, data] = JSON.parse(rawData); + if (event === "ping") { + ws.send(JSON.stringify(["pong", data])); + } + }); + + // Send init + ws.send(JSON.stringify(["init", { publicIp: req.headers["x-forwarded-for"] } ])); + + // Send current counter value immediately upon connection + ws.send(JSON.stringify(["counter", globalCounter])); +}); + + +// Broadcast counter to all clients +let globalCounter = 0; +function broadcastCounter() { + connectedClients.forEach((client) => { + client.send(JSON.stringify(["counter", globalCounter])); + }); +} + +setInterval(() => { + globalCounter++; + broadcastCounter(); +}, 1000); + +// Ready to start accepting players +rivet.lobbies.setLobbyReady({ lobbyId: LOBBY_ID, lobbyToken: LOBBY_TOKEN }) + .then(() => console.log("Lobby ready")) + .catch((err) => { + console.error("Failed to start lobby", err); + process.exit(1); + }); diff --git a/examples/html5-node/packages/game-server/tsconfig.json b/examples/html5-node/packages/game-server/tsconfig.json new file mode 100644 index 0000000000..f297ffb1cb --- /dev/null +++ b/examples/html5-node/packages/game-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/examples/html5-node/packages/rivet-sdk/package.json b/examples/html5-node/packages/rivet-sdk/package.json new file mode 100644 index 0000000000..4cd34fa622 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/package.json @@ -0,0 +1,37 @@ +{ + "name": "rivet-sdk", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/rivet-gg/rivet.git" + }, + "scripts": { + "build": "tsup", + "prepare": "npm run build" + }, + + "devDependencies": { + "typescript": "^4.0 || ^5.0", + "tsup": "^8.3.0" + }, + + "tsup": { + "entry": ["src/index.ts"], + "format": ["esm", "cjs", "iife"], + "sourcemap": true, + "dts": true + }, + + "files": ["dist"], + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "browser": "./dist/index.global.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/client/client.ts b/examples/html5-node/packages/rivet-sdk/src/client/client.ts new file mode 100644 index 0000000000..0b81e1b85b --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/client/client.ts @@ -0,0 +1,144 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Configuration } from "./configuration.ts"; +import { RivetRequestError } from "./error.ts"; +import { Logger } from "./logger.ts"; + +export interface ClientConfiguration { + endpoint?: string; + gameVersion?: string; +} + +/** Low-level API used to build HTTP requests to the backend. */ +export class Client { + constructor(private configuration: Configuration) {} + + /** Builds the headers for a request */ + private _buildHeaders(): HeadersInit { + return { + "Accept": "application/json", + "Content-Type": "application/json", + }; + } + + /** Builds the complete URL to the backend */ + private _buildUrl(path: string): string { + return `${this.configuration.endpoint}${path}`; + } + + /** Creates a request */ + async buildRequest( + requestName: string | null, + method: string, + path: string, + body: any, + ): Promise { + const url = this._buildUrl(path); + const headers = this._buildHeaders(); + const bodyJson = JSON.stringify(body); + const startedAt = performance.now(); + + let response: Response | null = null; + let responseBody: any = null; + + try { + response = await fetch(url, { + method: method, + headers: headers, + body: bodyJson, + }); + + const elapsed = performance.now() - startedAt; + + // Try to parse the response body as JSON + try { + responseBody = await response.json(); + } catch (jsonError) { + responseBody = null; + } + + // Log the request and response + this._logRequest(requestName, url, response, responseBody, elapsed); + + if (response.ok) { + return responseBody; + } else { + // Handle HTTP errors + const errorCode = responseBody?.code || "unknown_error"; + const errorMessage = responseBody?.message || response.statusText || + "Unknown error"; + const errorMeta = responseBody?.meta; + + throw new RivetRequestError( + errorCode, + errorMessage, + response.status, + errorMeta, + ); + } + } catch (error) { + const elapsed = performance.now() - startedAt; + + // Log the error + this._logRequest(requestName, url, response, responseBody, elapsed, true); + + // Re-throw the error + throw error; + } + } + + private _logRequest( + requestName: string | null, + url: string, + response: Response | null, + responseBody: any, + elapsed: number, + isErrorOverride = false, + ) { + let logStr: string; + let isError = false; + + if (requestName != null) { + logStr = `request=${requestName}`; + } else { + logStr = `request=${url}`; + } + + if (response && response.ok && responseBody != null) { + logStr += " result=ok"; + } else if ( + response && + (response.status === 400 || response.status === 500) && + responseBody != null && + "message" in responseBody + ) { + if ("code" in responseBody) { + logStr += ` result=${responseBody.code}`; + } else { + logStr += " result=unknown_error"; + } + if ("module" in responseBody) { + logStr += ` module=${responseBody.module}`; + } + logStr += ` message="${responseBody.message}"`; + if ("meta" in responseBody) { + logStr += ` meta=${JSON.stringify(responseBody.meta)}`; + } + } else { + isError = true; + const statusText = response ? response.statusText : "unknown"; + const status = response ? response.status : "unknown"; + logStr += ` result=${statusText} http_status=${status} response_code=${status}`; + } + + logStr += ` elapsed=${elapsed}ms`; + + if (isError || isErrorOverride) { + Logger.error(logStr); + } else { + Logger.log(logStr); + } + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/client/configuration.ts b/examples/html5-node/packages/rivet-sdk/src/client/configuration.ts new file mode 100644 index 0000000000..16d73bf6d1 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/client/configuration.ts @@ -0,0 +1,33 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Logger } from "./logger.ts"; + +export interface ConfigurationOpts { + endpoint?: string; + gameVersion?: string; +} + +export class Configuration { + endpoint: string; + gameVersion: string; + + constructor(opts?: ConfigurationOpts) { + this.endpoint = opts?.endpoint ?? + this._getEnvironmentVariable("RIVET_BACKEND_ENDPOINT") ?? + "http://localhost:6420"; + this.gameVersion = opts?.gameVersion ?? + this._getEnvironmentVariable("GAME_VERSION") ?? "unknown"; + Logger.log(`Endpoint: ${this.endpoint}`); + Logger.log(`Game version: ${this.gameVersion}`); + } + + private _getEnvironmentVariable(name: string): string | undefined { + try { + return (globalThis as any).process?.env?.[name]; + } catch { + return undefined; + } + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/client/error.ts b/examples/html5-node/packages/rivet-sdk/src/client/error.ts new file mode 100644 index 0000000000..0a291290c0 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/client/error.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +/** Custom error class for API errors */ +export class RivetRequestError extends Error { + public code: string; + public status: number; + public meta?: any; + + constructor(code: string, message: string, status: number, meta?: any) { + super(message); + this.name = "RivetRequestError"; + this.code = code; + this.status = status; + this.meta = meta; + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/client/logger.ts b/examples/html5-node/packages/rivet-sdk/src/client/logger.ts new file mode 100644 index 0000000000..e478924028 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/client/logger.ts @@ -0,0 +1,17 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +export class Logger { + static log(message: string) { + console.log(`[Rivet] ${message}`); + } + + static warning(message: string) { + console.warn(`[Rivet] ${message}`); + } + + static error(message: string) { + console.error(`[Rivet] ${message}`); + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/index.ts b/examples/html5-node/packages/rivet-sdk/src/index.ts new file mode 100644 index 0000000000..7f932fddc8 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/index.ts @@ -0,0 +1,44 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Client } from "./client/client.ts"; +import { Configuration, ConfigurationOpts } from "./client/configuration.ts"; + +import { RivetRateLimit } from "./modules/rate_limit.ts"; + +import { RivetTokens } from "./modules/tokens.ts"; + +import { RivetLobbies } from "./modules/lobbies.ts"; + +import { RivetRivet } from "./modules/rivet.ts"; + +/** Singleton class for Rivet API */ +export class Rivet { + /** Configuration for interacting with Rivet. */ + public configuration: Configuration; + + /** Client used to connect to the backend. */ + private client: Client; + + rate_limit: RivetRateLimit; + + tokens: RivetTokens; + + lobbies: RivetLobbies; + + rivet: RivetRivet; + + constructor(opts?: ConfigurationOpts) { + this.configuration = new Configuration(opts); + this.client = new Client(this.configuration); + + this.rate_limit = new RivetRateLimit(this.client); + + this.tokens = new RivetTokens(this.client); + + this.lobbies = new RivetLobbies(this.client); + + this.rivet = new RivetRivet(this.client); + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/modules/lobbies.ts b/examples/html5-node/packages/rivet-sdk/src/modules/lobbies.ts new file mode 100644 index 0000000000..ffe3e6a609 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/modules/lobbies.ts @@ -0,0 +1,187 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Client } from "../client/client.ts"; + +/** + * Lobbies + * + * Lobby & player management. Create & join lobbies instantly. + */ +export class RivetLobbies { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + /** + * Create Lobby + * Creates a new lobby on-demand. + */ + create(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.create", + "POST", + "/modules/lobbies/scripts/create/call", + body, + ); + } + + /** + * Destroy Lobby + * Destroys an existing lobby. + */ + destroy(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.destroy", + "POST", + "/modules/lobbies/scripts/destroy/call", + body, + ); + } + + /** + * Find Or Create Lobby + * Finds a lobby or creates one if there are no available spots for players. + */ + findOrCreate(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.find_or_create", + "POST", + "/modules/lobbies/scripts/find_or_create/call", + body, + ); + } + + /** + * Join Lobby + * Add a player to an existing lobby. + */ + join(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.join", + "POST", + "/modules/lobbies/scripts/join/call", + body, + ); + } + + /** + * List Lobbies + * List & query all lobbies. + */ + list(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.list", + "POST", + "/modules/lobbies/scripts/list/call", + body, + ); + } + + /** + * Set Lobby Ready + * Called on lobby startup after initiation to notify it can start accepting player. This should be called after operations like loading maps are complete. + */ + setLobbyReady(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.set_lobby_ready", + "POST", + "/modules/lobbies/scripts/set_lobby_ready/call", + body, + ); + } + + /** + * Set Player Connected + * Called when a player connects to the lobby. + */ + setPlayerConnected(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.set_player_connected", + "POST", + "/modules/lobbies/scripts/set_player_connected/call", + body, + ); + } + + /** + * Set Player Disconnected + * Called when a player disconnects from the lobby. + */ + setPlayerDisconnected(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.set_player_disconnected", + "POST", + "/modules/lobbies/scripts/set_player_disconnected/call", + body, + ); + } + + /** + * Find Lobby + * Finds an existing lobby with a given query. This will not create a new lobby, see `find_or_create` instead. + */ + find(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.find", + "POST", + "/modules/lobbies/scripts/find/call", + body, + ); + } + + /** + * List Regions + * List available regions. + */ + listRegions(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.list_regions", + "POST", + "/modules/lobbies/scripts/list_regions/call", + body, + ); + } + + /** + * Force Garbage Collection + * Rarely used. Forces the matchmaker to purge lobbies & players. + */ + forceGc(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.force_gc", + "POST", + "/modules/lobbies/scripts/force_gc/call", + body, + ); + } + + /** + * Fetch Lobby Manager State + * See full state of the lobby manager for debugging. + */ + fetchLobbyManagerState(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.fetch_lobby_manager_state", + "POST", + "/modules/lobbies/scripts/fetch_lobby_manager_state/call", + body, + ); + } + + /** + * Reset Lobby Manager State + * Reset lobby manager state. For debugging only. + */ + resetLobbyManagerState(body: any = {}): Promise { + return this.client.buildRequest( + "lobbies.reset_lobby_manager_state", + "POST", + "/modules/lobbies/scripts/reset_lobby_manager_state/call", + body, + ); + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/modules/rate_limit.ts b/examples/html5-node/packages/rivet-sdk/src/modules/rate_limit.ts new file mode 100644 index 0000000000..e46c529f5d --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/modules/rate_limit.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Client } from "../client/client.ts"; + +/** + * Rate Limit + * + * Prevent abuse by limiting request rate. + */ +export class RivetRateLimit { + client: Client; + + constructor(client: Client) { + this.client = client; + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/modules/rivet.ts b/examples/html5-node/packages/rivet-sdk/src/modules/rivet.ts new file mode 100644 index 0000000000..9c0ad25041 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/modules/rivet.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Client } from "../client/client.ts"; + +/** + * Rivet + * + * Open-source game servers & backend for multiplayer games. + */ +export class RivetRivet { + client: Client; + + constructor(client: Client) { + this.client = client; + } +} diff --git a/examples/html5-node/packages/rivet-sdk/src/modules/tokens.ts b/examples/html5-node/packages/rivet-sdk/src/modules/tokens.ts new file mode 100644 index 0000000000..7de71bdc87 --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/src/modules/tokens.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by the Rivet (https://rivet.gg) build system. +// +// Do not edit this file directly. + +import { Client } from "../client/client.ts"; + +/** + * Tokens + * + * Create & verify tokens for authorization purposes. + */ +export class RivetTokens { + client: Client; + + constructor(client: Client) { + this.client = client; + } +} diff --git a/examples/html5-node/packages/rivet-sdk/tsconfig.json b/examples/html5-node/packages/rivet-sdk/tsconfig.json new file mode 100644 index 0000000000..0490d9c1ed --- /dev/null +++ b/examples/html5-node/packages/rivet-sdk/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "esnext", + "module": "commonjs", + "moduleResolution": "node", + "allowImportingTsExtensions": true + }, + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/examples/html5-node/rivet.dev.json b/examples/html5-node/rivet.dev.json new file mode 100644 index 0000000000..84b50f8297 --- /dev/null +++ b/examples/html5-node/rivet.dev.json @@ -0,0 +1,20 @@ +{ + "extends": "./rivet.json", + "modules": { + "lobbies": { + "config": { + "lobbies": { + "regions": ["local"], + "backend": { + "localDevelopment": { + "tags": {}, + "ports": { + "game": {"protocol": "http", "port": 7777} + } + } + } + } + } + } + } +} diff --git a/examples/html5-node/rivet.json b/examples/html5-node/rivet.json new file mode 100644 index 0000000000..db74615d4d --- /dev/null +++ b/examples/html5-node/rivet.json @@ -0,0 +1,37 @@ +{ + "sdks": [{"output": "packages/rivet-sdk", "target": "typescript"}], + "runtime": { + "cors": { + "origins": ["http://127.0.0.1:8080", "http://localhost:8080"] + } + }, + "modules": { + "rate_limit": {}, + "tokens": {}, + "lobbies": { + "config": { + "lobbies": { + "regions": ["atl"], + "backend": { + "server": { + "environment": { + "SERVER_HOSTNAME": "0.0.0.0" + }, + "ports": { + "game": { + "protocol": "https", + "internalPort": 7777 + } + }, + "resources": { + "cpu": 250, + "memory": 250 + } + } + } + } + } + }, + "rivet": {} + } +} diff --git a/examples/js-deno/deno.json b/examples/js-deno/deno.json new file mode 100644 index 0000000000..5dcc847e2d --- /dev/null +++ b/examples/js-deno/deno.json @@ -0,0 +1,3 @@ +{ + "imports": { "@std/random": "jsr:@std/random@^0.1.0" } +} diff --git a/examples/js-deno/deno.lock b/examples/js-deno/deno.lock new file mode 100644 index 0000000000..85a765dc5b --- /dev/null +++ b/examples/js-deno/deno.lock @@ -0,0 +1,17 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/random@*": "0.1.0", + "jsr:@std/random@0.1": "0.1.0" + }, + "jsr": { + "@std/random@0.1.0": { + "integrity": "70a006be0ffb77d036bab54aa8ae6bd0119ba77ace0f2f56f63273d4262a5667" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/random@0.1" + ] + } +} diff --git a/examples/js-deno/rivet.jsonc b/examples/js-deno/rivet.jsonc new file mode 100644 index 0000000000..e989dc599c --- /dev/null +++ b/examples/js-deno/rivet.jsonc @@ -0,0 +1,10 @@ +{ + "version": "2.0", + "builds": [ + { + "tags": { "name": "rng" }, + "runtime": "javascript", + "script": "rng_http.ts" + } + ] +} diff --git a/examples/js-deno/rng_http.ts b/examples/js-deno/rng_http.ts new file mode 100644 index 0000000000..2418c3dc11 --- /dev/null +++ b/examples/js-deno/rng_http.ts @@ -0,0 +1,28 @@ +import { randomIntegerBetween, randomSeeded } from "@std/random"; + +console.log("Hello, world!"); +console.log(Deno.env.toObject()); + +const portStr = Deno.env.get("PORT_http") ?? Deno.env.get("HTTP_PORT"); +if (!portStr) throw "Missing port"; +const port = parseInt(portStr); +if (!isFinite(port)) throw "Invalid port"; + +const server = Deno.serve({ + handler, + port, + hostname: "0.0.0.0", +}); + +await server.finished; + +const prng = randomSeeded(1n); + +function handler(req: Request) { + console.log("Received request"); + + return new Response(randomIntegerBetween(1, 100, { prng }).toString(), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/examples/js-kv/deno.json b/examples/js-kv/deno.json new file mode 100644 index 0000000000..51f36d1a00 --- /dev/null +++ b/examples/js-kv/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@rivet-gg/actors-core": "jsr:@rivet-gg/actors-core@0.0.1-rc.4" + } +} diff --git a/examples/js-kv/deno.lock b/examples/js-kv/deno.lock new file mode 100644 index 0000000000..7c1e459b34 --- /dev/null +++ b/examples/js-kv/deno.lock @@ -0,0 +1,16 @@ +{ + "version": "4", + "specifiers": { + "jsr:@rivet-gg/actors-core@0.0.1-rc.4": "0.0.1-rc.4" + }, + "jsr": { + "@rivet-gg/actors-core@0.0.1-rc.4": { + "integrity": "7c2cc059d64f90171f66e8b42131ef4b17b77f749dc42b0997bc1e86fa337b96" + } + }, + "workspace": { + "dependencies": [ + "jsr:@rivet-gg/actors-core@0.0.1-rc.4" + ] + } +} diff --git a/examples/js-kv/index.ts b/examples/js-kv/index.ts new file mode 100644 index 0000000000..5007dc3937 --- /dev/null +++ b/examples/js-kv/index.ts @@ -0,0 +1,10 @@ +import Rivet from "@rivet-gg/actors-core"; + +await Rivet.kv.put("count", 0); + +console.log('Started', Deno.env.toObject()); +setInterval(async () => { + let x = await Rivet.kv.get("count"); + await Rivet.kv.put("count", x + 1); + console.log('Count', x); +}, 1000); diff --git a/examples/js-kv/rivet.jsonc b/examples/js-kv/rivet.jsonc new file mode 100644 index 0000000000..a9c33fc1ae --- /dev/null +++ b/examples/js-kv/rivet.jsonc @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "builds": [ + { + "tags": { "name": "kv" }, + "runtime": "javascript", + "bundler": "deno", + "script": "index.ts" + } + ] +} diff --git a/examples/js-manual/echo_http.js b/examples/js-manual/echo_http.js new file mode 100644 index 0000000000..0350aa0645 --- /dev/null +++ b/examples/js-manual/echo_http.js @@ -0,0 +1,18 @@ +console.log(Deno.env.toObject()); + +let server = Deno.serve({ + handler, + port: parseInt(Deno.env.get("PORT_ds_http") ?? Deno.env.get("HTTP_PORT")), + hostname: "0.0.0.0", +}); + +await server.finished; + +function handler(req) { + console.log("req"); + + return new Response(req.body, { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/examples/js-manual/rivet.jsonc b/examples/js-manual/rivet.jsonc new file mode 100644 index 0000000000..b477c8d818 --- /dev/null +++ b/examples/js-manual/rivet.jsonc @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "builds": [ + { + "tags": { "name": "echo-http" }, + "runtime": "javascript", + "bundler": "none", + "script": "echo_http.js" + } + ] +} diff --git a/examples/types-test/.vscode/settings.json b/examples/types-test/.vscode/settings.json new file mode 100644 index 0000000000..b943dbc7a3 --- /dev/null +++ b/examples/types-test/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/examples/types-test/deno.json b/examples/types-test/deno.json new file mode 100644 index 0000000000..72c98d4c1a --- /dev/null +++ b/examples/types-test/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@rivet-gg/actor-types": "jsr:@rivet-gg/actor-types@^0.0.9", + "@std/assert": "jsr:@std/assert@1" + } +} diff --git a/examples/types-test/deno.lock b/examples/types-test/deno.lock new file mode 100644 index 0000000000..95aac8bb30 --- /dev/null +++ b/examples/types-test/deno.lock @@ -0,0 +1,32 @@ +{ + "version": "4", + "specifiers": { + "jsr:@rivet-gg/actor-types@^0.0.4": "0.0.4", + "jsr:@rivet-gg/actor-types@^0.0.9": "0.0.9", + "jsr:@std/assert@1": "1.0.8", + "jsr:@std/internal@^1.0.5": "1.0.5" + }, + "jsr": { + "@rivet-gg/actor-types@0.0.4": { + "integrity": "7679e0a7e0456516453d3334835ca2e75cfb95534d265252b39aff5a7ba002d6" + }, + "@rivet-gg/actor-types@0.0.9": { + "integrity": "076edf5dbb0b523a2ba5cd26ea6f7099666dfda02cf3a55b2e84339e1b56be75" + }, + "@std/assert@1.0.8": { + "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + } + }, + "workspace": { + "dependencies": [ + "jsr:@rivet-gg/actor-types@^0.0.9", + "jsr:@std/assert@1" + ] + } +} diff --git a/examples/types-test/main.ts b/examples/types-test/main.ts new file mode 100644 index 0000000000..1d2596da26 --- /dev/null +++ b/examples/types-test/main.ts @@ -0,0 +1 @@ +import Rivet from "@rivet-gg/actor-types"; diff --git a/examples/wip/rivet.jsonc b/examples/wip/rivet.jsonc new file mode 100644 index 0000000000..1ce036e016 --- /dev/null +++ b/examples/wip/rivet.jsonc @@ -0,0 +1,56 @@ +{ + "version": "2.0", + "builds": [ + // Existing Docker image + { + "tags": { "name": "party" }, + "runtime": "docker", + "image": "existing image" + }, + // Build Docker image + { + "tags": { "name": "party" }, + "runtime": "docker", + "dockerfile": "Dockerfile", + // Optional: + "context": ".", + "build_target": "runner", + "args": { + "MY_ARG": "abc123" + }, + "unstable": { + "allow_root": true, + "build_method": "native", + "build_kind": "oci-bundle", + "build_compression": "lz4" + } + }, + // Build Deno script + // TODO: What build steps do we take? + // - Bundle Deno dependencies + // - Compile to JS + { + "tags": { "name": "party" }, + "runtime": "javascript", + "path": "./build.ts", + // Optional: + "import_map": "/path/to/import-map.json", + "deno": { + "config": "/path/to/deno.json", + "no_lock": false, + "no_remote": false + }, + "bundle": { + "minify": true + } + }, + // Prebuilt JavaScript + { + "tags": { "name": "party" }, + "runtime": "javascript", + "mode": "raw", + "command": "yarn run build", + "path": "./build.js" + } + ] +} diff --git a/packages/common/deno-embed/Cargo.toml b/packages/common/deno-embed/Cargo.toml new file mode 100644 index 0000000000..98ff897a90 --- /dev/null +++ b/packages/common/deno-embed/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "deno-embed" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow = "1.0" +tokio = { version = "1.40.0", features = ["full"] } + +[features] +# Enable if using from a build script, since this'll cause problems if cross +# compiling. +ignore-override-target = [] + +[build-dependencies] +dirs = "5.0.1" +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] } +serde_json = "1.0.128" +zip = "0.5" + +[dev-dependencies] +tempfile = "3.13.0" + diff --git a/packages/common/deno-embed/build.rs b/packages/common/deno-embed/build.rs new file mode 100644 index 0000000000..2f642f0d38 --- /dev/null +++ b/packages/common/deno-embed/build.rs @@ -0,0 +1,140 @@ +use dirs::cache_dir; +use reqwest::blocking::Client; +use serde_json::Value; +use std::{env, fs, path::Path}; +use zip::ZipArchive; + +const GITHUB_API_URL: &str = "https://api.github.com/repos/denoland/deno"; +const DENO_VERSION: &str = "2.0.6"; +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +fn main() -> Result<(), Box> { + for (key, value) in std::env::vars() { + println!("{}: {}", key, value); + } + + let target = if cfg!(feature = "ignore-override-target") { + env::var("TARGET").unwrap() + } else { + env::var("OVERRIDE_TARGET").unwrap_or_else(|_| env::var("TARGET").unwrap()) + }; + println!("cargo::rerun-if-env-changed=OVERRIDE_TARGET"); + + let out_dir = env::var("OUT_DIR")?; + let cache_dir = get_cache_dir()?; + + let release_data = fetch_release_data()?; + let asset = find_matching_asset(&release_data, &target)?; + let zip_path = download_binary_if_needed(&asset, &cache_dir)?; + let output_path = extract_and_save_binary(&zip_path, &out_dir)?; + + println!("cargo:rustc-env=DENO_BINARY_PATH={}", output_path.display()); + println!("cargo:rustc-env=DENO_VERSION={DENO_VERSION}"); + + Ok(()) +} + +fn fetch_release_data() -> Result> { + let release_url = format!("{}/releases/tags/v{}", GITHUB_API_URL, DENO_VERSION); + println!("Fetching release information from: {}", release_url); + + let client = Client::new(); + let mut request = client + .get(&release_url) + .header(reqwest::header::USER_AGENT, USER_AGENT); + + if let Ok(token) = env::var("GITHUB_TOKEN") { + request = request.header(reqwest::header::AUTHORIZATION, format!("token {}", token)); + } + + let response = request.send()?; + let status = response.status(); + if !status.is_success() { + let error_text = response.text()?; + eprintln!("Error response: {}", error_text); + return Err(format!("HTTP request failed with status {}: {}", status, error_text).into()); + } + Ok(response.json()?) +} + +fn find_matching_asset<'a>( + release_data: &'a Value, + target: &str, +) -> Result<&'a Value, Box> { + let assets = release_data["assets"].as_array().ok_or("No assets found")?; + let deno_target = map_rust_target_to_deno(target); + assets + .iter() + .find(|asset| { + let name = asset["name"].as_str().unwrap(); + name == deno_target + }) + .ok_or_else(|| format!("No matching asset found for the target: {}", deno_target).into()) +} + +fn map_rust_target_to_deno(target: &str) -> &'static str { + match target { + x if x.starts_with("x86_64-unknown-linux-") => "deno-x86_64-unknown-linux-gnu.zip", + x if x.starts_with("aarch64-unknown-linux-") => "deno-aarch64-unknown-linux-gnu.zip", + x if x.starts_with("x86_64-pc-windows-") => "deno-x86_64-pc-windows-msvc.zip", + "x86_64-apple-darwin" => "deno-x86_64-apple-darwin.zip", + "aarch64-apple-darwin" => "deno-aarch64-apple-darwin.zip", + _ => panic!( + "Unsupported target: {}. Set OVERRIDE_TARGET if needed.", + target + ), + } +} + +fn download_binary_if_needed( + asset: &Value, + cache_dir: &Path, +) -> Result> { + let download_url = asset["browser_download_url"].as_str().unwrap(); + let file_name = asset["name"].as_str().unwrap(); + let zip_path = cache_dir.join(file_name); + + if !zip_path.exists() { + println!("Downloading Deno binary from: {}", download_url); + + let client = Client::new(); + let mut request = client + .get(download_url) + .header(reqwest::header::USER_AGENT, USER_AGENT); + + if let Ok(token) = env::var("GITHUB_TOKEN") { + request = request.header(reqwest::header::AUTHORIZATION, format!("token {}", token)); + } + + let response = request.send()?.error_for_status()?; + + let mut file = fs::File::create(&zip_path)?; + std::io::copy(&mut response.bytes()?.as_ref(), &mut file)?; + } else { + println!("Using cached Deno binary: {}", zip_path.display()); + } + + Ok(zip_path) +} + +fn extract_and_save_binary( + zip_path: &Path, + out_dir: &str, +) -> Result> { + let file = fs::File::open(zip_path)?; + let mut archive = ZipArchive::new(file)?; + let mut file = archive.by_index(0)?; + let output_path = Path::new(out_dir).join("deno"); + + let mut output_file = fs::File::create(&output_path)?; + std::io::copy(&mut file, &mut output_file)?; + + Ok(output_path) +} + +fn get_cache_dir() -> Result> { + let system_cache_dir = cache_dir().ok_or("Failed to get system cache directory")?; + let deno_cache_dir = system_cache_dir.join("deno-embed").join(DENO_VERSION); + fs::create_dir_all(&deno_cache_dir)?; + Ok(deno_cache_dir) +} diff --git a/packages/common/deno-embed/src/lib.rs b/packages/common/deno-embed/src/lib.rs new file mode 100644 index 0000000000..b109da5000 --- /dev/null +++ b/packages/common/deno-embed/src/lib.rs @@ -0,0 +1,96 @@ +use anyhow::*; +use std::path::PathBuf; + +pub const DENO_BINARY: &[u8] = include_bytes!(env!("DENO_BINARY_PATH")); +pub const DENO_VERSION: &str = env!("DENO_VERSION"); + +pub struct DenoExecutable { + pub executable_path: PathBuf, +} + +/// Writes the executable to the file system if needed and returns the path. +pub async fn get_executable(data_dir: &PathBuf) -> Result { + let executable_name = if cfg!(windows) { "deno.exe" } else { "deno" }; + let executable_path = data_dir + .join("deno") + .join(DENO_VERSION) + .join(executable_name); + + if tokio::fs::metadata(&executable_path).await.is_ok() { + return Ok(DenoExecutable { executable_path }); + } + + // Ensure the parent directory exists + if let Some(parent) = executable_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + + // Write the binary to the executable path + tokio::fs::write(&executable_path, DENO_BINARY).await?; + + // Make the file executable on Unix-like systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = tokio::fs::metadata(&executable_path).await?.permissions(); + perms.set_mode(0o755); + tokio::fs::set_permissions(&executable_path, perms).await?; + } + + Ok(DenoExecutable { executable_path }) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use tokio; + + #[tokio::test] + async fn test_deno_get_or_download_executable() -> Result<()> { + // Create a temporary directory for the test + let temp_dir = TempDir::new()?; + let data_dir = temp_dir.path().to_path_buf(); + + // First call: should download the executable + let result1 = get_executable(&data_dir).await?; + assert!( + result1.executable_path.exists(), + "Executable should be created" + ); + + // Check if the file is executable on Unix-like systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let metadata = tokio::fs::metadata(&result1.executable_path).await?; + let permissions = metadata.permissions(); + assert_eq!( + permissions.mode() & 0o777, + 0o755, + "Executable should have correct permissions" + ); + } + + // Second call: should return the existing executable + let result2 = get_executable(&data_dir).await?; + assert_eq!( + result1.executable_path, result2.executable_path, + "Should return the same executable path" + ); + + // Run deno --version and check the output + let output = std::process::Command::new(&result1.executable_path) + .arg("--version") + .output()?; + + let version_output = + String::from_utf8(output.stdout).context("parse deno version output")?; + assert!( + version_output.contains(DENO_VERSION), + "Deno version output should contain the expected version" + ); + + Ok(()) + } +} diff --git a/packages/common/kv-str/Cargo.toml b/packages/common/kv-str/Cargo.toml new file mode 100644 index 0000000000..5bde1afc2d --- /dev/null +++ b/packages/common/kv-str/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "kv-str" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow = "1.0.93" +envy = "0.4.2" +serde = "1.0.215" diff --git a/packages/common/kv-str/src/lib.rs b/packages/common/kv-str/src/lib.rs new file mode 100644 index 0000000000..45b8cec1cc --- /dev/null +++ b/packages/common/kv-str/src/lib.rs @@ -0,0 +1,24 @@ +use anyhow::*; +use serde::de::DeserializeOwned; +use std::collections::HashMap; + +/// Parses a string like `foo=bar,hello=world` in to a Serde struct. +/// +/// This uses `envy` under the hood. Refer to that for reference on behavior. +pub fn from_str(input: &str) -> Result { + let vars_iter = input + .split(',') + .map(|pair| pair.split_once('=').unwrap_or((&pair, "true"))) + .map(|(k, v)| (k.to_string(), v.to_string())); + let output = envy::from_iter::<_, T>(vars_iter)?; + Ok(output) +} + +pub fn to_str(input: &HashMap) -> Result { + let mut input = input + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>(); + input.sort(); + Ok(input.join(" ")) +} diff --git a/packages/toolchain/cli/Cargo.toml b/packages/toolchain/cli/Cargo.toml new file mode 100644 index 0000000000..cc41e76730 --- /dev/null +++ b/packages/toolchain/cli/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "rivet-cli" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[[bin]] +name = "rivet" +path = "src/main.rs" + +[features] +default = ["sentry"] +sentry = [] + +[dependencies] +clap = { version = "4.5.9", features = ["derive"] } +toolchain = { path = "../toolchain", package = "rivet-toolchain" } +tokio = { version = "1.40.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.120" +anyhow = "1.0" +uuid = { version = "1.11.0", features = ["v4"] } +url = { version = "2.5.3", features = ["serde"] } +base64 = "0.22.1" +kv-str.workspace = true +inquire = "0.7.5" +webbrowser = "1.0.2" +sentry = { version = "0.34.0", features = ["anyhow"] } +sysinfo = "0.32.0" +ctrlc = "3.4.5" +async-posthog.workspace = true + +[build-dependencies] +anyhow = "1.0" + +[build-dependencies.vergen-git2] +version = "1.0.0" +features = [ + "build", + "cargo", + "rustc" +] + diff --git a/packages/toolchain/cli/build.rs b/packages/toolchain/cli/build.rs new file mode 100644 index 0000000000..c03a04c7c2 --- /dev/null +++ b/packages/toolchain/cli/build.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder}; + +fn main() -> Result<()> { + Emitter::default() + .add_instructions(&BuildBuilder::all_build()?)? + .add_instructions(&CargoBuilder::all_cargo()?)? + .add_instructions(&Git2Builder::default().sha(true).branch(true).build()?)? + .add_instructions(&RustcBuilder::all_rustc()?)? + .emit()?; + + Ok(()) +} diff --git a/packages/toolchain/cli/src/commands/actor/create.rs b/packages/toolchain/cli/src/commands/actor/create.rs new file mode 100644 index 0000000000..13cb5b4e25 --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/create.rs @@ -0,0 +1,282 @@ +use anyhow::*; +use clap::{Parser, ValueEnum}; +use serde::Deserialize; +use std::collections::HashMap; +use toolchain::{ + build, + rivet_api::{apis, models}, +}; +use uuid::Uuid; + +#[derive(ValueEnum, Clone)] +enum NetworkMode { + Bridge, + Host, +} + +/// Custom struct that includes the port name in it. The name is mapped to the key in the `ports` +/// map. +#[derive(Deserialize)] +struct Port { + name: String, + protocol: models::ActorPortProtocol, + internal_port: Option, + guard: Option, + #[serde(default)] + host: bool, +} + +#[derive(Parser)] +pub struct Opts { + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long, short = 'r')] + region: Option, + + /// Tags to use for both the actor & build tags. This allows for creating actors quickly since + /// the tags are often identical between the two. + #[clap(long = "tags", short = 't')] + universal_tags: Option, + + #[clap(long, short = 'a')] + actor_tags: Option, + + /// Build ID. + #[clap(long)] + build: Option, + + #[clap(long, short = 'b')] + build_tags: Option, + + #[clap(long = "env-var")] + env_vars: Option>, + + #[clap(long, value_enum)] + network_mode: Option, + + #[clap(long = "port", short = 'p')] + ports: Option>, + + #[clap(long, default_value = "1000")] + cpu: i32, + + #[clap(long, default_value = "1024")] + memory: i32, + + #[clap(long)] + kill_timeout: Option, + + #[clap(long)] + durable: bool, + + /// If included, the `current` tag will not be automatically inserted to the build tag. + #[clap(long)] + no_build_current_tag: bool, + + #[clap(long)] + logs: bool, + + #[clap(long)] + log_stream: Option, + + #[clap(long)] + deploy: bool, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + // Parse tags + let actor_tags = if let Some(t) = &self.actor_tags { + kv_str::from_str::>(t)? + } else if let Some(t) = &self.universal_tags { + kv_str::from_str::>(t)? + } else { + // No tags + HashMap::new() + }; + + // Parse build ID + let mut build_id = self + .build + .as_ref() + .map(|b| Uuid::parse_str(&b)) + .transpose() + .context("invalid build uuid")?; + + // Parse build tags + let mut build_tags = if let Some(t) = &self.build_tags { + Some(kv_str::from_str::>(t)?) + } else if let Some(t) = &self.universal_tags { + Some(kv_str::from_str::>(t)?) + } else { + None + }; + + // Parse ports + let ports = self + .ports + .as_ref() + .map(|ports| { + ports + .iter() + .map(|port_str| { + let port = kv_str::from_str::(port_str)?; + Ok(( + port.name, + models::ActorCreateActorPortRequest { + internal_port: port.internal_port, + protocol: port.protocol, + routing: Some(Box::new(models::ActorPortRouting { + guard: port.guard.map(Box::new), + host: if port.host { + Some(serde_json::json!({})) + } else { + None + }, + })), + }, + )) + }) + .collect::>>() + }) + .transpose()?; + + // Parse environment variables + let env_vars = self + .env_vars + .as_ref() + .map(|env_vars| { + env_vars + .iter() + .map(|env| { + env.split_once('=') + .map(|(k, v)| (k.to_string(), v.to_string())) + .with_context(|| anyhow!("invalid env value: {env}")) + }) + .collect::>>() + }) + .transpose()?; + + // Auto-deploy + if self.deploy { + // Remove build tags, since we'll be using the build ID + let build_tags = build_tags + .take() + .context("must define build tags when using deploy flag")?; + + // Deploys erver + let deploy_build_ids = crate::util::deploy::deploy(crate::util::deploy::DeployOpts { + environment: &env, + build_tags: Some(build_tags), + }) + .await?; + + if deploy_build_ids.len() > 1 { + println!("Warning: Multiple build IDs match tags, proceeding with first"); + } + + let deploy_build_id = deploy_build_ids + .first() + .context("No builds matched build tags")?; + build_id = Some(*deploy_build_id); + } + + // Automatically add `current` tag to make querying easier + // + // Do this AFTER the deploy since this will mess up the build filter. + if !self.no_build_current_tag { + if let Some(build_tags) = build_tags.as_mut() { + if !build_tags.contains_key(build::tags::VERSION) { + build_tags.insert(build::tags::CURRENT.into(), "true".into()); + } + } + } + + // Auto-select region if needed + let region = if let Some(region) = &self.region { + region.clone() + } else { + let regions = apis::actor_regions_api::actor_regions_list( + &ctx.openapi_config_cloud, + Some(&ctx.project.name_id.to_string()), + Some(&env), + ) + .await?; + + // TODO(RVT-4207): Improve automatic region selection logic + // Choose a region + let auto_region = if let Some(ideal_region) = regions + .regions + .iter() + .filter(|r| r.id == "lax" || r.id == "local") + .next() + { + ideal_region.id.clone() + } else { + regions.regions.first().context("no regions")?.id.clone() + }; + println!("Automatically selected region: {auto_region}"); + + auto_region + }; + + let request = models::ActorCreateActorRequest { + region: Some(region), + tags: Some(serde_json::json!(actor_tags)), + build: build_id, + build_tags: build_tags.map(|bt| Some(serde_json::json!(bt))), + runtime: Some(Box::new(models::ActorCreateActorRuntimeRequest { + environment: env_vars, + })), + network: Some(Box::new(models::ActorCreateActorNetworkRequest { + mode: self.network_mode.as_ref().map(|mode| match mode { + NetworkMode::Bridge => models::ActorNetworkMode::Bridge, + NetworkMode::Host => models::ActorNetworkMode::Host, + }), + ports, + })), + resources: Box::new(models::ActorResources { + cpu: self.cpu, + memory: self.memory, + }), + lifecycle: Some(Box::new(models::ActorLifecycle { + durable: Some(self.durable), + kill_timeout: self.kill_timeout, + })), + }; + + let response = apis::actor_api::actor_create( + &ctx.openapi_config_cloud, + request, + Some(&ctx.project.name_id), + Some(&env), + ) + .await?; + println!("Created actor:\n{:#?}", response.actor); + + // Tail logs + if self.logs { + crate::util::actor::logs::tail( + &ctx, + crate::util::actor::logs::TailOpts { + environment: &env, + actor_id: response.actor.id, + stream: self + .log_stream + .clone() + .unwrap_or(crate::util::actor::logs::LogStream::All), + follow: true, + timestamps: true, + }, + ) + .await?; + } + + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/actor/destroy.rs b/packages/toolchain/cli/src/commands/actor/destroy.rs new file mode 100644 index 0000000000..9f9a963e64 --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/destroy.rs @@ -0,0 +1,39 @@ +use anyhow::*; +use clap::Parser; +use toolchain::{errors, rivet_api::apis}; +use uuid::Uuid; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1)] + id: String, + + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long)] + override_kill_timeout: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let actor_id = + Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?; + + apis::actor_api::actor_destroy( + &ctx.openapi_config_cloud, + &actor_id.to_string(), + Some(&ctx.project.name_id), + Some(&env), + self.override_kill_timeout, + ) + .await?; + + println!("Destroyed actor: {actor_id}"); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/actor/get.rs b/packages/toolchain/cli/src/commands/actor/get.rs new file mode 100644 index 0000000000..e03000ed05 --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/get.rs @@ -0,0 +1,34 @@ +use anyhow::*; +use clap::Parser; +use toolchain::rivet_api::apis; +use uuid::Uuid; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1)] + id: String, + + #[clap(long, alias = "env", short = 'e')] + environment: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let actor_id = Uuid::parse_str(&self.id).context("invalid id uuid")?; + + let res = apis::actor_api::actor_get( + &ctx.openapi_config_cloud, + &actor_id.to_string(), + Some(&ctx.project.name_id), + Some(&env), + ) + .await?; + + println!("{:#?}", res.actor); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/actor/list.rs b/packages/toolchain/cli/src/commands/actor/list.rs new file mode 100644 index 0000000000..7feb6770cb --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/list.rs @@ -0,0 +1,48 @@ +use anyhow::*; +use clap::Parser; +use std::collections::HashMap; +use toolchain::rivet_api::apis; + +#[derive(Parser)] +pub struct Opts { + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long)] + tags: Option, + + #[clap(long)] + include_destroyed: bool, + + #[clap(long)] + cursor: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + // Parse tags + let tags = self + .tags + .as_ref() + .map(|tags_str| kv_str::from_str::>(tags_str)) + .transpose()?; + let tags_json = tags.map(|t| serde_json::to_string(&t)).transpose()?; + + let res = apis::actor_api::actor_list( + &ctx.openapi_config_cloud, + Some(&ctx.project.name_id), + Some(&env), + tags_json.as_deref(), + Some(self.include_destroyed), + self.cursor.as_deref(), + ) + .await?; + + println!("{:#?}", res.actors); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/actor/logs.rs b/packages/toolchain/cli/src/commands/actor/logs.rs new file mode 100644 index 0000000000..35c76085e9 --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/logs.rs @@ -0,0 +1,50 @@ +use anyhow::*; +use clap::Parser; +use toolchain::errors; +use uuid::Uuid; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1)] + id: String, + + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long, short = 's')] + stream: Option, + + #[clap(long)] + no_timestamps: bool, + + #[clap(long)] + no_follow: bool, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let actor_id = + Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?; + + crate::util::actor::logs::tail( + &ctx, + crate::util::actor::logs::TailOpts { + environment: &env, + actor_id, + stream: self + .stream + .clone() + .unwrap_or(crate::util::actor::logs::LogStream::All), + follow: !self.no_follow, + timestamps: !self.no_timestamps, + }, + ) + .await?; + + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/actor/mod.rs b/packages/toolchain/cli/src/commands/actor/mod.rs new file mode 100644 index 0000000000..fd906f597c --- /dev/null +++ b/packages/toolchain/cli/src/commands/actor/mod.rs @@ -0,0 +1,29 @@ +pub mod create; +pub mod destroy; +pub mod get; +pub mod list; +pub mod logs; + +use anyhow::*; +use clap::Subcommand; + +#[derive(Subcommand)] +pub enum SubCommand { + Create(create::Opts), + Get(get::Opts), + Destroy(destroy::Opts), + List(list::Opts), + Logs(logs::Opts), +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match &self { + SubCommand::Create(opts) => opts.execute().await, + SubCommand::Get(opts) => opts.execute().await, + SubCommand::Destroy(opts) => opts.execute().await, + SubCommand::List(opts) => opts.execute().await, + SubCommand::Logs(opts) => opts.execute().await, + } + } +} diff --git a/packages/toolchain/cli/src/commands/build/get.rs b/packages/toolchain/cli/src/commands/build/get.rs new file mode 100644 index 0000000000..7f474b846b --- /dev/null +++ b/packages/toolchain/cli/src/commands/build/get.rs @@ -0,0 +1,35 @@ +use anyhow::*; +use clap::Parser; +use toolchain::{errors, rivet_api::apis}; +use uuid::Uuid; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1)] + id: String, + + #[clap(long, alias = "env", short = 'e')] + environment: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let build_id = + Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?; + + let res = apis::actor_builds_api::actor_builds_get( + &ctx.openapi_config_cloud, + &build_id.to_string(), + Some(&ctx.project.name_id), + Some(&env), + ) + .await?; + + println!("{:#?}", res.build); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/build/list.rs b/packages/toolchain/cli/src/commands/build/list.rs new file mode 100644 index 0000000000..3ffb43a7a9 --- /dev/null +++ b/packages/toolchain/cli/src/commands/build/list.rs @@ -0,0 +1,40 @@ +use anyhow::*; +use clap::Parser; +use std::collections::HashMap; +use toolchain::rivet_api::apis; + +#[derive(Parser)] +pub struct Opts { + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long)] + tags: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + // Parse tags + let tags = self + .tags + .as_ref() + .map(|tags_str| kv_str::from_str::>(tags_str)) + .transpose()?; + let tags_json = tags.map(|t| serde_json::to_string(&t)).transpose()?; + + let res = apis::actor_builds_api::actor_builds_list( + &ctx.openapi_config_cloud, + Some(&ctx.project.name_id), + Some(&env), + tags_json.as_deref(), + ) + .await?; + + println!("{:#?}", res.builds); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/build/mod.rs b/packages/toolchain/cli/src/commands/build/mod.rs new file mode 100644 index 0000000000..0a0c3041fc --- /dev/null +++ b/packages/toolchain/cli/src/commands/build/mod.rs @@ -0,0 +1,23 @@ +use anyhow::*; +use clap::Subcommand; + +mod get; +mod list; +mod patch_tags; + +#[derive(Subcommand)] +pub enum SubCommand { + Get(get::Opts), + List(list::Opts), + PatchTags(patch_tags::Opts), +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match &self { + SubCommand::Get(opts) => opts.execute().await, + SubCommand::List(opts) => opts.execute().await, + SubCommand::PatchTags(opts) => opts.execute().await, + } + } +} diff --git a/packages/toolchain/cli/src/commands/build/patch_tags.rs b/packages/toolchain/cli/src/commands/build/patch_tags.rs new file mode 100644 index 0000000000..203543630a --- /dev/null +++ b/packages/toolchain/cli/src/commands/build/patch_tags.rs @@ -0,0 +1,54 @@ +use anyhow::*; +use clap::Parser; +use std::collections::HashMap; +use toolchain::rivet_api::{apis, models}; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1)] + build: String, + + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(short = 't', long = "tag")] + tags: Option, + + #[clap(short = 'e', long = "exclusive-tags")] + exclusive_tags: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + // Parse tags + let tags = self + .tags + .as_ref() + .map(|tags_str| kv_str::from_str::>(tags_str)) + .transpose()?; + let exclusive_tags = self.exclusive_tags.as_ref().map(|x| { + x.split(",") + .map(|x| x.trim().to_string()) + .collect::>() + }); + + apis::actor_builds_api::actor_builds_patch_tags( + &ctx.openapi_config_cloud, + &self.build, + models::ActorPatchBuildTagsRequest { + tags: tags.map(|x| serde_json::json!(x)), + exclusive_tags, + }, + Some(&ctx.project.name_id), + Some(&env), + ) + .await?; + + println!("Patched tags"); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/deploy.rs b/packages/toolchain/cli/src/commands/deploy.rs new file mode 100644 index 0000000000..ab490394e9 --- /dev/null +++ b/packages/toolchain/cli/src/commands/deploy.rs @@ -0,0 +1,35 @@ +use anyhow::*; +use clap::Parser; +use std::collections::HashMap; + +#[derive(Parser)] +pub struct Opts { + #[clap(long, alias = "env", short = 'e')] + environment: Option, + + #[clap(long, short = 't')] + tags: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let build_tags = self + .tags + .as_ref() + .map(|b| kv_str::from_str::>(b)) + .transpose() + .context("Failed to parse build tags")?; + + crate::util::deploy::deploy(crate::util::deploy::DeployOpts { + environment: &env, + build_tags, + }) + .await?; + + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/environment/mod.rs b/packages/toolchain/cli/src/commands/environment/mod.rs new file mode 100644 index 0000000000..d832323abd --- /dev/null +++ b/packages/toolchain/cli/src/commands/environment/mod.rs @@ -0,0 +1,18 @@ +use anyhow::*; +use clap::Subcommand; + +mod select; + +#[derive(Subcommand)] +pub enum SubCommand { + #[clap(alias = "s")] + Select(select::Opts), +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match &self { + SubCommand::Select(opts) => opts.execute().await, + } + } +} diff --git a/packages/toolchain/cli/src/commands/environment/select.rs b/packages/toolchain/cli/src/commands/environment/select.rs new file mode 100644 index 0000000000..0fc2d3e0ff --- /dev/null +++ b/packages/toolchain/cli/src/commands/environment/select.rs @@ -0,0 +1,13 @@ +use anyhow::*; +use clap::Parser; + +#[derive(Parser)] +pub struct Opts {} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + crate::util::env::select(&ctx, true).await?; + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/init.rs b/packages/toolchain/cli/src/commands/init.rs new file mode 100644 index 0000000000..c6e86aec0f --- /dev/null +++ b/packages/toolchain/cli/src/commands/init.rs @@ -0,0 +1,229 @@ +use anyhow::*; +use clap::Parser; +use inquire::validator::Validation; +use serde::Serialize; +use std::{fmt, result::Result as StdResult}; +use tokio::fs; +use toolchain::errors; + +/// Initiate a new project +#[derive(Parser, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Opts {} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + // Check if project already exists + if let Result::Ok(path) = toolchain::config::Config::config_path(None).await { + return Err(errors::UserError::new(format!( + "Rivet config already exists at {}", + path.display() + )) + .into()); + } + + // Prompt init settings + let prompt = tokio::task::spawn_blocking(|| { + let project_name = inquire::Text::new("What is your project name?") + .with_default("my-app") + .with_validator(|input: &str| { + let is_valid = input + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_'); + if is_valid { + StdResult::Ok(Validation::Valid) + } else { + StdResult::Ok(Validation::Invalid( + "Project name must be alphanumeric and can include '-' or '_'".into(), + )) + } + }) + .prompt()?; + + let lang = inquire::Select::new( + "What language will you write your Rivet Actor in?", + vec![Language::TypeScript, Language::JavaScript, Language::Docker], + ) + .with_starting_cursor(0) + .with_help_message( + "This can be changed later. Multiple languages can be used in the same project.", + ) + .prompt()?; + + let config_format = inquire::Select::new( + "What config format do you prefer?", + vec![ConfigFormat::Json, ConfigFormat::Jsonc], + ) + .with_starting_cursor(0) + .prompt()?; + + let login = inquire::Confirm::new("Would you like to log in to Rivet now?") + .with_default(true) + .with_help_message( + "This is required to deploy. You can run this later with `rivet login`.", + ) + .prompt()?; + + let api_endpoint = if login { + crate::util::login::inquire_self_hosting()? + } else { + None + }; + + Ok(PromptOutput { + project_name, + lang, + config_format, + login, + api_endpoint, + }) + }) + .await??; + + let project_path = std::env::current_dir()?.join(&prompt.project_name); + + println!(); + println!("Creating new Rivet project at {}", project_path.display()); + + fs::create_dir(&project_path) + .await + .map_err(|err| anyhow!("failed to create project dir: {err}"))?; + + // Generate config + let config = match prompt.lang { + Language::TypeScript | Language::JavaScript => { + // Write Deno config + let deno_config = include_str!("../../static/init/js/deno.json"); + let deno_config_name = match prompt.config_format { + ConfigFormat::Json => "deno.json", + ConfigFormat::Jsonc => "deno.jsonc", + }; + fs::write(project_path.join(deno_config_name), deno_config).await?; + + // Write script + let (script_name, script_body) = match prompt.lang { + Language::TypeScript => { + ("actor.ts", include_str!("../../static/init/js/actor.ts")) + } + Language::JavaScript => { + ("actor.js", include_str!("../../static/init/js/actor.js")) + } + _ => unreachable!(), + }; + fs::write(project_path.join(script_name), script_body).await?; + + // Generate config + let config = include_str!("../../static/init/js/rivet.json") + .replace("__NAME__", &prompt.project_name) + .replace("__SCRIPT__", &script_name); + + config + } + Language::Docker => { + // Write Dockerfile + let dockerfile_body = include_str!("../../static/init/docker/Dockerfile"); + fs::write(project_path.join("Dockerfile"), dockerfile_body).await?; + + // Generate config + let config = include_str!("../../static/init/docker/rivet.json") + .replace("__NAME__", &prompt.project_name); + + config + } + }; + + // Create JSON config + let config_name = match prompt.config_format { + ConfigFormat::Json => "rivet.json", + ConfigFormat::Jsonc => "rivet.jsonc", + }; + fs::write(project_path.join(config_name), config).await?; + + println!("Project created successfully."); + + // Login to Rivet + if prompt.login { + println!(); + println!("Loggin in to Rivet..."); + crate::util::login::login(prompt.api_endpoint.clone()).await?; + } + + println!(); + println!(); + println!(" ========== Welcome to Rivet! =========="); + println!(); + println!("Next steps:"); + println!(); + println!(" $ cd {}", prompt.project_name); + println!(" $ rivet deploy"); + println!(); + println!("Resources:"); + println!(); + println!(" Quickstart: https://rivet.gg/docs/quickstart"); + println!(" Examples: https://rivet.gg/docs/examples"); + println!(" Discord: https://rivet.gg/discord"); + println!(" Enterprise: https://rivet.gg/sales"); + println!(); + + crate::util::telemetry::capture_event( + "cli_init", + Some(|event: &mut async_posthog::Event| { + event.insert_prop( + "$set", + serde_json::json!({ + "cli_init_prompt": prompt, + }), + )?; + Ok(()) + }), + ) + .await; + + Ok(()) + } +} + +#[derive(Serialize)] +struct PromptOutput { + project_name: String, + lang: Language, + config_format: ConfigFormat, + login: bool, + api_endpoint: Option, +} + +#[derive(Serialize)] +enum Language { + TypeScript, + JavaScript, + Docker, +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match self { + Language::TypeScript => "TypeScript", + Language::JavaScript => "JavaScript", + Language::Docker => "Other (Docker)", + }; + write!(f, "{}", output) + } +} + +#[derive(Serialize)] +enum ConfigFormat { + Json, + Jsonc, +} + +impl fmt::Display for ConfigFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match self { + ConfigFormat::Json => "JSON (rivet.json)", + ConfigFormat::Jsonc => "JSON with comments (rivet.jsonc)", + // ConfigFormat::Json => "rivet.json (vanilla JSON)", + // ConfigFormat::Jsonc => "rivet.jsonc (JSON with comments)", + }; + write!(f, "{}", output) + } +} diff --git a/packages/toolchain/cli/src/commands/login.rs b/packages/toolchain/cli/src/commands/login.rs new file mode 100644 index 0000000000..5a85e7970c --- /dev/null +++ b/packages/toolchain/cli/src/commands/login.rs @@ -0,0 +1,24 @@ +use anyhow::*; +use clap::Parser; + + +/// Login to a project +#[derive(Parser)] +pub struct Opts { + #[clap(long)] + api_endpoint: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let api_endpoint = if let Some(e) = &self.api_endpoint { + Some(e.clone()) + } else { + tokio::task::spawn_blocking(|| crate::util::login::inquire_self_hosting()).await?? + }; + + crate::util::login::login(api_endpoint).await?; + + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/logout.rs b/packages/toolchain/cli/src/commands/logout.rs new file mode 100644 index 0000000000..656de7e3a6 --- /dev/null +++ b/packages/toolchain/cli/src/commands/logout.rs @@ -0,0 +1,21 @@ +use anyhow::*; +use clap::Parser; +use toolchain::tasks; + +use crate::util::task::{run_task, TaskOutputStyle}; + +/// Logout from a game +#[derive(Parser)] +pub struct Opts {} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + run_task::( + TaskOutputStyle::None, + tasks::auth::sign_out::Input {}, + ) + .await?; + eprintln!("Logged out"); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/metadata/mod.rs b/packages/toolchain/cli/src/commands/metadata/mod.rs new file mode 100644 index 0000000000..aa6db4292f --- /dev/null +++ b/packages/toolchain/cli/src/commands/metadata/mod.rs @@ -0,0 +1,24 @@ +use anyhow::*; +use clap::Subcommand; +use toolchain::paths; + +#[derive(Subcommand)] +pub enum SubCommand { + Path, +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match self { + SubCommand::Path => { + println!( + "{}", + paths::project_data_dir(&paths::data_dir().expect("data_dir")) + .expect("project_data_dir") + .display() + ); + Ok(()) + } + } + } +} diff --git a/packages/toolchain/cli/src/commands/mod.rs b/packages/toolchain/cli/src/commands/mod.rs new file mode 100644 index 0000000000..8ca71b8517 --- /dev/null +++ b/packages/toolchain/cli/src/commands/mod.rs @@ -0,0 +1,63 @@ +pub mod actor; +pub mod build; +pub mod deploy; +pub mod environment; +pub mod init; +pub mod login; +pub mod logout; +pub mod metadata; +pub mod region; + +use anyhow::*; +use clap::Parser; + +#[derive(Parser)] +pub enum SubCommand { + Init(init::Opts), + #[clap(alias = "signin")] + Login(login::Opts), + #[clap(alias = "signout")] + Logout(logout::Opts), + #[clap(alias = "d")] + Deploy(deploy::Opts), + #[clap(alias = "e", alias = "env")] + Environment { + #[clap(subcommand)] + subcommand: environment::SubCommand, + }, + #[clap(alias = "a")] + Actor { + #[clap(subcommand)] + subcommand: actor::SubCommand, + }, + #[clap(alias = "b")] + Build { + #[clap(subcommand)] + subcommand: build::SubCommand, + }, + Region { + #[clap(subcommand)] + subcommand: region::SubCommand, + }, + #[clap(alias = "meta")] + Metadata { + #[clap(subcommand)] + subcommand: metadata::SubCommand, + }, +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match self { + SubCommand::Init(opts) => opts.execute().await, + SubCommand::Login(opts) => opts.execute().await, + SubCommand::Logout(opts) => opts.execute().await, + SubCommand::Deploy(opts) => opts.execute().await, + SubCommand::Environment { subcommand } => subcommand.execute().await, + SubCommand::Actor { subcommand } => subcommand.execute().await, + SubCommand::Build { subcommand } => subcommand.execute().await, + SubCommand::Region { subcommand } => subcommand.execute().await, + SubCommand::Metadata { subcommand } => subcommand.execute().await, + } + } +} diff --git a/packages/toolchain/cli/src/commands/region/list.rs b/packages/toolchain/cli/src/commands/region/list.rs new file mode 100644 index 0000000000..7e790c5d13 --- /dev/null +++ b/packages/toolchain/cli/src/commands/region/list.rs @@ -0,0 +1,28 @@ +use anyhow::*; +use clap::Parser; +use toolchain::rivet_api::apis; + +#[derive(Parser)] +pub struct Opts { + #[clap(long, alias = "env", short = 'e')] + environment: Option, +} + +impl Opts { + pub async fn execute(&self) -> Result<()> { + let ctx = toolchain::toolchain_ctx::load().await?; + + let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; + + let res = apis::actor_regions_api::actor_regions_list( + &ctx.openapi_config_cloud, + Some(&ctx.project.name_id), + Some(&env), + ) + .await + .context("Failed to list regions")?; + + println!("{:#?}", res.regions); + Ok(()) + } +} diff --git a/packages/toolchain/cli/src/commands/region/mod.rs b/packages/toolchain/cli/src/commands/region/mod.rs new file mode 100644 index 0000000000..75f2b9d33b --- /dev/null +++ b/packages/toolchain/cli/src/commands/region/mod.rs @@ -0,0 +1,17 @@ +use anyhow::*; +use clap::Subcommand; + +mod list; + +#[derive(Subcommand)] +pub enum SubCommand { + List(list::Opts), +} + +impl SubCommand { + pub async fn execute(&self) -> Result<()> { + match &self { + SubCommand::List(opts) => opts.execute().await, + } + } +} diff --git a/packages/toolchain/cli/src/main.rs b/packages/toolchain/cli/src/main.rs new file mode 100644 index 0000000000..3303058c15 --- /dev/null +++ b/packages/toolchain/cli/src/main.rs @@ -0,0 +1,98 @@ +pub mod commands; +pub mod util; + +use clap::{builder::styling, Parser}; +use std::process::ExitCode; +use toolchain::errors; + +const STYLES: styling::Styles = styling::Styles::styled() + .header(styling::AnsiColor::Red.on_default().bold()) + .usage(styling::AnsiColor::Red.on_default().bold()) + .literal(styling::AnsiColor::White.on_default().bold()) + .placeholder(styling::AnsiColor::White.on_default()); + +#[derive(Parser)] +#[clap( + author = "Rivet Gaming, Inc. ", + about = "https://rivet.gg/", + version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("VERGEN_GIT_SHA"), ")"), + long_version = concat!( + "\n\n", + "git sha: ", env!("VERGEN_GIT_SHA"), "\n", + "git branch: ", env!("VERGEN_GIT_BRANCH"), "\n", + "build semver: ", env!("CARGO_PKG_VERSION"), "\n", + "build timestamp: ", env!("VERGEN_BUILD_TIMESTAMP"), "\n", + "build target: ", env!("VERGEN_CARGO_TARGET_TRIPLE"), "\n", + "build debug: ", env!("VERGEN_CARGO_DEBUG"), "\n", + "rustc version: ", env!("VERGEN_RUSTC_SEMVER"), + ), + styles = STYLES +)] + +struct Cli { + #[command(subcommand)] + command: commands::SubCommand, +} + +fn main() -> ExitCode { + // We use a sync main for Sentry. Read more: https://docs.sentry.io/platforms/rust/#async-main-function + + // This has a 2 second deadline to flush any remaining events which is sufficient for + // short-lived commands. + let _guard = sentry::init(("https://b329eb15c63e1002611fb3b7a58a1dfa@o4504307129188352.ingest.us.sentry.io/4508361147809792", sentry::ClientOptions { + release: sentry::release_name!(), + ..Default::default() +})); + + // Run main + let exit_code = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { main_async().await }); + + exit_code +} + +async fn main_async() -> ExitCode { + let cli = Cli::parse(); + let exit_code = match cli.command.execute().await { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + // TODO(TOOL-438): Catch 400 API errors as user errors + if err.is::() || err.is::() { + // Don't print anything, already handled + } else if let Some(err) = err.downcast_ref::() { + // Don't report error since this is a user error + eprintln!("\n{err}"); + } else { + // This is an internal error, report error + eprintln!("\n{err}"); + report_error(err).await; + } + + ExitCode::FAILURE + } + }; + + // Wait for telemetry to publish + util::telemetry::wait_all().await; + + exit_code +} + +async fn report_error(err: anyhow::Error) { + let event_id = sentry::integrations::anyhow::capture_anyhow(&err); + + // Capture event in PostHog + util::telemetry::capture_event( + "$exception", + Some(|event: &mut async_posthog::Event| { + event.insert_prop("errors", format!("{}", err))?; + event.insert_prop("$sentry_event_id", event_id.to_string())?; + event.insert_prop("$sentry_url", format!("https://sentry.io/organizations/rivet-gaming/issues/?project=4508361147809792&query={event_id}"))?; + Ok(()) + }), + ) + .await; +} diff --git a/packages/toolchain/cli/src/util/actor/logs.rs b/packages/toolchain/cli/src/util/actor/logs.rs new file mode 100644 index 0000000000..929de19f34 --- /dev/null +++ b/packages/toolchain/cli/src/util/actor/logs.rs @@ -0,0 +1,173 @@ +use anyhow::*; +use base64::{engine::general_purpose::STANDARD, Engine}; +use clap::ValueEnum; +use std::time::Duration; +use tokio::signal; +use tokio::sync::watch; +use toolchain::rivet_api::{apis, models}; +use uuid::Uuid; + +#[derive(ValueEnum, Clone)] +pub enum LogStream { + #[clap(name = "all")] + All, + #[clap(name = "stdout")] + StdOut, + #[clap(name = "stderr")] + StdErr, +} + +pub struct TailOpts<'a> { + pub environment: &'a str, + pub actor_id: Uuid, + pub stream: LogStream, + pub follow: bool, + pub timestamps: bool, +} + +/// Reads the logs of an actor. +pub async fn tail(ctx: &toolchain::ToolchainCtx, opts: TailOpts<'_>) -> Result<()> { + let (stdout_fetched_tx, stdout_fetched_rx) = watch::channel(false); + let (stderr_fetched_tx, stderr_fetched_rx) = watch::channel(false); + + tokio::select! { + result = tail_streams(ctx, &opts, stdout_fetched_tx, stderr_fetched_tx) => result, + result = poll_actor_state(ctx, &opts, stdout_fetched_rx, stderr_fetched_rx) => result, + _ = signal::ctrl_c() => { + Ok(()) + } + } +} + +/// Reads the streams of an actor's logs. +async fn tail_streams( + ctx: &toolchain::ToolchainCtx, + opts: &TailOpts<'_>, + stdout_fetched_tx: watch::Sender, + stderr_fetched_tx: watch::Sender, +) -> Result<()> { + tokio::try_join!( + tail_stream( + ctx, + &opts, + models::ActorLogStream::StdOut, + stdout_fetched_tx + ), + tail_stream( + ctx, + &opts, + models::ActorLogStream::StdErr, + stderr_fetched_tx + ), + ) + .map(|_| ()) +} + +/// Reads a specific stream of an actor's log. +async fn tail_stream( + ctx: &toolchain::ToolchainCtx, + opts: &TailOpts<'_>, + stream: models::ActorLogStream, + log_fetched_tx: watch::Sender, +) -> Result<()> { + let mut watch_index: Option = None; + let mut first_batch_fetched = false; + + // Check if this stream is intended to be polled. If not, sleep indefinitely so the other + // future doesn't exit. + match (&opts.stream, stream) { + (LogStream::All, _) => {} + (LogStream::StdOut, models::ActorLogStream::StdOut) => {} + (LogStream::StdErr, models::ActorLogStream::StdErr) => {} + _ => { + // Notify poll_actor_state + log_fetched_tx.send(true).ok(); + + // Do nothing + return Ok(()); + } + } + + loop { + let res = apis::actor_logs_api::actor_logs_get( + &ctx.openapi_config_cloud, + &opts.actor_id.to_string(), + stream, + Some(&ctx.project.name_id), + Some(opts.environment), + watch_index.as_deref(), + ) + .await + .map_err(|err| anyhow!("Failed to fetch logs: {err}"))?; + watch_index = Some(res.watch.index); + + if !first_batch_fetched { + log_fetched_tx.send(true).ok(); + first_batch_fetched = true; + } + + for (ts, line) in res.timestamps.iter().zip(res.lines.iter()) { + let decoded_line = match STANDARD.decode(line) { + Result::Ok(bytes) => String::from_utf8_lossy(&bytes).to_string(), + Err(_) => { + eprintln!("Failed to decode base64: {line}"); + continue; + } + }; + + if opts.timestamps { + println!("{ts} {decoded_line}"); + } else { + println!("{decoded_line}"); + } + } + + if !opts.follow { + break; + } + } + + Ok(()) +} + +/// Polls the actor state. Exits when finished. +/// +/// Using this in a `tokio::select` will make all other tasks cancel when the actor finishes. +async fn poll_actor_state( + ctx: &toolchain::ToolchainCtx, + opts: &TailOpts<'_>, + mut stdout_fetched_rx: watch::Receiver, + mut stderr_fetched_rx: watch::Receiver, +) -> Result<()> { + // Never resolve if not following this actor in order to just print logs + if !opts.follow { + return std::future::pending().await; + } + + // Wait for the first batch of logs to be fetched before polling actor state. + // + // This way, if fetching the logs of an actor, we don't abort the logs until logs have been + // successfully printed. + stdout_fetched_rx.changed().await.ok(); + stderr_fetched_rx.changed().await.ok(); + + // Poll actor state to shut down when actor finishes + let mut interval = tokio::time::interval(Duration::from_millis(2_500)); + loop { + interval.tick().await; + + let res = apis::actor_api::actor_get( + &ctx.openapi_config_cloud, + &opts.actor_id.to_string(), + Some(&ctx.project.name_id), + Some(opts.environment), + ) + .await + .map_err(|err| anyhow!("Failed to poll actor: {err}"))?; + + if res.actor.destroyed_at.is_some() { + println!("Actor finished"); + return Ok(()); + } + } +} diff --git a/packages/toolchain/cli/src/util/actor/mod.rs b/packages/toolchain/cli/src/util/actor/mod.rs new file mode 100644 index 0000000000..af2c2c342f --- /dev/null +++ b/packages/toolchain/cli/src/util/actor/mod.rs @@ -0,0 +1 @@ +pub mod logs; diff --git a/packages/toolchain/cli/src/util/deploy.rs b/packages/toolchain/cli/src/util/deploy.rs new file mode 100644 index 0000000000..48b0941cdb --- /dev/null +++ b/packages/toolchain/cli/src/util/deploy.rs @@ -0,0 +1,57 @@ +use anyhow::*; +use std::collections::HashMap; +use toolchain::{ + errors, + tasks::{deploy, get_bootstrap_data}, +}; +use uuid::Uuid; + +use crate::util::task::{run_task, TaskOutputStyle}; + +pub struct DeployOpts<'a> { + pub environment: &'a str, + pub build_tags: Option>, +} + +pub async fn deploy(opts: DeployOpts<'_>) -> Result> { + let bootstrap_data = + run_task::(TaskOutputStyle::None, get_bootstrap_data::Input {}) + .await?; + let Some(cloud_data) = bootstrap_data.cloud else { + eprintln!("Not signed in. Please run `rivet login`."); + return Err(errors::GracefulExit.into()); + }; + + // Find environment + let environment = match cloud_data + .envs + .iter() + .find(|env| env.slug == opts.environment) + { + Some(env) => env, + None => { + eprintln!( + "Environment '{}' not found. Available environments:", + opts.environment + ); + for env in &cloud_data.envs { + eprintln!("- {}", env.slug); + } + return Err(errors::GracefulExit.into()); + } + }; + + let config = toolchain::config::Config::load(None).await?; + + let build = run_task::( + TaskOutputStyle::PlainNoResult, + deploy::Input { + config, + environment_id: environment.id, + build_tags: opts.build_tags, + }, + ) + .await?; + + Ok(build.build_ids) +} diff --git a/packages/toolchain/cli/src/util/env.rs b/packages/toolchain/cli/src/util/env.rs new file mode 100644 index 0000000000..183588480e --- /dev/null +++ b/packages/toolchain/cli/src/util/env.rs @@ -0,0 +1,99 @@ +use anyhow::*; +use toolchain::{ + meta, paths, + tasks::{self}, +}; +use uuid::Uuid; + +use crate::util::task::{run_task, TaskOutputStyle}; + +/// Select an environment or use the provided env. +pub async fn get_or_select( + ctx: &toolchain::ToolchainCtx, + slug: Option, +) -> Result { + // Return configured env + if let Some(slug) = slug { + return Ok(slug.to_string()); + }; + + // Read the selected env + let selected_env_id = + meta::try_read_project(&paths::data_dir()?, |p| Ok(p.cloud()?.selected_environment)) + .await?; + if let Some(env) = ctx + .project + .namespaces + .iter() + .find(|x| Some(x.namespace_id) == selected_env_id) + { + return Ok(env.name_id.clone()); + } + + // Prompt user for selection + select(ctx, false).await +} + +/// Select an environment. +/// +/// Forcing selection will prompt the user for selection, even if there's only 1 item. +pub async fn select(ctx: &toolchain::ToolchainCtx, force_select: bool) -> Result { + // Build selections + let mut envs = ctx + .project + .namespaces + .iter() + .map(|n| EnvWrapper { + id: n.namespace_id.clone(), + slug: n.name_id.clone(), + name: n.display_name.clone(), + }) + .collect::>(); + envs.sort_by_key(|e| e.name.clone()); + + // If only one env, don't prompt + if !force_select && envs.len() == 1 { + let env = envs.into_iter().next().expect("should have 1 env value"); + return Ok(env.slug); + } + + // Choose starting index + let start_env_id = + meta::try_read_project(&paths::data_dir()?, |p| Ok(p.cloud()?.selected_environment)) + .await?; + let start_idx = envs + .iter() + .position(|e| Some(e.id) == start_env_id) + .unwrap_or(0); + + // Prompt + let selected = tokio::task::block_in_place(|| { + inquire::Select::new("Environment", envs) + .with_starting_cursor(start_idx) + .prompt() + })?; + + // Update settings + run_task::( + TaskOutputStyle::None, + tasks::env::select::Input { + environment_id: selected.id, + }, + ) + .await?; + + Ok(selected.slug.clone()) +} + +/// Struct used for wrapping data in selector. +struct EnvWrapper { + id: Uuid, + slug: String, + name: String, +} + +impl std::fmt::Display for EnvWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} ({})", self.name, self.slug) + } +} diff --git a/packages/toolchain/cli/src/util/login.rs b/packages/toolchain/cli/src/util/login.rs new file mode 100644 index 0000000000..ac48b86166 --- /dev/null +++ b/packages/toolchain/cli/src/util/login.rs @@ -0,0 +1,90 @@ +use anyhow::Result; +use inquire::validator::Validation; +use std::result::Result as StdResult; +use toolchain::tasks; + +use crate::util::{ + os, + task::{run_task, TaskOutputStyle}, +}; + +pub fn inquire_self_hosting() -> Result> { + let self_hosting = inquire::Confirm::new("Are you self-hosting Rivet?") + .with_default(false) + .prompt()?; + + let api_endpoint = if self_hosting { + let e = inquire::Text::new("What is the API endpoint?") + .with_default("http://localhost:8080") + .with_validator(|input: &str| match url::Url::parse(input) { + Result::Ok(_) => StdResult::Ok(Validation::Valid), + Err(err) => StdResult::Ok(Validation::Invalid(format!("{err}").into())), + }) + .prompt()?; + + Some(e) + } else { + None + }; + + Ok(api_endpoint) +} + +pub async fn login(api_endpoint: Option) -> Result<()> { + let api_endpoint = api_endpoint.unwrap_or_else(|| "https://api.rivet.gg".to_string()); + + // Check if linked + let output = run_task::( + TaskOutputStyle::None, + tasks::auth::check_state::Input {}, + ) + .await?; + if output.signed_in { + eprintln!("Already logged in. Log out with `rivet logout`."); + return Ok(()); + } + + // Start device link + let device_link_output = run_task::( + TaskOutputStyle::None, + tasks::auth::start_sign_in::Input { + api_endpoint: api_endpoint.clone(), + }, + ) + .await?; + + // Open link in browser + // + // Linux root users often cannot open the browser, so we fallback to printing the URL + if !os::is_linux_and_root() + && webbrowser::open_browser_with_options( + webbrowser::Browser::Default, + &device_link_output.device_link_url, + webbrowser::BrowserOptions::new().with_suppress_output(true), + ) + .is_ok() + { + println!( + "Waiting for browser...\n\nIf browser did not open, open this URL to login:\n{}", + device_link_output.device_link_url + ); + } else { + println!( + "Open this URL to login:\n{}", + device_link_output.device_link_url + ); + } + + // Wait for finish + run_task::( + TaskOutputStyle::None, + tasks::auth::wait_for_sign_in::Input { + api_endpoint: api_endpoint.clone(), + device_link_token: device_link_output.device_link_token, + }, + ) + .await?; + eprintln!("Logged in"); + + Ok(()) +} diff --git a/packages/toolchain/cli/src/util/mod.rs b/packages/toolchain/cli/src/util/mod.rs new file mode 100644 index 0000000000..3feafda32a --- /dev/null +++ b/packages/toolchain/cli/src/util/mod.rs @@ -0,0 +1,7 @@ +pub mod actor; +pub mod deploy; +pub mod env; +pub mod login; +pub mod os; +pub mod task; +pub mod telemetry; diff --git a/packages/toolchain/cli/src/util/os.rs b/packages/toolchain/cli/src/util/os.rs new file mode 100644 index 0000000000..ed2d64e270 --- /dev/null +++ b/packages/toolchain/cli/src/util/os.rs @@ -0,0 +1,18 @@ +pub fn is_linux() -> bool { + std::env::consts::OS == "linux" +} + +#[cfg(target_os = "linux")] +pub fn is_root() -> bool { + nix::unistd::Uid::current().is_root() +} + +#[cfg(not(target_os = "linux"))] +pub fn is_root() -> bool { + false +} + +/// There are a lot of edge cases for Linux root that we need to frequently handle. +pub fn is_linux_and_root() -> bool { + is_linux() && is_root() +} diff --git a/packages/toolchain/cli/src/util/task.rs b/packages/toolchain/cli/src/util/task.rs new file mode 100644 index 0000000000..e3174b0241 --- /dev/null +++ b/packages/toolchain/cli/src/util/task.rs @@ -0,0 +1,162 @@ +use anyhow::*; +use std::{io::Write, process::ExitCode}; +use tokio::{ + sync::{broadcast, mpsc}, + task::block_in_place, +}; +use toolchain::util::task::{self, TaskEvent}; + +/// Runs a task in a CLI-friendly way. +pub async fn run_task(output_style: TaskOutputStyle, input: T::Input) -> Result +where + T: task::Task, +{ + let (run_config, handles) = task::RunConfig::build(); + + // Spawn aborter + tokio::spawn(abort_handler(handles.abort_tx, handles.shutdown_rx)); + + // Spawn event handler + let event_join_handle = tokio::spawn(event_handler(handles.event_rx, output_style)); + + // Run task + let result = task::run_task::(run_config, input).await; + + // Wait for logger to shut down + match event_join_handle.await { + Result::Ok(_) => {} + Err(err) => eprintln!("error waiting for event handle: {err:?}"), + }; + + result +} + +/// Runs a task and returns an exit code. Useful for commands that are a thin wrapper around a +/// task. +pub async fn run_task_simple(input: T::Input) -> ExitCode +where + T: task::Task, +{ + match run_task::(TaskOutputStyle::PlainNoResult, input).await { + Result::Ok(_) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("Error: {e:?}"); + ExitCode::from(1) + } + } +} + +pub async fn run_task_json( + output_style: TaskOutputStyle, + name: &str, + input_json: &str, +) -> Result { + let (run_config, handles) = task::RunConfig::build(); + + // Spawn aborter + tokio::spawn(abort_handler(handles.abort_tx, handles.shutdown_rx)); + + // Spawn event handler + let event_join_handle = tokio::spawn(event_handler(handles.event_rx, output_style)); + + // Run task + let result = toolchain::tasks::run_task_json(run_config, name, input_json).await; + + // Wait for logger to shut down + match event_join_handle.await { + Result::Ok(_) => {} + Err(err) => eprintln!("error waiting for event handle: {err:?}"), + }; + + Ok(result) +} + +/// Handles aborting the task. +async fn abort_handler(abort_tx: mpsc::Sender<()>, mut shutdown_rx: broadcast::Receiver<()>) { + tokio::select! { + result = tokio::signal::ctrl_c() => { + match result { + Result::Ok(_) => {} + Err(err) => { + eprintln!("error waiting for ctrl c: {err:?}"); + } + } + + // Abort task + let _ = abort_tx.send(()).await; + } + _ = shutdown_rx.recv() => { + // Stop waiting + } + } +} + +/// Handles output from the task. +async fn event_handler( + mut event_rx: mpsc::UnboundedReceiver, + output_style: TaskOutputStyle, +) { + let mut stdout = std::io::stdout(); + let mut stderr = std::io::stdout(); + while let Some(event) = event_rx.recv().await { + block_in_place(|| { + print_event(&mut stdout, &mut stderr, &event, output_style); + }); + } +} + +/// Prints an event depending on the output style. +fn print_event( + stdout: &mut impl Write, + stderr: &mut impl Write, + event: &TaskEvent, + output_style: TaskOutputStyle, +) { + match output_style { + TaskOutputStyle::None => {} + TaskOutputStyle::Json => { + if let Err(err) = serde_json::to_writer(&mut *stdout, event) { + eprintln!("failed to serialize output: {err:?}"); + } + writeln!(stdout).unwrap(); + } + TaskOutputStyle::Plain => match event { + TaskEvent::Log(x) => { + if let Err(err) = writeln!(stderr, "{x}") { + eprintln!("failed to write output: {err:?}"); + } + } + TaskEvent::Result { result } => { + if let Err(err) = writeln!(stdout, "{}", serde_json::to_string(&result).unwrap()) { + eprintln!("failed to serialize output: {err:?}"); + } + } + }, + TaskOutputStyle::PlainNoResult => match event { + TaskEvent::Log(x) => { + if let Err(err) = writeln!(stderr, "{x}") { + eprintln!("failed to write output: {err:?}"); + } + } + _ => {} + }, + } +} + +#[derive(Copy, Clone)] +pub enum TaskOutputStyle { + /// Does not output anything. + None, + /// Writes all events to stdout in JSON. + Json, + /// Writes logs to stderr and result to stdout. + Plain, + /// Writes logs to stderr but does not return result. + PlainNoResult, +} + +impl Default for TaskOutputStyle { + fn default() -> Self { + Self::Plain + } +} diff --git a/packages/toolchain/cli/src/util/telemetry.rs b/packages/toolchain/cli/src/util/telemetry.rs new file mode 100644 index 0000000000..6dc0883b3e --- /dev/null +++ b/packages/toolchain/cli/src/util/telemetry.rs @@ -0,0 +1,149 @@ +use anyhow::*; +use serde_json::json; +use sysinfo::System; +use tokio::{ + sync::{Mutex, OnceCell}, + task::JoinSet, + time::Duration, +}; +use toolchain::{meta, paths}; + +pub static JOIN_SET: OnceCell>> = OnceCell::const_new(); + +/// Get the global join set for telemetry futures. +async fn join_set() -> &'static Mutex> { + JOIN_SET + .get_or_init(|| async { Mutex::new(JoinSet::new()) }) + .await +} + +/// Waits for all telemetry events to finish. +pub async fn wait_all() { + let mut join_set = join_set().await.lock().await; + match tokio::time::timeout(Duration::from_secs(5), async move { + while join_set.join_next().await.is_some() {} + }) + .await + { + Result::Ok(_) => {} + Err(_) => { + println!("Timed out waiting for request to finish") + } + } +} + +// This API key is safe to hardcode. It will not change and is intended to be public. +const POSTHOG_API_KEY: &str = "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo"; + +fn build_client() -> async_posthog::Client { + async_posthog::client(POSTHOG_API_KEY) +} + +/// Builds a new PostHog event with associated data. +/// +/// This is slightly expensive, so it should not be used frequently. +pub async fn capture_event(name: &str, mutate: Option) +where + F: FnOnce(&mut async_posthog::Event) -> Result<()>, +{ + let capture_res = capture_event_inner(name, mutate).await; + if cfg!(debug_assertions) { + if let Err(err) = capture_res { + eprintln!("Failed to capture event in PostHog: {:?}", err); + } + } +} + +async fn capture_event_inner(name: &str, mutate: Option) -> Result<()> +where + F: FnOnce(&mut async_posthog::Event) -> Result<()>, +{ + // Check if telemetry disabled + let (toolchain_instance_id, telemetry_disabled, api_endpoint) = + meta::read_project(&paths::data_dir()?, |x| { + let api_endpoint = x.cloud.as_ref().map(|cloud| cloud.api_endpoint.clone()); + (x.toolchain_instance_id, x.telemetry_disabled, api_endpoint) + }) + .await?; + + if telemetry_disabled { + return Ok(()); + } + + // Read project ID. If not signed in or fails to reach server, then ignore. + let (project_id, project_name) = match toolchain::toolchain_ctx::try_load().await { + Result::Ok(Some(ctx)) => ( + Some(ctx.project.game_id), + Some(ctx.project.display_name.clone()), + ), + Result::Ok(None) => (None, None), + Err(_) => { + // Ignore error + (None, None) + } + }; + + let distinct_id = format!("toolchain:{toolchain_instance_id}"); + + let mut event = async_posthog::Event::new(name, &distinct_id); + + // Helps us understand what version of the CLI is being used. + let version = json!({ + "git_sha": env!("VERGEN_GIT_SHA"), + "git_branch": env!("VERGEN_GIT_BRANCH"), + "build_semver": env!("CARGO_PKG_VERSION"), + "build_timestamp": env!("VERGEN_BUILD_TIMESTAMP"), + "build_target": env!("VERGEN_CARGO_TARGET_TRIPLE"), + "build_debug": env!("VERGEN_CARGO_DEBUG"), + "rustc_version": env!("VERGEN_RUSTC_SEMVER"), + }); + + // Add properties + if let Some(project_id) = project_id { + event.insert_prop( + "$groups", + &json!({ + "project_id": project_id, + }), + )?; + } + + event.insert_prop( + "$set", + &json!({ + "name": project_name, + "toolchain_instance_id": toolchain_instance_id, + "api_endpoint": api_endpoint, + "version": version, + "project_id": project_id, + "project_root": paths::project_root()?, + "sys": { + "name": System::name(), + "kernel_version": System::kernel_version(), + "os_version": System::os_version(), + "host_name": System::host_name(), + "cpu_arch": System::cpu_arch(), + }, + }), + )?; + + event.insert_prop("api_endpoint", api_endpoint)?; + event.insert_prop("args", std::env::args().collect::>())?; + + // Customize the event properties + if let Some(mutate) = mutate { + mutate(&mut event)?; + } + + // Capture event + join_set().await.lock().await.spawn(async move { + match build_client().capture(event).await { + Result::Ok(_) => {} + Err(_) => { + // Fail silently + } + } + }); + + Ok(()) +} diff --git a/packages/toolchain/cli/static/init/docker/Dockerfile b/packages/toolchain/cli/static/init/docker/Dockerfile new file mode 100644 index 0000000000..e151d4aaf4 --- /dev/null +++ b/packages/toolchain/cli/static/init/docker/Dockerfile @@ -0,0 +1 @@ +# TODO: diff --git a/packages/toolchain/cli/static/init/docker/rivet.json b/packages/toolchain/cli/static/init/docker/rivet.json new file mode 100644 index 0000000000..8aab717c55 --- /dev/null +++ b/packages/toolchain/cli/static/init/docker/rivet.json @@ -0,0 +1,9 @@ +{ + "builds": [ + { + "tags": { "name": "__NAME__" }, + "runtime": "docker", + "dockerfile": "Dockerfile" + } + ] +} diff --git a/packages/toolchain/cli/static/init/js/actor.js b/packages/toolchain/cli/static/init/js/actor.js new file mode 100644 index 0000000000..8407c78f5a --- /dev/null +++ b/packages/toolchain/cli/static/init/js/actor.js @@ -0,0 +1,4 @@ +import Rivet from "@rivet-gg/actors-core"; + +console.log("Hello, world!"); + diff --git a/packages/toolchain/cli/static/init/js/actor.ts b/packages/toolchain/cli/static/init/js/actor.ts new file mode 100644 index 0000000000..8407c78f5a --- /dev/null +++ b/packages/toolchain/cli/static/init/js/actor.ts @@ -0,0 +1,4 @@ +import Rivet from "@rivet-gg/actors-core"; + +console.log("Hello, world!"); + diff --git a/packages/toolchain/cli/static/init/js/deno.json b/packages/toolchain/cli/static/init/js/deno.json new file mode 100644 index 0000000000..51f36d1a00 --- /dev/null +++ b/packages/toolchain/cli/static/init/js/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@rivet-gg/actors-core": "jsr:@rivet-gg/actors-core@0.0.1-rc.4" + } +} diff --git a/packages/toolchain/cli/static/init/js/rivet.json b/packages/toolchain/cli/static/init/js/rivet.json new file mode 100644 index 0000000000..a3a8bd5775 --- /dev/null +++ b/packages/toolchain/cli/static/init/js/rivet.json @@ -0,0 +1,9 @@ +{ + "builds": [ + { + "tags": { "name": "__NAME__" }, + "runtime": "javascript", + "script": "__SCRIPT__" + } + ] +} diff --git a/packages/toolchain/js-utils-embed/Cargo.toml b/packages/toolchain/js-utils-embed/Cargo.toml new file mode 100644 index 0000000000..96453ee905 --- /dev/null +++ b/packages/toolchain/js-utils-embed/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rivet-js-utils-embed" +build = "build.rs" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow = "1.0" +include_dir = "0.7.4" +tokio = { version = "1.40.0", default-features = false, features = ["fs", "rt-multi-thread"] } + +[build-dependencies] +anyhow = "1.0" +fs_extra = "1.3.0" +merkle_hash = "3.7.0" +deno-embed.workspace = true +sha2 = "0.10.8" +tempfile = "3.13.0" +tokio = { version = "1.40.0", default-features = false, features = ["fs", "rt-multi-thread"] } +walkdir = "2.5.0" diff --git a/packages/toolchain/js-utils-embed/build.rs b/packages/toolchain/js-utils-embed/build.rs new file mode 100644 index 0000000000..ef0a1ea5c9 --- /dev/null +++ b/packages/toolchain/js-utils-embed/build.rs @@ -0,0 +1,153 @@ +use anyhow::*; +use merkle_hash::MerkleTree; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[tokio::main] +async fn main() -> Result<()> { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?; + let out_dir = std::env::var("OUT_DIR")?; + + let mut js_utils_path = PathBuf::from(manifest_dir.clone()); + js_utils_path.push("js"); + + // Copy js-utils directory to out_dir + let out_js_utils_path = Path::new(&out_dir).join("js-utils"); + + // Remove old dir + if out_js_utils_path.is_dir() { + fs::remove_dir_all(&out_js_utils_path).context("fs::remove_dir_all")?; + } + + // Copy js-utils directory to out_dir + // TODO: This breaks deno check`` + // let mut copy_options = CopyOptions::new(); + // copy_options.overwrite = true; + // copy_options.copy_inside = true; + // copy(&js_utils_path, &out_js_utils_path, ©_options).with_context(|| { + // format!( + // "failed to copy directory from {} to {}", + // js_utils_path.display(), + // out_js_utils_path.display() + // ) + // })?; + + let status = std::process::Command::new("cp") + .arg("-R") + .arg(&js_utils_path) + .arg(&out_js_utils_path) + .status() + .with_context(|| { + format!( + "failed to copy directory from {} to {}", + js_utils_path.display(), + out_js_utils_path.display() + ) + })?; + if !status.success() { + return Err(anyhow!("cp command failed")); + } + + // Install deno + let deno_dir = Path::new(&out_dir).join("deno"); + let deno_exec = deno_embed::get_executable(&deno_dir).await?; + + // Prepare the directory for `include_dir!` + let status = Command::new(&deno_exec.executable_path) + .arg("task") + .arg("prepare") + // Deno runs out of memory on Windows + .env( + "DENO_V8_FLAGS", + "--max-heap-size=8192,--max-old-space-size=8192", + ) + .current_dir(&out_js_utils_path) + .status()?; + if !status.success() { + panic!("cache dependencies failed"); + } + + // TODO: This doesn't work + // Removes files that are not cross-platform & deletes + // broken symlinks. + // strip_cross_platform(&out_js_utils_path)?; + + println!("cargo:rerun-if-changed={}", js_utils_path.display()); + println!( + "cargo:rustc-env=JS_UTILS_PATH={}", + out_js_utils_path.display() + ); + println!( + "cargo:rustc-env=JS_UTILS_HASH={}", + hash_directory(&out_js_utils_path)? + ); + + Ok(()) +} + +fn hash_directory>(path: P) -> Result { + let tree = MerkleTree::builder(&path.as_ref().display().to_string()).build()?; + let hash = tree + .root + .item + .hash + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + Ok(hash) +} + +// fn strip_cross_platform(path: &Path) -> Result<()> { +// // Remove directories starting with "@esbuild+" +// let esbuild_path = path.join("node_modules").join(".deno"); +// let output = Command::new("find") +// .arg(&esbuild_path) +// .arg("-type") +// .arg("d") +// .arg("-name") +// .arg("@esbuild+*") +// .arg("-exec") +// .arg("rm") +// .arg("-rf") +// .arg("{}") +// .arg("+") +// .output() +// .context("Failed to execute 'find' command to remove @esbuild+ directories")?; +// +// if !output.status.success() { +// return Err(anyhow!( +// "Failed to remove @esbuild+ directories. Path: {}, Status: {}, Stdout: {}, Stderr: {}", +// esbuild_path.display(), +// output.status, +// String::from_utf8_lossy(&output.stdout), +// String::from_utf8_lossy(&output.stderr) +// )); +// } +// +// // Remove broken symlinks +// let output = Command::new("find") +// .arg(path) +// .arg("-type") +// .arg("l") +// .arg("-exec") +// .arg("sh") +// .arg("-c") +// .arg("for x; do [ -e \"$x\" ] || rm \"$x\"; done") +// .arg("{}") +// .arg("+") +// .output() +// .context("Failed to execute 'find' command to remove broken symlinks")?; +// +// if !output.status.success() { +// return Err(anyhow!( +// "Failed to remove broken symlinks. Status: {}, Stdout: {}, Stderr: {}", +// output.status, +// String::from_utf8_lossy(&output.stdout), +// String::from_utf8_lossy(&output.stderr) +// )); +// } +// +// Ok(()) +// } diff --git a/packages/toolchain/js-utils-embed/js/.gitignore b/packages/toolchain/js-utils-embed/js/.gitignore new file mode 100644 index 0000000000..2e48fcaf2f --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/.gitignore @@ -0,0 +1,3 @@ +vendor/ +node_modules/ + diff --git a/packages/toolchain/js-utils-embed/js/deno.jsonc b/packages/toolchain/js-utils-embed/js/deno.jsonc new file mode 100644 index 0000000000..459742af05 --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/deno.jsonc @@ -0,0 +1,89 @@ +{ + "tasks": { + // Format + "format": "deno fmt .", + "format:check": "deno fmt --check .", + + "check": "deno check src/**/*.ts", + + // Remove old vendored files in order to ensure a consistent cache. + "cache": "deno task cache:purge && deno task cache:download", + "cache:purge": "rm -rf vendor node_modules", + "cache:download": "deno cache src/**/*.ts", + + // Clear cache first in order to have as clean an environment as possible. + // Cache must be the last step in order to prevent unwanted extra cached + // files. Need to download cache before check in order to prevent error. + "prepare": "deno clean && deno task format && deno task check && deno task cache", + + "lint": "deno lint cli/**/*.ts toolchain/**/*.ts runtime/**/*.ts", + "lint:fix": "deno lint cli/**/*.ts toolchain/**/*.ts runtime/**/*.ts", + + "test:core": "deno test -A ." + }, + "lint": { + "include": ["packages/"], + "exclude": ["tests/"], + "rules": { + "exclude": ["no-empty-interface", "no-explicit-any", "require-await"] + } + }, + "fmt": { + "lineWidth": 120, + "useTabs": true + }, + "vendor": true, + "imports": { + "@asteasolutions/zod-to-openapi": "npm:@asteasolutions/zod-to-openapi@^7.1.1", + "@bartlomieju/postgres": "jsr:@bartlomieju/postgres@^0.17.2", + "@cross/dir": "jsr:@cross/dir@^1.1.0", + "@hono/hono": "jsr:@hono/hono@^4.6.3", + "@luca/esbuild-deno-loader": "/Users/nathan/misc/esbuild_deno_loader/mod.ts", + "@rivet-gg/esbuild-deno-loader": "jsr:@rivet-gg/esbuild-deno-loader@^0.10.3-fork.3", + "@std/assert": "jsr:@std/assert@0.213", + "@std/async": "jsr:@std/async@^1.0.4", + "@std/cli": "jsr:@std/cli@^1.0.5", + "@std/collections": "jsr:@std/collections@^1.0.5", + "@std/crypto": "jsr:@std/crypto@^1.0.3", + "@std/encoding": "jsr:@std/encoding@^1.0.3", + "@std/fmt": "jsr:@std/fmt@^1.0.1", + "@std/fs": "jsr:@std/fs@0.213", + "@std/jsonc": "jsr:@std/jsonc@0.213", + "@std/path": "jsr:@std/path@0.213", + "@ts-morph/ts-morph": "jsr:@ts-morph/ts-morph@^23.0.0", + "cloudflare:workers": "npm:@cloudflare/workers-types", + "dedent": "npm:dedent@^1.5.3", + "esbuild": "npm:esbuild@^0.20.2", + "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@1.6.4", + "glob": "npm:glob@^11.0.0", + "nanoevents": "npm:nanoevents@^9.0.0", + "tar": "npm:tar@^7.4.3", + "typescript-json-schema": "npm:typescript-json-schema@^0.62.0", + "x/importmap": "/Users/nathan/misc/esbuild_deno_loader/vendor/x/importmap/mod.ts", + "x/importmap/_util.ts": "/Users/nathan/misc/esbuild_deno_loader/vendor/x/importmap/_util.ts", + "zod": "npm:zod@^3.23.8", + "zod-validation-error": "npm:zod-validation-error@^3.3.1" + }, + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "alwaysStrict": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + // "noPropertyAccessFromIndexSignature": true, + "allowUnusedLabels": true, + "allowUnreachableCode": true, + "noImplicitAny": true + } +} diff --git a/packages/toolchain/js-utils-embed/js/deno.lock b/packages/toolchain/js-utils-embed/js/deno.lock new file mode 100644 index 0000000000..75d6c7d4b7 --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/deno.lock @@ -0,0 +1,286 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@rivet-gg/esbuild-deno-loader@^0.10.3-fork.3": "jsr:@rivet-gg/esbuild-deno-loader@0.10.3-fork.3", + "jsr:@std/assert@^0.213.1": "jsr:@std/assert@0.213.1", + "jsr:@std/cli@^1.0.5": "jsr:@std/cli@1.0.6", + "jsr:@std/encoding@0.213": "jsr:@std/encoding@0.213.1", + "jsr:@std/fmt@^1.0.1": "jsr:@std/fmt@1.0.2", + "jsr:@std/fs@0.213": "jsr:@std/fs@0.213.1", + "jsr:@std/json@^0.213.1": "jsr:@std/json@0.213.1", + "jsr:@std/jsonc@0.213": "jsr:@std/jsonc@0.213.1", + "jsr:@std/path@0.213": "jsr:@std/path@0.213.1", + "jsr:@std/path@^0.213.1": "jsr:@std/path@0.213.1", + "npm:esbuild-plugins-node-modules-polyfill@1.6.4": "npm:esbuild-plugins-node-modules-polyfill@1.6.4_esbuild@0.20.2", + "npm:esbuild@^0.20.2": "npm:esbuild@0.20.2", + "npm:zod-validation-error@^3.3.1": "npm:zod-validation-error@3.4.0_zod@3.23.8", + "npm:zod@^3.23.8": "npm:zod@3.23.8" + }, + "jsr": { + "@rivet-gg/esbuild-deno-loader@0.10.3-fork.3": { + "integrity": "41712fa34aa18d6e5b644a5921dd4ca31f5924ce327288007bc7273b092932d3", + "dependencies": [ + "jsr:@std/encoding@0.213", + "jsr:@std/jsonc@0.213", + "jsr:@std/path@0.213" + ] + }, + "@std/assert@0.213.1": { + "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" + }, + "@std/cli@1.0.6": { + "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" + }, + "@std/encoding@0.213.1": { + "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" + }, + "@std/fmt@1.0.2": { + "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" + }, + "@std/fs@0.213.1": { + "integrity": "fbcaf099f8a85c27ab0712b666262cda8fe6d02e9937bf9313ecaea39a22c501", + "dependencies": [ + "jsr:@std/assert@^0.213.1", + "jsr:@std/path@^0.213.1" + ] + }, + "@std/json@0.213.1": { + "integrity": "f572b1de605d07c4a5602445dac54bfc51b1fb87a3710a17aed2608bfca54e68" + }, + "@std/jsonc@0.213.1": { + "integrity": "5578f21aa583b7eb7317eed077ffcde47b294f1056bdbb9aacec407758637bfe", + "dependencies": [ + "jsr:@std/assert@^0.213.1", + "jsr:@std/json@^0.213.1" + ] + }, + "@std/path@0.213.1": { + "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", + "dependencies": [ + "jsr:@std/assert@^0.213.1" + ] + } + }, + "npm": { + "@esbuild/aix-ppc64@0.20.2": { + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "dependencies": {} + }, + "@esbuild/android-arm64@0.20.2": { + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "dependencies": {} + }, + "@esbuild/android-arm@0.20.2": { + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "dependencies": {} + }, + "@esbuild/android-x64@0.20.2": { + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "dependencies": {} + }, + "@esbuild/darwin-arm64@0.20.2": { + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "dependencies": {} + }, + "@esbuild/darwin-x64@0.20.2": { + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "dependencies": {} + }, + "@esbuild/freebsd-arm64@0.20.2": { + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "dependencies": {} + }, + "@esbuild/freebsd-x64@0.20.2": { + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "dependencies": {} + }, + "@esbuild/linux-arm64@0.20.2": { + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "dependencies": {} + }, + "@esbuild/linux-arm@0.20.2": { + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "dependencies": {} + }, + "@esbuild/linux-ia32@0.20.2": { + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "dependencies": {} + }, + "@esbuild/linux-loong64@0.20.2": { + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "dependencies": {} + }, + "@esbuild/linux-mips64el@0.20.2": { + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "dependencies": {} + }, + "@esbuild/linux-ppc64@0.20.2": { + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "dependencies": {} + }, + "@esbuild/linux-riscv64@0.20.2": { + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "dependencies": {} + }, + "@esbuild/linux-s390x@0.20.2": { + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "dependencies": {} + }, + "@esbuild/linux-x64@0.20.2": { + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "dependencies": {} + }, + "@esbuild/netbsd-x64@0.20.2": { + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "dependencies": {} + }, + "@esbuild/openbsd-x64@0.20.2": { + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "dependencies": {} + }, + "@esbuild/sunos-x64@0.20.2": { + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "dependencies": {} + }, + "@esbuild/win32-arm64@0.20.2": { + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "dependencies": {} + }, + "@esbuild/win32-ia32@0.20.2": { + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "dependencies": {} + }, + "@esbuild/win32-x64@0.20.2": { + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "dependencies": {} + }, + "@jspm/core@2.1.0": { + "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==", + "dependencies": {} + }, + "acorn@8.14.0": { + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dependencies": {} + }, + "confbox@0.1.8": { + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dependencies": {} + }, + "esbuild-plugins-node-modules-polyfill@1.6.4_esbuild@0.20.2": { + "integrity": "sha512-x3MCOvZrKDGAfqAYS/pZUUSwiN+XH7x84A+Prup0CZBJKuGfuGkTAC4g01D6JPs/GCM9wzZVfd8bmiy+cP/iXA==", + "dependencies": { + "@jspm/core": "@jspm/core@2.1.0", + "esbuild": "esbuild@0.20.2", + "local-pkg": "local-pkg@0.5.1", + "resolve.exports": "resolve.exports@2.0.2" + } + }, + "esbuild@0.20.2": { + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dependencies": { + "@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.20.2", + "@esbuild/android-arm": "@esbuild/android-arm@0.20.2", + "@esbuild/android-arm64": "@esbuild/android-arm64@0.20.2", + "@esbuild/android-x64": "@esbuild/android-x64@0.20.2", + "@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.20.2", + "@esbuild/darwin-x64": "@esbuild/darwin-x64@0.20.2", + "@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.20.2", + "@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.20.2", + "@esbuild/linux-arm": "@esbuild/linux-arm@0.20.2", + "@esbuild/linux-arm64": "@esbuild/linux-arm64@0.20.2", + "@esbuild/linux-ia32": "@esbuild/linux-ia32@0.20.2", + "@esbuild/linux-loong64": "@esbuild/linux-loong64@0.20.2", + "@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.20.2", + "@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.20.2", + "@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.20.2", + "@esbuild/linux-s390x": "@esbuild/linux-s390x@0.20.2", + "@esbuild/linux-x64": "@esbuild/linux-x64@0.20.2", + "@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.20.2", + "@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.20.2", + "@esbuild/sunos-x64": "@esbuild/sunos-x64@0.20.2", + "@esbuild/win32-arm64": "@esbuild/win32-arm64@0.20.2", + "@esbuild/win32-ia32": "@esbuild/win32-ia32@0.20.2", + "@esbuild/win32-x64": "@esbuild/win32-x64@0.20.2" + } + }, + "local-pkg@0.5.1": { + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dependencies": { + "mlly": "mlly@1.7.3", + "pkg-types": "pkg-types@1.2.1" + } + }, + "mlly@1.7.3": { + "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "dependencies": { + "acorn": "acorn@8.14.0", + "pathe": "pathe@1.1.2", + "pkg-types": "pkg-types@1.2.1", + "ufo": "ufo@1.5.4" + } + }, + "pathe@1.1.2": { + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dependencies": {} + }, + "pkg-types@1.2.1": { + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "dependencies": { + "confbox": "confbox@0.1.8", + "mlly": "mlly@1.7.3", + "pathe": "pathe@1.1.2" + } + }, + "resolve.exports@2.0.2": { + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dependencies": {} + }, + "ufo@1.5.4": { + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dependencies": {} + }, + "zod-validation-error@3.4.0_zod@3.23.8": { + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dependencies": { + "zod": "zod@3.23.8" + } + }, + "zod@3.23.8": { + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dependencies": {} + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@bartlomieju/postgres@^0.17.2", + "jsr:@cross/dir@^1.1.0", + "jsr:@hono/hono@^4.6.3", + "jsr:@rivet-gg/esbuild-deno-loader@^0.10.3-fork.3", + "jsr:@std/assert@0.213", + "jsr:@std/async@^1.0.4", + "jsr:@std/cli@^1.0.5", + "jsr:@std/collections@^1.0.5", + "jsr:@std/crypto@^1.0.3", + "jsr:@std/encoding@^1.0.3", + "jsr:@std/fmt@^1.0.1", + "jsr:@std/fs@0.213", + "jsr:@std/jsonc@0.213", + "jsr:@std/path@0.213", + "jsr:@ts-morph/ts-morph@^23.0.0", + "npm:@asteasolutions/zod-to-openapi@^7.1.1", + "npm:@cloudflare/workers-types", + "npm:dedent@^1.5.3", + "npm:esbuild-plugins-node-modules-polyfill@1.6.4", + "npm:esbuild@^0.20.2", + "npm:glob@^11.0.0", + "npm:nanoevents@^9.0.0", + "npm:tar@^7.4.3", + "npm:typescript-json-schema@^0.62.0", + "npm:zod-validation-error@^3.3.1", + "npm:zod@^3.23.8" + ] + } +} diff --git a/packages/toolchain/js-utils-embed/js/src/tasks/build/build.ts b/packages/toolchain/js-utils-embed/js/src/tasks/build/build.ts new file mode 100644 index 0000000000..757c1bf360 --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/src/tasks/build/build.ts @@ -0,0 +1,74 @@ +import { resolve } from "@std/path"; +import { denoPlugins } from "@rivet-gg/esbuild-deno-loader"; +import * as esbuild from "esbuild"; +import { Input, Output } from "./mod.ts"; + +export async function build(input: Input): Promise { + let outfile = resolve(input.outDir, "index.js"); + const result = await esbuild.build({ + entryPoints: [input.entryPoint], + outfile, + format: "esm", + sourcemap: true, + plugins: [ + // Bundle Deno dependencies + ...denoPlugins({ + loader: "native", + configPath: input.deno.configPath, + importMapURL: input.deno.importMapUrl, + lockPath: input.deno.lockPath, + }), + + // HACK: esbuild-deno-loader does not play nice with + // Windows paths, so we manually resolve any paths that + // start with a Windows path separator (\) and resolve + // them to the full path. + // { + // name: "fix-windows-paths", + // setup(build: esbuild.PluginBuild) { + // build.onResolve({ filter: /^\\.*/ }, (args) => { + // const resolvedPath = resolve(args.resolveDir, args.path); + // if (!exists(resolvedPath, { isFile: true })) { + // return { + // errors: [{ text: `File could not be resolved: ${resolvedPath}` }], + // }; + // } + + // return { + // path: resolve(args.resolveDir, args.path), + // }; + // }); + // }, + // } satisfies esbuild.Plugin, + ], + define: { + // HACK: Disable `process.domain` in order to correctly handle this edge case: + // https://github.com/brianc/node-postgres/blob/50c06f9bc6ff2ca1e8d7b7268b9af54ce49d72c1/packages/pg/lib/native/query.js#L126 + "process.domain": "undefined", + }, + external: [ + // Provided by Deno + "node:*", + + // Wasm must be loaded as a separate file manually, cannot be bundled + "*.wasm", + "*.wasm?module", + ], + bundle: true, + minify: input.bundle.minify, + + // TODO: Remove any + logLevel: input.bundle.logLevel as any, + metafile: input.bundle.analyzeResult, + }); + + let analyzedMetafile = undefined; + if (result.metafile) { + analyzedMetafile = await esbuild.analyzeMetafile(result.metafile); + } + + return { + files: ["index.js"], + analyzedMetafile, + }; +} diff --git a/packages/toolchain/js-utils-embed/js/src/tasks/build/mod.ts b/packages/toolchain/js-utils-embed/js/src/tasks/build/mod.ts new file mode 100644 index 0000000000..ae59a78c4e --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/src/tasks/build/mod.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { runTask } from "../../util/task/task.ts"; +import { build } from "./build.ts"; + +export const inputSchema = z.object({ + entryPoint: z.string(), + outDir: z.string(), + deno: z.object({ + configPath: z.string().optional(), + importMapUrl: z.string().optional(), + lockPath: z.string().optional(), + }), + bundle: z.object({ + minify: z.boolean(), + analyzeResult: z.boolean(), + logLevel: z.string(), + }), +}); + +export type Input = z.infer; + +export interface Output { + files: string[]; + analyzedMetafile?: string; +} + +runTask({ + inputSchema, + async run(input) { + let output = await build(input); + console.log(JSON.stringify(output)); + }, +}); diff --git a/packages/toolchain/js-utils-embed/js/src/util/task/error.ts b/packages/toolchain/js-utils-embed/js/src/util/task/error.ts new file mode 100644 index 0000000000..90ee40907b --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/src/util/task/error.ts @@ -0,0 +1,257 @@ +import { relative } from "@std/path"; +import * as colors from "@std/fmt/colors"; +import { fromError as fromValidationError } from "zod-validation-error"; + +/** + * Error type known to this program. If an error does not extend KnownError, + * it's a bug. + */ +class KnownError extends Error { +} + +export interface BuildErrorOpts extends ErrorOptions { + details?: string; + + /** + * Path of the relevant file. + */ + path?: string; + /** + * Paths of the relevant files. + */ + paths?: string[]; +} + +class BuildError extends KnownError { + public readonly details?: string; + public readonly paths: string[] = []; + + constructor(message: string, opts?: BuildErrorOpts) { + super(message, opts); + this.name = "BuildError"; + + this.details = opts?.details; + if (opts?.path) this.paths.push(opts.path); + if (opts?.paths) this.paths.push(...opts.paths); + } +} + +export interface UserErrorOpts extends BuildErrorOpts { + suggest?: string; +} + +export class UserError extends BuildError { + public readonly suggest?: string; + + constructor(message: string, opts?: UserErrorOpts) { + super(message, opts); + this.name = "UserError"; + this.suggest = opts?.suggest; + } +} + +export interface InternalErrorOpts extends BuildErrorOpts { + originalError?: Error; +} + +export class InternalError extends BuildError { + public readonly originalError?: Error; + + constructor(message: string, opts?: InternalErrorOpts) { + super(message, opts); + this.name = "InternalError"; + + this.originalError = opts?.originalError; + } +} + +export class AbortError extends InternalError { + constructor(message: string) { + super(message); + this.name = "AbortError"; + } +} + +export class UnreachableError extends InternalError { + constructor(public readonly value: never) { + super("Unreachable."); + this.name = "UnreachableError"; + } +} + +export interface CommandErrorOpts extends BuildErrorOpts { + commandOutput: Deno.CommandOutput; +} + +export class CommandError extends BuildError { + public readonly commandOutput: Deno.CommandOutput; + + constructor(message: string, opts: CommandErrorOpts) { + super(message, opts); + this.name = "CommandError"; + + this.commandOutput = opts?.commandOutput; + } +} + +export class CombinedError extends KnownError { + public readonly errors: Error[] = []; + + constructor(errors: Error[]) { + super("Combined error"); + this.name = "CombinedError"; + + // Flatten errors + for (const error of errors) { + if (error instanceof CombinedError) { + this.errors.push(...error.errors); + } else { + this.errors.push(error); + } + } + } +} +interface ValidationErrorOpts extends UserErrorOpts { + validationError: Error; + info?: Record; +} + +export class ValidationError extends UserError { + public readonly validationError: Error; + public readonly info: Record = {}; + constructor(message: string, opts: ValidationErrorOpts) { + super(message, opts); + this.name = "ValidationError"; + if (opts.info) { + this.info = opts.info; + } + this.validationError = opts.validationError; + } +} + +export function printError(error: unknown) { + // Padding + console.error(); + + if (error instanceof CombinedError) { + for (const subError of error.errors) { + printError(subError); + } + + console.error(); + console.error( + `${ + colors.bold( + colors.red( + `Failed. Found ${error.errors.length} ${error.errors.length == 1 ? "error" : "errors"}.`, + ), + ) + }`, + ); + } else if (error instanceof KnownError) { + let str = ""; + + // Message + str += `${colors.bold(colors.red("error"))}: ${colors.bold(error.message)}\n`; + str += "\n"; + + if (error instanceof BuildError && error.details) { + // Details + for (const line of error.details.split("\n")) { + str += ` ${colors.dim(line)}\n`; + } + } + + if (error instanceof ValidationError) { + str += ` ${colors.dim(fromValidationError(error.validationError).toString())}\n`; + } + + if (error instanceof UserError && error.suggest) { + // Suggest + for (const line of error.suggest.split("\n")) { + str += ` ${colors.brightBlue(line)}\n`; + } + } + + if (error instanceof BuildError) { + // Path + if (error.paths) { + let i = 0; + for (const path of error.paths) { + const pathRelative = relative(Deno.cwd(), path); + if (i == 0) { + str += ` ${colors.dim("see " + pathRelative)}\n`; + } else if (i < 4) { + str += ` ${colors.dim("see " + pathRelative)}\n`; + } else { + str += ` ${colors.dim(`...${error.paths.length - i} more`)}\n`; + break; + } + i++; + } + } + } + + if (error instanceof UnreachableError) { + str += ` ${colors.dim("value")}: ${JSON.stringify(error.value)}\n`; + } + + if (error instanceof InternalError) { + // Stack + str += prettyPrintStack(error.originalError ?? error); + } + + if (error instanceof CommandError) { + // Command output + try { + const stdout = new TextDecoder().decode(error.commandOutput.stdout).trimEnd(); + str += ` ${colors.dim("stdout")}: ${stdout}\n`; + for (const line of stdout.split("\n")) { + str += ` ${colors.dim(line)}\n`; + } + } catch (err) { + // HACK: If the command did not pipe stdout, Deno throws a TypeError. There's no + // way to check if the command piped stdout without catching the error. + if (err instanceof Error && err.name !== "TypeError") throw err; + } + + try { + if (error.commandOutput.stderr.length > 0) { + const stderr = new TextDecoder().decode(error.commandOutput.stderr).trimEnd(); + str += ` ${colors.dim("stderr")}: ${stderr}\n`; + for (const line of stderr.split("\n")) { + str += ` ${colors.dim(line)}\n`; + } + } + } catch (err) { + // HACK: See above + if (err instanceof Error && err.name !== "TypeError") throw err; + } + } + + console.error(str); + } else if (error instanceof Error) { + let str = `${colors.bold(colors.red("[UNCAUGHT] " + error.name))}: ${error.message}\n`; + + // Stack + str += prettyPrintStack(error); + + console.error(str); + } else { + // Unknown error type + + const str = `${colors.bold(colors.red("[UNCAUGHT] " + error))}\n`; + console.error(str); + } +} + +function prettyPrintStack(error: Error): string { + if (!error.stack) return ""; + + let str = ""; + for (let line of error.stack.split("\n")) { + line = line.trim(); + if (line.startsWith("at ")) str += ` ${colors.dim(line)}\n`; + } + return str; +} diff --git a/packages/toolchain/js-utils-embed/js/src/util/task/task.ts b/packages/toolchain/js-utils-embed/js/src/util/task/task.ts new file mode 100644 index 0000000000..fa23897ae8 --- /dev/null +++ b/packages/toolchain/js-utils-embed/js/src/util/task/task.ts @@ -0,0 +1,47 @@ +import { parseArgs } from "@std/cli/parse-args"; +import { printError, UserError } from "./error.ts"; +import { z } from "zod"; + +export interface Task { + inputSchema: T; + run(input: z.infer): Promise; +} + +export async function runTask(task: Task) { + Deno.addSignalListener(Deno.build.os == "windows" ? "SIGBREAK" : "SIGINT", async () => { + console.log("Received shutdown signal"); + Deno.exit(0); + }); + + let exitCode = 0; + try { + // Parse flags + const args = parseArgs(Deno.args); + const inputJson = args["input"]; + if (!inputJson) { + throw new UserError("Missing --input argument"); + } + + // Parse input + let input; + try { + input = JSON.parse(inputJson); + } catch (cause) { + throw new UserError("Invalid input JSON", { cause }); + } + + // Validate input using the task's inputSchema + const validatedInput = task.inputSchema.safeParse(input); + if (!validatedInput.success) { + throw new UserError("Input violates schema", { details: JSON.stringify(validatedInput.error, null, 2) }); + } + + // Execute task + await task.run(validatedInput.data); + } catch (err) { + printError(err); + exitCode = 1; + } + + Deno.exit(exitCode); +} diff --git a/packages/toolchain/js-utils-embed/src/lib.rs b/packages/toolchain/js-utils-embed/src/lib.rs new file mode 100644 index 0000000000..e3ac49bf06 --- /dev/null +++ b/packages/toolchain/js-utils-embed/src/lib.rs @@ -0,0 +1,55 @@ +use anyhow::*; +use include_dir::{include_dir, Dir}; +use std::path::PathBuf; +use tokio::fs; + +const JS_UTILS_DIR: Dir = include_dir!("$JS_UTILS_PATH"); +const JS_UTILS_HASH: &'static str = env!("JS_UTILS_HASH"); + +/// Return a path for the source dir. If one does not exist, the source dir will automatically be +/// extracted and executables will be set. +pub async fn src_path(data_dir: &PathBuf) -> Result { + // Create path to src based on hash + let src_dir = data_dir.join("js-utils").join(JS_UTILS_HASH); + + // Write js-utils if does not exist + if !src_dir.exists() { + fs::create_dir_all(&src_dir).await?; + tokio::task::block_in_place(|| JS_UTILS_DIR.extract(&src_dir))?; + + // Update executables + #[cfg(unix)] + set_executables(&JS_UTILS_DIR, &src_dir).await?; + } + + Ok(src_dir) +} + +/// HACK: Make all binaries in `bin` folders executable. This is because +/// bundling the vendored folders strips permissions, so executables can't be ran. +#[cfg(unix)] +async fn set_executables(dir: &Dir<'_>, fs_path: &PathBuf) -> Result<()> { + use include_dir::DirEntry; + use std::os::unix::fs::PermissionsExt; + + for entry in dir.entries() { + match entry { + DirEntry::Dir(subdir) => { + let file_name = subdir.path().file_name().unwrap_or_default(); + if file_name == "bin" || file_name == ".bin" { + for file_entry in subdir.files() { + let file_path = fs_path.join(file_entry.path()); + let metadata = fs::metadata(&file_path).await?; + let mut perms = metadata.permissions(); + perms.set_mode(perms.mode() | 0o111); + fs::set_permissions(file_path, perms).await?; + } + } + + Box::pin(set_executables(subdir, &fs_path)).await?; + } + DirEntry::File(_) => {} // Skip files at this level + } + } + Ok(()) +} diff --git a/packages/toolchain/toolchain/Cargo.toml b/packages/toolchain/toolchain/Cargo.toml new file mode 100644 index 0000000000..5e51d681dd --- /dev/null +++ b/packages/toolchain/toolchain/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "rivet-toolchain" +build = "build.rs" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +async-stream = "0.3.3" +console = "0.15" +dirs = "5.0" +futures-util = "0.3" +humansize = "1.1" +ignore = "0.4" +indicatif = "0.17" +lz4 = "1.24" +mime_guess = "2.0" +regex = "1.10" +reqwest = { version = "0.11", default-features = false, features = ["stream", "blocking", "rustls-tls"] } +rivet-api.workspace = true +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = { version = "1.0", features = ["raw_value"] } +strum = { version = "0.24", features = ["derive"] } +tar = "0.4.40" +tempfile = "3.13.0" +tokio = { version = "1.40.0", default-features = false, features = ["fs", "macros", "process", "rt", "io-util"] } +tokio-util = { version = "0.7", default-features = false, features = ["io-util"] } +typed-path = "0.7.0" +url = "2.5.0" +uuid = { version = "1.3", features = ["v4"] } +which = "5.0.0" +zip = "0.5" +const_format = "0.2.32" +pkg-version = "1.0.0" +anyhow = "1.0" +deno-embed.workspace = true +rivet-js-utils-embed.workspace = true +lazy_static = "1.5.0" +sha1 = "0.10.6" +jsonc-parser = { version = "0.26.2", features = ["serde"] } +kv-str.workspace = true + +[target.'cfg(unix)'.dependencies] +nix = { version = "0.27", default-features = false, features = ["user", "signal"] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Diagnostics", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading", "Win32_System_Console", "Win32_System_ProcessStatus"] } + +[dev-dependencies] +assert_cmd = "2.0" + +[build-dependencies] +anyhow = "1.0" +vergen-git2 = "1.0.0" + diff --git a/packages/toolchain/toolchain/build.rs b/packages/toolchain/toolchain/build.rs new file mode 100644 index 0000000000..c6f52aecc7 --- /dev/null +++ b/packages/toolchain/toolchain/build.rs @@ -0,0 +1,10 @@ +use anyhow::Result; +use vergen_git2::{Emitter, Git2Builder}; + +fn main() -> Result<()> { + Emitter::default() + .add_instructions(&Git2Builder::default().sha(true).build()?)? + .emit()?; + + Ok(()) +} diff --git a/packages/toolchain/toolchain/src/build.rs b/packages/toolchain/toolchain/src/build.rs new file mode 100644 index 0000000000..0ab23a2d9d --- /dev/null +++ b/packages/toolchain/toolchain/src/build.rs @@ -0,0 +1,4 @@ +pub mod tags { + pub const VERSION: &str = "version"; + pub const CURRENT: &str = "current"; +} diff --git a/packages/toolchain/toolchain/src/config/build/docker.rs b/packages/toolchain/toolchain/src/config/build/docker.rs new file mode 100644 index 0000000000..1ad638fc03 --- /dev/null +++ b/packages/toolchain/toolchain/src/config/build/docker.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::Compression; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Build { + /// Existing image tag to upload. + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + /// Dockerfile to build. + #[serde(skip_serializing_if = "Option::is_none")] + pub dockerfile: Option, + /// Directory to build the Docker image from. + #[serde(skip_serializing_if = "Option::is_none")] + pub build_path: Option, + /// Build target to upload. + #[serde(skip_serializing_if = "Option::is_none")] + pub build_target: Option, + /// Build arguments to pass to the build. + #[serde(skip_serializing_if = "Option::is_none")] + pub build_args: Option>, + /// Unstable features. + #[serde(skip_serializing_if = "Option::is_none")] + pub unstable: Option, +} + +impl Build { + pub fn unstable(&self) -> Unstable { + self.unstable.clone().unwrap_or_default() + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Unstable { + pub allow_root: Option, + pub build_method: Option, + pub bundle: Option, + pub compression: Option, +} + +impl Unstable { + pub fn allow_root(&self) -> bool { + self.allow_root.unwrap_or(false) + } + + pub fn build_method(&self) -> BuildMethod { + self.build_method.unwrap_or(BuildMethod::Buildx) + } + + pub fn bundle(&self) -> BundleKind { + self.bundle.unwrap_or(BundleKind::OciBundle) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum BuildMethod { + /// Use the native Docker build command. Only used if Buildx is not available. + Buildx, + + /// Create & use a Buildx builder on this machine. Required for cross-platform compilation. + Native, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, strum::AsRefStr)] +#[serde(rename_all = "snake_case")] +pub enum BundleKind { + /// Legacy option. Docker image archive output from `docker save`. Slower lobby start + /// times. + #[strum(serialize = "docker_image")] + DockerImage, + + /// OCI bundle archive derived from a generated Docker image. Optimized for fast lobby start + /// times. + #[strum(serialize = "oci_bundle")] + OciBundle, +} diff --git a/packages/toolchain/toolchain/src/config/build/javascript.rs b/packages/toolchain/toolchain/src/config/build/javascript.rs new file mode 100644 index 0000000000..3199a6a282 --- /dev/null +++ b/packages/toolchain/toolchain/src/config/build/javascript.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; + +use super::Compression; + +// TODO: Add back `deny_unknown_fields` after https://github.com/serde-rs/serde/issues/1600 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Build { + pub script: String, + pub bundler: Option, + #[serde(default)] + pub deno: Deno, + #[serde(default)] + pub unstable: Unstable, +} + +impl Build { + pub fn bundler(&self) -> Bundler { + self.bundler.unwrap_or(Bundler::Deno) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Bundler { + Deno, + None, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Deno { + pub config_path: Option, + pub import_map_url: Option, + pub lock_path: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Unstable { + pub minify: Option, + pub analyze_result: Option, + pub esbuild_log_level: Option, + pub compression: Option, +} + +impl Unstable { + pub fn minify(&self) -> bool { + self.minify.unwrap_or(true) + } + + pub fn analyze_result(&self) -> bool { + self.analyze_result.unwrap_or(false) + } + + pub fn esbuild_log_level(&self) -> String { + self.esbuild_log_level + .clone() + .unwrap_or_else(|| "error".to_string()) + } + + pub fn compression(&self) -> Compression { + // TODO: Change back to Lz4 default + self.compression.unwrap_or(Compression::None) + } +} diff --git a/packages/toolchain/toolchain/src/config/build/mod.rs b/packages/toolchain/toolchain/src/config/build/mod.rs new file mode 100644 index 0000000000..493b04e2b3 --- /dev/null +++ b/packages/toolchain/toolchain/src/config/build/mod.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +pub mod docker; +pub mod javascript; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "runtime")] +pub enum Runtime { + Docker(docker::Build), + #[serde(rename = "javascript")] + JavaScript(javascript::Build), +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, strum::AsRefStr)] +#[serde(rename_all = "snake_case")] +pub enum Compression { + /// No compression. + #[strum(serialize = "none")] + None, + + /// LZ4 compression. Fast compression optimized for fast lobby start times. + #[strum(serialize = "lz4")] + Lz4, +} + +impl Compression { + pub fn default_from_bundle_kind(build_kind: docker::BundleKind) -> Self { + match build_kind { + docker::BundleKind::DockerImage => Compression::None, + docker::BundleKind::OciBundle => Compression::Lz4, + } + } +} diff --git a/packages/toolchain/toolchain/src/config/mod.rs b/packages/toolchain/toolchain/src/config/mod.rs new file mode 100644 index 0000000000..2c1cbbe3b4 --- /dev/null +++ b/packages/toolchain/toolchain/src/config/mod.rs @@ -0,0 +1,79 @@ +use anyhow::*; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub mod build; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config(Arc); + +impl Config { + pub async fn config_path(path: Option<&Path>) -> Result { + let path = path.unwrap_or_else(|| Path::new(".")); + let jsonc_path = path.join("rivet.jsonc"); + let json_path = path.join("rivet.json"); + + let file_path = match (jsonc_path.exists(), json_path.exists()) { + (true, true) => bail!("Both rivet.jsonc and rivet.json exist. Please remove one."), + (false, false) => bail!("Neither rivet.jsonc nor rivet.json exist."), + (true, false) => jsonc_path, + (false, true) => json_path, + }; + + Ok(file_path) + } + + pub async fn load(path: Option<&Path>) -> Result { + let file_path = Self::config_path(path).await?; + let content = tokio::fs::read_to_string(&file_path) + .await + .with_context(|| anyhow!("failed to open config: {}", file_path.display()))?; + + let parsed_value = jsonc_parser::parse_to_serde_value(&content, &Default::default()) + .map_err(|err| anyhow!("Failed to parse {}: {err}", file_path.display()))? + .with_context(|| format!("Config file is empty: {}", file_path.display()))?; + let root: Root = serde_json::from_value::(parsed_value) + .map_err(|err| anyhow!("Invalid config {}: {err}", file_path.display()))?; + + Ok(Config(Arc::new(root))) + } +} + +impl Deref for Config { + type Target = Root; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Root { + pub builds: Vec, + pub unstable: Option, +} + +impl Root { + pub fn unstable(&self) -> Unstable { + self.unstable.clone().unwrap_or_default() + } +} + +// TODO: Add back `deny_unknown_fields` after https://github.com/serde-rs/serde/issues/1600 +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Build { + pub tags: HashMap, + #[serde(flatten)] + pub runtime: build::Runtime, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct Unstable {} diff --git a/packages/toolchain/toolchain/src/errors.rs b/packages/toolchain/toolchain/src/errors.rs new file mode 100644 index 0000000000..1de818c108 --- /dev/null +++ b/packages/toolchain/toolchain/src/errors.rs @@ -0,0 +1,69 @@ +/// This error type will exit without printing anything. +/// +/// This should be used for errors where the error was already printed and the program should exit +/// gracefully. +pub struct GracefulExit; + +impl std::fmt::Debug for GracefulExit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GracefulExit").finish() + } +} + +impl std::fmt::Display for GracefulExit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GracefulExit occurred") + } +} + +impl std::error::Error for GracefulExit {} + +/// This error type will exit without printing anything. +/// +/// This indicates the program was exited with a Ctrl-C event. +pub struct CtrlC; + +impl std::fmt::Debug for CtrlC { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CtrlC").finish() + } +} + +impl std::fmt::Display for CtrlC { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CtrlC occurred") + } +} + +impl std::error::Error for CtrlC {} + +/// This error type will exit with a message, but will not report the error to Rivet. +/// +/// This should be used for errors where the user input is incorrect. +pub struct UserError { + pub message: String, +} + +impl UserError { + pub fn new(msg: impl ToString) -> Self { + UserError { + message: msg.to_string(), + } + } +} + +impl std::fmt::Debug for UserError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UserError") + .field("message", &self.message) + .finish() + } +} + +impl std::fmt::Display for UserError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for UserError {} diff --git a/packages/toolchain/toolchain/src/lib.rs b/packages/toolchain/toolchain/src/lib.rs new file mode 100644 index 0000000000..8a1d6d35c5 --- /dev/null +++ b/packages/toolchain/toolchain/src/lib.rs @@ -0,0 +1,12 @@ +pub mod build; +pub mod config; +pub mod errors; +pub mod meta; +pub mod paths; +pub mod project; +pub mod tasks; +pub mod toolchain_ctx; +pub mod util; + +pub use rivet_api; +pub use toolchain_ctx::ToolchainCtx; diff --git a/packages/toolchain/toolchain/src/meta.rs b/packages/toolchain/toolchain/src/meta.rs new file mode 100644 index 0000000000..43cb6bdd99 --- /dev/null +++ b/packages/toolchain/toolchain/src/meta.rs @@ -0,0 +1,176 @@ +use anyhow::*; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::PathBuf}; +use tokio::{fs, sync::Mutex}; +use uuid::Uuid; + +use crate::{errors, paths}; + +/// Config stored in {data_dir}/meta.json. Used to store persistent data, such as tokens & cache. +#[derive(Serialize, Deserialize)] +pub struct Meta { + /// Unique ID for this instance of the toolchain. + /// + /// This ID is unique to each project folder. + pub toolchain_instance_id: Uuid, + + /// If logged in to Rivet, this will include relevant information. + /// + /// If not logged in, will be None. + pub cloud: Option, + + pub telemetry_disabled: bool, +} + +impl Meta { + fn new() -> Self { + Self { + toolchain_instance_id: Uuid::new_v4(), + cloud: None, + telemetry_disabled: false, + } + } +} + +impl Meta { + pub fn cloud(&self) -> Result<&Cloud> { + Ok(self + .cloud + .as_ref() + .ok_or_else(|| errors::UserError::new("Not logged in. Please run `rivet login`."))?) + } + + pub fn cloud_mut(&mut self) -> Result<&mut Cloud> { + Ok(self + .cloud + .as_mut() + .ok_or_else(|| errors::UserError::new("Not logged in. Please run `rivet login`."))?) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Cloud { + /// Rivet API endpoint to connect to. + pub api_endpoint: String, + + /// Cloud token used to authenticate all API requests. + pub cloud_token: String, + + /// Environment to use by default for all actions. + pub selected_environment: Option, +} + +impl Cloud { + pub fn new(api_endpoint: String, cloud_token: String) -> Self { + Self { + api_endpoint, + cloud_token, + selected_environment: None, + } + } +} + +lazy_static! { + /// List of all meta paths cached in memory. + /// + /// We can't assume the toolchain will only load one meta, so we need to support multiple + /// metas. + static ref META: Mutex> = Mutex::new(HashMap::new()); + + /// Lock on writing to the file. + static ref META_FILE_LOCK: Mutex<()> = Mutex::new(()); +} + +/// Writes the config to the file system. +/// +/// Use `mutate` to make changes to the config publicly. +async fn write(base_data_dir: &PathBuf, meta: &Meta) -> Result<()> { + // Serialize meta + let json_str = serde_json::to_string(meta)?; + + // Write file + let _write_guard = META_FILE_LOCK.lock().await; + let path = paths::meta_config_file(base_data_dir)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await?; + } + fs::write(path, json_str).await?; + + Ok(()) +} + +/// Reads from the project meta. +/// +/// If project meta does not exist, returns the default value. +pub async fn try_read_project Result, T>( + base_data_dir: &PathBuf, + cb: F, +) -> Result { + let meta_path = paths::meta_config_file(base_data_dir)?; + let mut global_meta = META.lock().await; + if !global_meta.contains_key(&meta_path) { + let mut meta = match fs::read_to_string(&meta_path).await { + Result::Ok(config) => serde_json::from_str::(&config) + .context(format!("deserialize meta ({})", meta_path.display()))?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::new(), + Err(err) => return Err(err.into()), + }; + + let result = cb(&mut meta)?; + global_meta.insert(meta_path.clone(), meta); + + Ok(result) + } else { + let meta = global_meta + .get_mut(&meta_path) + .context("global_meta[meta_path]")?; + cb(meta) + } +} + +/// Non-failable version of `try_read_project`. +pub async fn read_project T, T>(base_data_dir: &PathBuf, cb: F) -> Result { + try_read_project(base_data_dir, |x| Ok(cb(x))).await +} + +/// Mutates the project meta. +/// +/// If the project meta does not exist, a default one will be inserted and modified. +pub async fn try_mutate_project Result, T>( + base_data_dir: &PathBuf, + cb: F, +) -> Result { + // Get project + let meta_path = paths::meta_config_file(base_data_dir)?; + let mut global_meta = META.lock().await; + if !global_meta.contains_key(&meta_path) { + let mut meta = match fs::read_to_string(&meta_path).await { + Result::Ok(config) => serde_json::from_str::(&config) + .context(format!("deserialize meta ({})", meta_path.display()))?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::new(), + Err(err) => return Err(err.into()), + }; + + let result = cb(&mut meta)?; + write(base_data_dir, &meta).await?; + + Ok(result) + } else { + let meta = global_meta + .get_mut(&meta_path) + .context("global_meta[meta_path]")?; + let result = cb(meta)?; + write(base_data_dir, &meta).await?; + + Ok(result) + } +} + +/// Non-failable version of `try_mutate_project`. +pub async fn mutate_project T, T>( + base_data_dir: &PathBuf, + cb: F, +) -> Result { + try_mutate_project(base_data_dir, |x| Ok(cb(x))).await +} diff --git a/packages/toolchain/toolchain/src/paths.rs b/packages/toolchain/toolchain/src/paths.rs new file mode 100644 index 0000000000..82f814eb88 --- /dev/null +++ b/packages/toolchain/toolchain/src/paths.rs @@ -0,0 +1,51 @@ +use anyhow::*; +use sha1::{Digest, Sha1}; +use std::{env, path::PathBuf}; + +/// Root of the current project. +pub fn project_root() -> Result { + Ok(env::current_dir()?) +} + +/// Returns a unique hash to the current project's path. +pub fn project_path_hash() -> Result { + let project_root = project_root()?; + + // Build clean file name + let file_name = project_root + .file_name() + .map(|name| name.to_string_lossy().to_lowercase()) + .unwrap_or_default() + .replace(|c: char| !c.is_alphanumeric(), "_"); + + // Hash the full path to ensure it's unique + let mut hasher = Sha1::new(); + hasher.update(project_root.to_string_lossy().as_bytes()); + let hash = format!("{:.16x}", hasher.finalize()); + + // Return a human-readable name + Ok(format!("{}_{}", file_name, hash)) +} + +/// Where all data gets stored globally. +pub fn data_dir() -> Result { + Ok(dirs::data_dir().context("dirs::data_dir()")?.join("rivet")) +} + +/// Global config data. +pub fn user_config_dir(base_data_dir: &PathBuf) -> Result { + Ok(base_data_dir.join("config")) +} + +/// Directory specific to this project. +/// +/// This is not stored within the project itself since it causes problems with version control & +/// bugs in WSL. +pub fn project_data_dir(base_data_dir: &PathBuf) -> Result { + Ok(base_data_dir.join("projects").join(project_path_hash()?)) +} + +/// Stores all meta. +pub fn meta_config_file(base_data_dir: &PathBuf) -> Result { + Ok(project_data_dir(base_data_dir)?.join("meta.json")) +} diff --git a/packages/toolchain/toolchain/src/project/environment.rs b/packages/toolchain/toolchain/src/project/environment.rs new file mode 100644 index 0000000000..1926ea6424 --- /dev/null +++ b/packages/toolchain/toolchain/src/project/environment.rs @@ -0,0 +1,48 @@ +use anyhow::*; +use rivet_api::{apis, models}; +use serde::Serialize; +use uuid::Uuid; + +use crate::ToolchainCtx; + +// TODO: Replace this with a production API +#[derive(Clone, Debug, Serialize)] +pub struct TEMPEnvironment { + pub id: Uuid, + pub created_at: String, + pub slug: String, + pub name: String, +} + +impl From for TEMPEnvironment { + fn from(ns: models::CloudNamespaceSummary) -> Self { + TEMPEnvironment { + id: ns.namespace_id, + created_at: ns.create_ts, + slug: ns.name_id, + name: ns.display_name, + } + } +} + +impl From for TEMPEnvironment { + fn from(ns: models::CloudNamespaceFull) -> Self { + TEMPEnvironment { + id: ns.namespace_id, + created_at: ns.create_ts, + slug: ns.name_id, + name: ns.display_name, + } + } +} + +pub async fn get_env(ctx: &ToolchainCtx, env_id: Uuid) -> Result { + let res = apis::cloud_games_namespaces_api::cloud_games_namespaces_get_game_namespace_by_id( + &ctx.openapi_config_cloud, + &ctx.project.game_id.to_string(), + &env_id.to_string(), + ) + .await?; + + Ok(TEMPEnvironment::from(*res.namespace)) +} diff --git a/packages/toolchain/toolchain/src/project/mod.rs b/packages/toolchain/toolchain/src/project/mod.rs new file mode 100644 index 0000000000..640a7930b4 --- /dev/null +++ b/packages/toolchain/toolchain/src/project/mod.rs @@ -0,0 +1 @@ +pub mod environment; diff --git a/packages/toolchain/toolchain/src/tasks/auth/check_state.rs b/packages/toolchain/toolchain/src/tasks/auth/check_state.rs new file mode 100644 index 0000000000..28466abd6b --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/auth/check_state.rs @@ -0,0 +1,29 @@ +use anyhow::*; +use serde::{Deserialize, Serialize}; + +use crate::{meta, paths, util::task}; + +#[derive(Deserialize)] +pub struct Input {} + +#[derive(Serialize)] +pub struct Output { + pub signed_in: bool, +} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "auth.check_state" + } + + async fn run(_task: task::TaskCtx, _input: Input) -> Result { + let signed_in = + meta::read_project(&paths::data_dir()?, |meta| meta.cloud.is_some()).await?; + Ok(Output { signed_in }) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/auth/mod.rs b/packages/toolchain/toolchain/src/tasks/auth/mod.rs new file mode 100644 index 0000000000..bc4772abf8 --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/auth/mod.rs @@ -0,0 +1,4 @@ +pub mod check_state; +pub mod sign_out; +pub mod start_sign_in; +pub mod wait_for_sign_in; diff --git a/packages/toolchain/toolchain/src/tasks/auth/sign_out.rs b/packages/toolchain/toolchain/src/tasks/auth/sign_out.rs new file mode 100644 index 0000000000..a8fd78b878 --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/auth/sign_out.rs @@ -0,0 +1,29 @@ +use anyhow::*; +use serde::{Deserialize, Serialize}; + +use crate::{meta, paths, util::task}; + +#[derive(Deserialize)] +pub struct Input {} + +#[derive(Serialize)] +pub struct Output {} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "auth.sign_out" + } + + async fn run(_task: task::TaskCtx, _input: Self::Input) -> Result { + meta::mutate_project(&paths::data_dir()?, |meta| { + meta.cloud = None; + }) + .await?; + Ok(Output {}) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/auth/start_sign_in.rs b/packages/toolchain/toolchain/src/tasks/auth/start_sign_in.rs new file mode 100644 index 0000000000..5930deeb3e --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/auth/start_sign_in.rs @@ -0,0 +1,45 @@ +use anyhow::*; +use rivet_api::apis; +use serde::{Deserialize, Serialize}; + +use crate::{toolchain_ctx, util::task}; + +#[derive(Deserialize)] +pub struct Input { + pub api_endpoint: String, +} + +#[derive(Serialize)] +pub struct Output { + pub device_link_url: String, + pub device_link_token: String, +} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "auth.start_sign_in" + } + + async fn run(_task: task::TaskCtx, input: Self::Input) -> Result { + let openapi_config_cloud_unauthed = apis::configuration::Configuration { + base_path: input.api_endpoint, + user_agent: Some(toolchain_ctx::user_agent()), + ..Default::default() + }; + + let prepare_res = apis::cloud_devices_links_api::cloud_devices_links_prepare( + &openapi_config_cloud_unauthed, + ) + .await?; + + Ok(Output { + device_link_url: prepare_res.device_link_url, + device_link_token: prepare_res.device_link_token, + }) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/auth/wait_for_sign_in.rs b/packages/toolchain/toolchain/src/tasks/auth/wait_for_sign_in.rs new file mode 100644 index 0000000000..ab7088e92c --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/auth/wait_for_sign_in.rs @@ -0,0 +1,74 @@ +use anyhow::*; +use rivet_api::apis; +use serde::{Deserialize, Serialize}; + +use crate::{meta, paths, toolchain_ctx, util::task}; + +#[derive(Deserialize)] +pub struct Input { + pub api_endpoint: String, + pub device_link_token: String, +} + +#[derive(Serialize)] +pub struct Output {} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "auth.wait_for_sign_in" + } + + async fn run(_task: task::TaskCtx, input: Self::Input) -> Result { + let openapi_config_cloud_unauthed = apis::configuration::Configuration { + base_path: input.api_endpoint.clone(), + user_agent: Some(toolchain_ctx::user_agent()), + ..Default::default() + }; + + let mut watch_index = None; + let token = loop { + let prepare_res = apis::cloud_devices_links_api::cloud_devices_links_get( + &openapi_config_cloud_unauthed, + &input.device_link_token, + watch_index.as_ref().map(String::as_str), + ) + .await?; + + watch_index = Some(prepare_res.watch.index); + + if let Some(token) = prepare_res.cloud_token { + break token; + } + }; + + let new_ctx = crate::toolchain_ctx::init(input.api_endpoint.clone(), token.clone()).await?; + + let inspect_res = + apis::cloud_auth_api::cloud_auth_inspect(&new_ctx.openapi_config_cloud).await?; + + let game_id = inspect_res + .agent + .game_cloud + .context("no game cloud token found")? + .game_id; + + let _game_res = apis::cloud_games_api::cloud_games_get_game_by_id( + &new_ctx.openapi_config_cloud, + &game_id.to_string(), + None, + ) + .await?; + + meta::mutate_project(&paths::data_dir()?, |meta| { + meta.cloud = Some(meta::Cloud::new(input.api_endpoint, token)) + }) + .await?; + + Ok(Output {}) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/deploy/docker.rs b/packages/toolchain/toolchain/src/tasks/deploy/docker.rs new file mode 100644 index 0000000000..d43f0d197f --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/deploy/docker.rs @@ -0,0 +1,216 @@ +use anyhow::*; +use std::path::Path; +use uuid::Uuid; + +use crate::{ + config, paths, + project::environment::TEMPEnvironment, + toolchain_ctx::ToolchainCtx, + util::{ + cmd::{self, shell_cmd}, + docker::{self, generate_unique_image_tag}, + task, + }, +}; + +pub struct BuildAndUploadOpts { + pub env: TEMPEnvironment, + pub config: config::Config, + pub build_config: config::build::docker::Build, + pub version_name: String, +} + +/// Builds image if not specified and returns the build ID. +pub async fn build_and_upload( + ctx: &ToolchainCtx, + task: task::TaskCtx, + opts: BuildAndUploadOpts, +) -> Result { + let project_root = paths::project_root()?; + + // Determine build attributes + let build_config_unstable = opts.build_config.unstable(); + let bundle = build_config_unstable.bundle(); + let compression = build_config_unstable + .compression + .unwrap_or_else(|| config::build::Compression::default_from_bundle_kind(bundle)); + + // Deploy Docker build + let build_id = if let Some(image) = &opts.build_config.image { + let push_output = docker_push( + ctx, + task.clone(), + &DockerPushOpts { + config: opts.config.clone(), + env: opts.env, + name: Some(opts.version_name.to_string()), + docker_tag: image.clone(), + bundle, + compression, + allow_root: build_config_unstable.allow_root(), + }, + ) + .await?; + + task.log(format!("[Created Build] {}", push_output.build_id)); + + push_output.build_id + } else { + let dockerfile = opts + .build_config + .dockerfile + .unwrap_or_else(|| "Dockerfile".to_string()); + + let path = opts + .build_config + .build_path + .as_ref() + .map(|x| x.as_str()) + .unwrap_or("."); + + let push_output = docker_build_and_push( + ctx, + task.clone(), + &project_root.join(path), + &DockerBuildPushOpts { + config: opts.config.clone(), + env: opts.env.clone(), + dockerfile: dockerfile.clone(), + name: Some(opts.version_name.clone()), + build_args: Some( + opts.build_config + .build_args + .iter() + .flatten() + .map(|(k, v)| format!("{k}={v}")) + .collect(), + ), + build_target: opts.build_config.build_target.clone(), + build_method: build_config_unstable.build_method(), + bundle, + compression, + allow_root: build_config_unstable.allow_root(), + }, + ) + .await?; + + task.log(format!("[Created Build] {}", push_output.build_id)); + + push_output.build_id + }; + + Ok(build_id) +} + +pub struct DockerBuildPushOpts { + pub config: config::Config, + pub env: TEMPEnvironment, + pub name: Option, + + /// Path to Dockerfile + pub dockerfile: String, + + /// Docker build args + pub build_args: Option>, + + /// Target build stage to build. + pub build_target: Option, + + pub build_method: config::build::docker::BuildMethod, + pub bundle: config::build::docker::BundleKind, + pub compression: config::build::Compression, + pub allow_root: bool, +} + +/// Build and push a Dockerfile. +pub async fn docker_build_and_push( + ctx: &ToolchainCtx, + task: task::TaskCtx, + current_dir: &Path, + push_opts: &DockerBuildPushOpts, +) -> Result { + // Build image + let build_output = docker::build::build_image( + ctx, + task.clone(), + current_dir, + &Path::new(&push_opts.dockerfile), + push_opts.build_method, + push_opts.bundle, + push_opts.compression, + push_opts.build_args.as_ref().map(|x| x.as_slice()), + push_opts.build_target.as_ref().map(String::as_str), + push_opts.allow_root, + ) + .await?; + + // Upload build + docker::push::push_tar( + ctx, + task.clone(), + &docker::push::PushOpts { + config: push_opts.config.clone(), + env: push_opts.env.clone(), + path: build_output.path.to_owned(), + docker_tag: build_output.tag, + name: push_opts.name.clone(), + bundle: push_opts.bundle, + compression: push_opts.compression, + }, + ) + .await +} + +pub struct DockerPushOpts { + pub config: config::Config, + pub env: TEMPEnvironment, + pub name: Option, + + pub docker_tag: String, + + pub bundle: config::build::docker::BundleKind, + pub compression: config::build::Compression, + pub allow_root: bool, +} + +/// Push an image that's already built. +pub async fn docker_push( + ctx: &ToolchainCtx, + task: task::TaskCtx, + push_opts: &DockerPushOpts, +) -> Result { + // Re-tag image with unique tag + let unique_image_tag = generate_unique_image_tag(); + let mut tag_cmd = shell_cmd("docker"); + tag_cmd + .arg("image") + .arg("tag") + .arg(&push_opts.docker_tag) + .arg(&unique_image_tag); + cmd::execute_docker_cmd_silent(tag_cmd, "failed to tag Docker image").await?; + + // Archive image + let archive_path = docker::archive::create_archive( + task.clone(), + &unique_image_tag, + push_opts.bundle, + push_opts.compression, + push_opts.allow_root, + ) + .await?; + + docker::push::push_tar( + ctx, + task.clone(), + &docker::push::PushOpts { + config: push_opts.config.clone(), + env: push_opts.env.clone(), + path: archive_path.to_owned(), + docker_tag: unique_image_tag, + name: push_opts.name.clone(), + bundle: push_opts.bundle, + compression: push_opts.compression, + }, + ) + .await +} diff --git a/packages/toolchain/toolchain/src/tasks/deploy/js.rs b/packages/toolchain/toolchain/src/tasks/deploy/js.rs new file mode 100644 index 0000000000..a5fdeedeea --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/deploy/js.rs @@ -0,0 +1,260 @@ +use anyhow::*; +use futures_util::{StreamExt, TryStreamExt}; +use rivet_api::{apis, models}; +use std::{path::PathBuf, sync::Arc}; +use tokio::fs; +use uuid::Uuid; + +use crate::{ + config, paths, + project::environment::TEMPEnvironment, + toolchain_ctx::ToolchainCtx, + util::{js_utils, net::upload, task, term}, +}; + +/// File name for the index path to the script. +const BUILD_INDEX_NAME: &str = "index.js"; + +pub struct BuildAndUploadOpts { + pub env: TEMPEnvironment, + pub build_config: config::build::javascript::Build, + pub version_name: String, +} + +/// Builds image if not specified and returns the build ID. +pub async fn build_and_upload( + ctx: &ToolchainCtx, + task: task::TaskCtx, + opts: BuildAndUploadOpts, +) -> Result { + let project_root = paths::project_root()?; + + // Create dir to write build artifacts to + let build_dir = tempfile::TempDir::new()?; + + // Bundle JS + match opts.build_config.bundler() { + config::build::javascript::Bundler::Deno => { + // Validate that the script path has a .ts or .js extension + let script_path = project_root.join(&opts.build_config.script); + ensure!( + script_path.extension().and_then(|s| s.to_str()) == Some("ts") + || script_path.extension().and_then(|s| s.to_str()) == Some("js"), + "script file must have a .ts or .js extension for Deno bundler" + ); + + // Search for deno.json or deno.jsonc + let deno_config_path = ["deno.json", "deno.jsonc"].iter().find_map(|file_name| { + let path = project_root.join(file_name); + if path.exists() { + Some(path.display().to_string()) + } else { + None + } + }); + + // Search for a Deno lockfile + let project_deno_lockfile_path = project_root.join("deno.lock"); + let deno_lockfile_path = if project_deno_lockfile_path.exists() { + Some(project_deno_lockfile_path.display().to_string()) + } else { + opts.build_config.deno.lock_path.clone() + }; + + // Build the bundle to the output dir. This will bundle all Deno dependencies into a + // single JS file. + // + // The Deno command is run in the project root, so `config_path`, `lock_path`, etc can + // all safely be passed as relative paths without joining with `project_root`. + let output = js_utils::run_command_and_parse_output::< + js_utils::schemas::build::Input, + js_utils::schemas::build::Output, + >( + "src/tasks/build/mod.ts", + &js_utils::schemas::build::Input { + entry_point: script_path, + out_dir: build_dir.path().to_path_buf(), + deno: js_utils::schemas::build::Deno { + config_path: deno_config_path.or_else(|| { + opts.build_config + .deno + .config_path + .map(|x| project_root.join(x).display().to_string()) + }), + import_map_url: opts.build_config.deno.import_map_url.clone(), + lock_path: deno_lockfile_path, + }, + bundle: js_utils::schemas::build::Bundle { + minify: opts.build_config.unstable.minify(), + analyze_result: opts.build_config.unstable.analyze_result(), + log_level: opts.build_config.unstable.esbuild_log_level(), + }, + }, + ) + .await?; + if let Some(analyze_result) = output.analyzed_metafile { + task.log("[Bundle Analysis]"); + task.log(analyze_result); + } + } + config::build::javascript::Bundler::None => { + // Ensure the script path has a .js extension + let script_path = project_root.join(opts.build_config.script); + ensure!( + script_path.extension().and_then(|s| s.to_str()) == Some("js"), + "script file must have a .js extension when not using a bundler" + ); + + // Validate script exists + match fs::metadata(&script_path).await { + Result::Ok(_) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + bail!("script not found: {}", script_path.display()) + } + Err(err) => bail!("failed to read script at {}: {err}", script_path.display()), + } + + // Copy index file to build dir + fs::copy(&script_path, build_dir.path().join(BUILD_INDEX_NAME)).await?; + } + }; + + // Deploy JS build + let build_id = upload_bundle( + ctx, + task.clone(), + &UploadBundleOpts { + env: opts.env, + version_name: opts.version_name, + build_path: build_dir.path().into(), + compression: opts.build_config.unstable.compression(), + }, + ) + .await?; + + Ok(build_id) +} + +struct UploadBundleOpts { + env: TEMPEnvironment, + version_name: String, + + /// Path to the root of the built files. + build_path: PathBuf, + + compression: config::build::Compression, +} + +/// Uploads a built JavaScript bundle. +async fn upload_bundle( + ctx: &ToolchainCtx, + task: task::TaskCtx, + push_opts: &UploadBundleOpts, +) -> Result { + // Validate bundle + match fs::metadata(push_opts.build_path.join(BUILD_INDEX_NAME)).await { + Result::Ok(_) => {} + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + bail!("index.js does not exist in javascript bundle") + } else { + bail!("error reading javascript index.js: {err}") + } + } + } + + // Archive build + let build_tar_file = tempfile::NamedTempFile::new()?; + let mut build_archive = tar::Builder::new(build_tar_file); + build_archive.append_dir_all(".", &push_opts.build_path)?; + let build_tar_file = build_archive.into_inner()?; + + let build_kind = models::ActorBuildKind::Javascript; + let build_compression = match push_opts.compression { + config::build::Compression::None => models::ActorBuildCompression::None, + config::build::Compression::Lz4 => models::ActorBuildCompression::Lz4, + }; + + // Compress build + let compressed_path = + crate::util::build::compress_build(build_tar_file.as_ref(), push_opts.compression).await?; + + let image_file = upload::prepare_upload_file( + &compressed_path, + &crate::util::build::file_name(build_kind, build_compression), + fs::metadata(&compressed_path).await?, + )?; + let files = vec![image_file.clone()]; + + let total_len = files + .iter() + .fold(0, |acc, x| acc + x.prepared.content_length); + + task.log(format!( + "[Uploading] {size}", + size = upload::format_file_size(total_len as u64)?, + )); + + let prepare_res = apis::actor_builds_api::actor_builds_prepare( + &ctx.openapi_config_cloud, + models::ActorPrepareBuildRequest { + name: push_opts.version_name.clone(), + image_tag: None, + image_file: Box::new(image_file.prepared), + kind: Some(build_kind), + compression: Some(build_compression), + }, + Some(&ctx.project.name_id), + Some(&push_opts.env.slug), + ) + .await + .map_err(|err| anyhow!("Failed to prepare deploy: {err}"))?; + + // Upload files + let reqwest_client = Arc::new(reqwest::Client::new()); + let pb = term::EitherProgressBar::Multi(term::multi_progress_bar(task.clone())); + + futures_util::stream::iter(prepare_res.presigned_requests) + .map(Ok) + .try_for_each_concurrent(8, |presigned_req| { + let task = task.clone(); + let pb = pb.clone(); + let files = files.clone(); + let reqwest_client = reqwest_client.clone(); + + async move { + // Find the matching prepared file + let file = files + .iter() + .find(|f| f.prepared.path == presigned_req.path) + .context("missing prepared file")?; + + upload::upload_file( + task.clone(), + &reqwest_client, + &presigned_req, + &file.absolute_path, + file.prepared.content_type.as_ref(), + pb, + ) + .await?; + + Result::<()>::Ok(()) + } + }) + .await?; + + let complete_res = apis::actor_builds_api::actor_builds_complete( + &ctx.openapi_config_cloud, + &prepare_res.build.to_string(), + Some(&ctx.project.name_id), + Some(&push_opts.env.slug), + ) + .await; + if let Err(err) = complete_res.as_ref() { + task.log(format!("{err:?}")); + } + complete_res.context("complete_res")?; + + Ok(prepare_res.build) +} diff --git a/packages/toolchain/toolchain/src/tasks/deploy/mod.rs b/packages/toolchain/toolchain/src/tasks/deploy/mod.rs new file mode 100644 index 0000000000..43029ee3c9 --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/deploy/mod.rs @@ -0,0 +1,228 @@ +use anyhow::*; +use rivet_api::{apis, models}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use uuid::Uuid; + +use crate::{ + build, + project::environment::TEMPEnvironment, + ToolchainCtx, + {config, util::task}, +}; + +mod docker; +mod js; + +#[derive(Deserialize)] +pub struct Input { + pub config: config::Config, + pub environment_id: Uuid, + pub build_tags: Option>, +} + +#[derive(Serialize)] +pub struct Output { + pub build_ids: Vec, +} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "deploy" + } + + async fn run(task: task::TaskCtx, input: Self::Input) -> Result { + let ctx = crate::toolchain_ctx::load().await?; + + let env = crate::project::environment::get_env(&ctx, input.environment_id).await?; + + // Reserve version name + let reserve_res = + apis::cloud_games_versions_api::cloud_games_versions_reserve_version_name( + &ctx.openapi_config_cloud, + &ctx.project.game_id.to_string(), + ) + .await?; + let version_name = reserve_res.version_display_name; + + // Build + let mut build_ids = Vec::new(); + for build in &input.config.builds { + // Filter out builds that match the tags + if let Some(filter) = &input.build_tags { + if !filter.iter().all(|(k, v)| build.tags.get(k) == Some(v)) { + continue; + } + } + + task.log(format!("[Building] {}", kv_str::to_str(&build.tags)?)); + + // Build + let build_id = build_and_upload( + &ctx, + task.clone(), + input.config.clone(), + &env, + &version_name, + build, + ) + .await?; + build_ids.push(build_id); + } + + ensure!(!build_ids.is_empty(), "No builds matched build tags"); + + task.log("[Deploy Finished]"); + + Ok(Output { build_ids }) + } +} + +/// Builds the required resources and uploads it to Rivet. +/// +/// Returns the resulting build ID. +async fn build_and_upload( + ctx: &ToolchainCtx, + task: task::TaskCtx, + config: config::Config, + env: &TEMPEnvironment, + version_name: &str, + build: &config::Build, +) -> Result { + // Build tags + // + // **version** + // + // Unique ident for this build. Used for figuring out which server to start when + // passing dynamic version from client. + // + // **latest** + // + // Indicates the latest build to use for this environment. Used if not providing a client-side + // version. + // let mut tags = HashMap::from([ + // (build::tags::VERSION.to_string(), version_name.to_string()), + // (build::tags::CURRENT.to_string(), "true".to_string()), + // ]); + // tags.extend(build.tags.clone()); + + // let exclusive_tags = vec![ + // build::tags::VERSION.to_string(), + // build::tags::CURRENT.to_string(), + // ]; + + // Build & upload + let build_id = match &build.runtime { + config::build::Runtime::Docker(docker) => { + docker::build_and_upload( + &ctx, + task.clone(), + docker::BuildAndUploadOpts { + env: env.clone(), + config: config.clone(), + build_config: docker.clone(), + version_name: version_name.to_string(), + }, + ) + .await? + } + config::build::Runtime::JavaScript(js) => { + js::build_and_upload( + &ctx, + task.clone(), + js::BuildAndUploadOpts { + env: env.clone(), + build_config: js.clone(), + version_name: version_name.to_string(), + }, + ) + .await? + } + }; + + // // Tag build + // let complete_res = apis::actor_builds_api::actor_builds_patch_tags( + // &ctx.openapi_config_cloud, + // &build_id.to_string(), + // models::ActorPatchBuildTagsRequest { + // tags: Some(serde_json::to_value(&tags)?), + // exclusive_tags: Some(exclusive_tags.clone()), + // }, + // Some(&ctx.project.name_id), + // Some(&env.slug), + // ) + // .await; + // if let Err(err) = complete_res.as_ref() { + // task.log(format!("{err:?}")); + // } + // complete_res.context("complete_res")?; + + // HACK: Multiple exclusive tags doesn't work atm + { + let complete_res = apis::actor_builds_api::actor_builds_patch_tags( + &ctx.openapi_config_cloud, + &build_id.to_string(), + models::ActorPatchBuildTagsRequest { + tags: Some(serde_json::to_value(&build.tags)?), + exclusive_tags: None, + }, + Some(&ctx.project.name_id), + Some(&env.slug), + ) + .await; + if let Err(err) = complete_res.as_ref() { + task.log(format!("{err:?}")); + } + complete_res.context("complete_res")?; + + let complete_res = apis::actor_builds_api::actor_builds_patch_tags( + &ctx.openapi_config_cloud, + &build_id.to_string(), + models::ActorPatchBuildTagsRequest { + tags: Some(serde_json::to_value(&HashMap::from([( + build::tags::CURRENT.to_string(), + "true".to_string(), + )]))?), + exclusive_tags: Some(vec![build::tags::CURRENT.to_string()]), + }, + Some(&ctx.project.name_id), + Some(&env.slug), + ) + .await; + if let Err(err) = complete_res.as_ref() { + task.log(format!("{err:?}")); + } + complete_res.context("complete_res")?; + + let complete_res = apis::actor_builds_api::actor_builds_patch_tags( + &ctx.openapi_config_cloud, + &build_id.to_string(), + models::ActorPatchBuildTagsRequest { + tags: Some(serde_json::to_value(&HashMap::from([( + build::tags::VERSION.to_string(), + version_name.to_string(), + )]))?), + // TODO: This does not behave correctly atm + exclusive_tags: None, + // exclusive_tags: Some(vec![build::tags::VERSION.to_string()]), + }, + Some(&ctx.project.name_id), + Some(&env.slug), + ) + .await; + if let Err(err) = complete_res.as_ref() { + task.log(format!("{err:?}")); + } + complete_res.context("complete_res")?; + } + + task.log(format!("[Build Finished] {build_id}")); + task.log(""); + + Ok(build_id) +} diff --git a/packages/toolchain/toolchain/src/tasks/env/mod.rs b/packages/toolchain/toolchain/src/tasks/env/mod.rs new file mode 100644 index 0000000000..0c305a7ee9 --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/env/mod.rs @@ -0,0 +1 @@ +pub mod select; diff --git a/packages/toolchain/toolchain/src/tasks/env/select.rs b/packages/toolchain/toolchain/src/tasks/env/select.rs new file mode 100644 index 0000000000..7172026650 --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/env/select.rs @@ -0,0 +1,38 @@ +use anyhow::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{meta, paths, util::task}; + +#[derive(Deserialize)] +pub struct Input { + pub environment_id: Uuid, +} + +#[derive(Serialize)] +pub struct Output {} + +#[derive(Serialize)] +pub struct CloudData {} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "env_select" + } + + async fn run(_task: task::TaskCtx, input: Self::Input) -> Result { + meta::try_mutate_project(&paths::data_dir()?, |project| { + let cloud = project.cloud_mut()?; + cloud.selected_environment = Some(input.environment_id); + Ok(()) + }) + .await?; + + Ok(Output {}) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/get_bootstrap_data.rs b/packages/toolchain/toolchain/src/tasks/get_bootstrap_data.rs new file mode 100644 index 0000000000..f07755721d --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/get_bootstrap_data.rs @@ -0,0 +1,58 @@ +use anyhow::*; +use rivet_api::apis; +use serde::{Deserialize, Serialize}; + +use crate::{project::environment::TEMPEnvironment, util::task}; + +#[derive(Deserialize)] +pub struct Input {} + +#[derive(Serialize)] +pub struct Output { + pub cloud: Option, +} + +#[derive(Serialize)] +pub struct CloudData { + pub token: String, + pub api_endpoint: String, + pub envs: Vec, +} + +pub struct Task; + +impl task::Task for Task { + type Input = Input; + type Output = Output; + + fn name() -> &'static str { + "get_bootstrap_data" + } + + async fn run(_task: task::TaskCtx, _input: Self::Input) -> Result { + let cloud = if let Some(ctx) = crate::toolchain_ctx::try_load().await? { + // HACK: Map ns to temporary env data structure + let envs = apis::cloud_games_api::cloud_games_get_game_by_id( + &ctx.openapi_config_cloud, + &ctx.project.game_id.to_string(), + None, + ) + .await? + .game + .namespaces + .into_iter() + .map(TEMPEnvironment::from) + .collect::>(); + + Some(CloudData { + token: ctx.access_token.clone(), + api_endpoint: ctx.api_endpoint.clone(), + envs, + }) + } else { + None + }; + + Ok(Output { cloud }) + } +} diff --git a/packages/toolchain/toolchain/src/tasks/mod.rs b/packages/toolchain/toolchain/src/tasks/mod.rs new file mode 100644 index 0000000000..228945b13f --- /dev/null +++ b/packages/toolchain/toolchain/src/tasks/mod.rs @@ -0,0 +1,14 @@ +pub mod auth; +pub mod deploy; +pub mod env; +pub mod get_bootstrap_data; + +crate::task_registry!( + auth::check_state::Task, + auth::sign_out::Task, + auth::start_sign_in::Task, + auth::wait_for_sign_in::Task, + env::select::Task, + deploy::Task, + get_bootstrap_data::Task, +); diff --git a/packages/toolchain/toolchain/src/toolchain_ctx.rs b/packages/toolchain/toolchain/src/toolchain_ctx.rs new file mode 100644 index 0000000000..1ef5fcd7ab --- /dev/null +++ b/packages/toolchain/toolchain/src/toolchain_ctx.rs @@ -0,0 +1,125 @@ +use anyhow::*; +use pkg_version::{pkg_version_major, pkg_version_minor, pkg_version_patch}; +use rivet_api::{apis, models}; +use std::{env, sync::Arc}; +use tokio::sync::OnceCell; + +use crate::{meta, paths}; + +pub const VERSION: &str = { + const MAJOR: u32 = pkg_version_major!(); + const MINOR: u32 = pkg_version_minor!(); + const PATCH: u32 = pkg_version_patch!(); + const GIT_SHA: &str = env!("VERGEN_GIT_SHA"); + const_format::formatcp!("{MAJOR}.{MINOR}.{PATCH} ({GIT_SHA})") +}; + +pub fn user_agent() -> String { + format!("CLI/{VERSION}") +} + +pub type ToolchainCtx = Arc; + +pub struct CtxInner { + pub api_endpoint: String, + pub access_token: String, + pub project: models::CloudGameFull, + + /// Domains that host parts of Rivet + pub bootstrap: rivet_api::models::CloudBootstrapResponse, + + pub openapi_config_cloud: apis::configuration::Configuration, +} + +static TOOLCHAIN_CTX: OnceCell = OnceCell::const_new(); + +pub async fn try_load() -> Result> { + let data = meta::read_project(&paths::data_dir()?, |x| { + x.cloud + .as_ref() + .map(|cloud| (cloud.api_endpoint.clone(), cloud.cloud_token.clone())) + }) + .await?; + if let Some((api_endpoint, token)) = data { + let ctx = TOOLCHAIN_CTX + .get_or_try_init(|| async { init(api_endpoint, token).await }) + .await?; + Ok(Some(ctx.clone())) + } else { + Ok(None) + } +} + +pub async fn load() -> Result { + let (api_endpoint, token) = meta::try_read_project(&paths::data_dir()?, |x| { + let cloud = x.cloud()?; + Ok((cloud.api_endpoint.clone(), cloud.cloud_token.clone())) + }) + .await?; + let ctx = TOOLCHAIN_CTX + .get_or_try_init(|| async { init(api_endpoint, token).await }) + .await?; + Ok(ctx.clone()) +} + +pub async fn init(api_endpoint: String, cloud_token: String) -> Result { + // Disable connection pooling to fix "connection closed before message completed" + // + // See https://github.com/hyperium/hyper/issues/2136#issuecomment-861826148 + let client = reqwest::Client::builder() + .no_proxy() + .pool_max_idle_per_host(0) + .build()?; + + // Create OpenAPI config + let openapi_config_cloud = apis::configuration::Configuration { + base_path: api_endpoint.clone(), + bearer_access_token: Some(cloud_token.clone()), + user_agent: Some(user_agent()), + client, + ..Default::default() + }; + + // Make requests + let (inspect_response, bootstrap_response): ( + rivet_api::models::CloudInspectResponse, + rivet_api::models::CloudBootstrapResponse, + ) = tokio::try_join!( + async { + Result::Ok( + apis::cloud_auth_api::cloud_auth_inspect(&openapi_config_cloud) + .await + .context("inspect failed")?, + ) + }, + async { + Result::Ok( + apis::cloud_api::cloud_bootstrap(&openapi_config_cloud) + .await + .context("bootstrap failed")?, + ) + } + )?; + + let project_id = if let Some(game_cloud) = inspect_response.agent.game_cloud { + game_cloud.game_id + } else { + bail!("invalid agent kind") + }; + + let project_res = apis::cloud_games_api::cloud_games_get_game_by_id( + &openapi_config_cloud, + &project_id.to_string(), + None, + ) + .await + .context("get project failed")?; + + Ok(Arc::new(CtxInner { + api_endpoint, + access_token: cloud_token, + project: *project_res.game, + bootstrap: bootstrap_response, + openapi_config_cloud, + })) +} diff --git a/packages/toolchain/toolchain/src/util/api.rs b/packages/toolchain/toolchain/src/util/api.rs new file mode 100644 index 0000000000..7eac2a3cc2 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/api.rs @@ -0,0 +1,77 @@ +use anyhow::*; +use regex::Regex; +use url::Url; + +/// Normalize an API endpoint to a standardized URL. +/// +/// This allows us to pass shorthand URLs like: +/// +/// * api.staging2.gameinc.io -> https://api.staging2.gameinc.io +/// * 127.0.0.1:8080 -> http://127.0.0.1:8080 +pub fn normalize_api_endpoint(endpoint: &str) -> Result { + // Attempt to parse URL + let url = match Url::parse(endpoint) { + Result::Ok(url) => url, + Err(url::ParseError::RelativeUrlWithoutBase) => { + // No scheme was provided, determine if to use HTTP or HTTPS based on if the URL is + // localhost. + let localhost_regex = Regex::new(r"^(127\.0\.0\.1|\[::1\])(:\d+)?$")?; + let proto = if localhost_regex.is_match(endpoint) { + "http" + } else { + "https" + }; + + // Attempt to parse endpoint + match url::Url::parse(format!("{proto}://{endpoint}").as_str()) { + Result::Ok(url) => url, + Err(_) => { + bail!("failed to parse endpoint: {}", endpoint) + } + } + } + Err(_) => { + bail!("failed to parse endpoint: {}", endpoint) + } + }; + + let url_str = url.to_string(); + + // Remove trailing path + let url_str = if let Some(x) = url_str.strip_suffix('/') { + x.to_string() + } else { + url_str + }; + + Ok(url_str) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_normalize_api_endpoint() { + assert_eq!( + "https://api.example.com", + normalize_api_endpoint("https://api.example.com").unwrap() + ); + assert_eq!( + "https://api.example.com", + normalize_api_endpoint("api.example.com").unwrap() + ); + assert_eq!( + "http://127.0.0.1", + normalize_api_endpoint("127.0.0.1").unwrap() + ); + assert_eq!( + "http://127.0.0.1:8080", + normalize_api_endpoint("127.0.0.1:8080").unwrap() + ); + assert_eq!("http://[::1]", normalize_api_endpoint("[::1]").unwrap()); + assert_eq!( + "http://[::1]:8080", + normalize_api_endpoint("[::1]:8080").unwrap() + ); + } +} diff --git a/packages/toolchain/toolchain/src/util/build.rs b/packages/toolchain/toolchain/src/util/build.rs new file mode 100644 index 0000000000..f3dcfc8660 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/build.rs @@ -0,0 +1,44 @@ +use anyhow::*; +use rivet_api::models::{ActorBuildCompression, ActorBuildKind}; +use std::path::Path; + +use crate::{config, util::lz4}; + +/// Generates the file name that holds the build tar. +pub fn file_name(kind: ActorBuildKind, compression: ActorBuildCompression) -> String { + let file_name = match kind { + ActorBuildKind::DockerImage => "image", + ActorBuildKind::OciBundle => "oci-bundle", + ActorBuildKind::Javascript => "js-bundle", + }; + let file_ext = "tar"; + let file_ext_compression = match compression { + ActorBuildCompression::None => "", + ActorBuildCompression::Lz4 => ".lz4", + }; + format!("{file_name}.{file_ext}{file_ext_compression}") +} + +/// Compresses a given file with the given build compression. This is used as the last step in the +/// build process before uploading the output file. +pub async fn compress_build( + input_path: &Path, + compression: config::build::Compression, +) -> Result { + // Compress the bundle + let compressed_file = tempfile::NamedTempFile::new()?; + let compressed_file_path = compressed_file.into_temp_path(); + match compression { + config::build::Compression::None => { + tokio::fs::rename(&input_path, &compressed_file_path).await?; + } + config::build::Compression::Lz4 => { + let input_path = input_path.to_owned(); + let compressed_file_path = compressed_file_path.to_owned(); + tokio::task::spawn_blocking(move || lz4::compress(&input_path, &compressed_file_path)) + .await??; + } + } + + Ok(compressed_file_path) +} diff --git a/packages/toolchain/toolchain/src/util/cmd.rs b/packages/toolchain/toolchain/src/util/cmd.rs new file mode 100644 index 0000000000..769853d5ad --- /dev/null +++ b/packages/toolchain/toolchain/src/util/cmd.rs @@ -0,0 +1,130 @@ +use anyhow::*; +use tokio::process::Command; + +use crate::util::task; + +/// Runs a command in a cross-platform compatible way. +pub async fn run(task: task::TaskCtx, command: &str, envs: Vec<(String, String)>) -> Result<()> { + if cfg!(unix) { + let mut cmd = Command::new("/bin/sh"); + cmd.envs(envs).arg("-c").arg(command); + let build_status = task.spawn_cmd(cmd).await?; + ensure!(build_status.success(), "command failed"); + } else if cfg!(windows) { + let mut cmd = Command::new("cmd.exe"); + cmd.envs(envs).arg("/C").arg(command); + let build_status = task.spawn_cmd(cmd).await?; + ensure!(build_status.success(), "command failed"); + } else { + bail!("unknown machine type, expected unix or windows") + } + + Ok(()) +} + +/// Run a Docker command with full output. +pub async fn execute_docker_cmd( + task: task::TaskCtx, + command: tokio::process::Command, + error_message: impl std::fmt::Display, +) -> Result<()> { + match task.spawn_cmd(command).await { + Result::Ok(status) => { + if !status.success() { + bail!( + "{error_message} ({})\n\nValidate that Docker is installed and running.", + status + ); + } + Ok(()) + } + Err(err) => { + // TODO: This will not correctly handle this error anymore + // if let std::io::ErrorKind::NotFound = err.kind() { + // bail!("Docker not installed, install at https://docs.docker.com/get-docker/") + // } else { + // Err(err.into()) + // } + Err(err) + } + } +} + +/// Run a Docker command without output. +pub async fn execute_docker_cmd_silent( + command: tokio::process::Command, + error_message: impl std::fmt::Display, +) -> Result { + let output = execute_docker_cmd_silent_fallible(command).await?; + error_for_output_failure(&output, error_message)?; + Ok(output) +} + +/// Run a Docker command without output and ignore failures. +pub async fn execute_docker_cmd_silent_fallible( + mut command: tokio::process::Command, +) -> Result { + match command.output().await { + Result::Ok(output) => Ok(output), + Err(err) => { + if let std::io::ErrorKind::NotFound = err.kind() { + bail!("Docker not installed, install at https://docs.docker.com/get-docker/") + } else { + Err(err.into()) + } + } + } +} + +/// Throw an error if the output of a command failed. +pub fn error_for_output_failure( + output: &std::process::Output, + error_message: impl std::fmt::Display, +) -> Result<()> { + if !output.status.success() { + bail!( + "{error_message} ({})\n\nstdout:\n{}\n\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(()) +} + +pub fn shell_cmd(cmd: &str) -> Command { + tokio::process::Command::from(shell_cmd_std(cmd)) +} + +pub fn shell_cmd_std(cmd: &str) -> std::process::Command { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + use windows::Win32::System::Threading::CREATE_NO_WINDOW; + let mut cmd = std::process::Command::new(cmd); + cmd.creation_flags(CREATE_NO_WINDOW.0); + cmd + } + + #[cfg(not(windows))] + { + // Load the user's profile & shell on Linux in order to ensure we have the correct $PATH + let shell = std::env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh")); + + let mut shell_cmd = std::process::Command::new(&shell); + shell_cmd + // Load profile + .arg("-l") + // Load rc file + .arg("-i") + .arg("-c") + // Will accept the cmd & all following args + .arg("\"$@\"") + // This arg is ignored + .arg("noop") + // Pass the actual command + .arg(cmd); + shell_cmd + } +} diff --git a/packages/toolchain/toolchain/src/util/docker/archive.rs b/packages/toolchain/toolchain/src/util/docker/archive.rs new file mode 100644 index 0000000000..6976a8159f --- /dev/null +++ b/packages/toolchain/toolchain/src/util/docker/archive.rs @@ -0,0 +1,428 @@ +use anyhow::*; +use serde::Deserialize; +use serde_json::json; +use std::io::Read; +use typed_path::{TryAsRef, UnixPath}; +use uuid::Uuid; + +use crate::{ + config::{self}, + util::{ + cmd::{self, shell_cmd, shell_cmd_std}, + task, + }, +}; + +pub async fn create_archive( + task: task::TaskCtx, + image_tag: &str, + build_kind: config::build::docker::BundleKind, + build_compression: config::build::Compression, + allow_root: bool, +) -> Result { + task.log(format!( + "[Archiving Image] {} {}", + build_kind.as_ref(), + build_compression.as_ref() + )); + + // Build archive + let build_tar_path = match build_kind { + config::build::docker::BundleKind::DockerImage => { + archive_docker_image(task, &image_tag).await? + } + config::build::docker::BundleKind::OciBundle => { + archive_oci_bundle(task, &image_tag, allow_root).await? + } + }; + + // Compress archive + let compressed_path = + crate::util::build::compress_build(build_tar_path.as_ref(), build_compression).await?; + + Ok(compressed_path) +} + +/// Save Docker image +async fn archive_docker_image(task: task::TaskCtx, image_tag: &str) -> Result { + let build_tar_path = tempfile::NamedTempFile::new()?.into_temp_path(); + + let mut build_cmd = shell_cmd("docker"); + build_cmd + .arg("save") + .arg("--output") + .arg(&build_tar_path) + .arg(&image_tag); + cmd::execute_docker_cmd(task, build_cmd, "Docker failed to save image").await?; + + // We have no way of validating that this Docker image is not running as root, so the container + // will fail on setup at runtime if attempting to run as UID/GID 0. + + Ok(build_tar_path) +} + +/// Convert the Docker image to an OCI bundle +/// +/// This entire operation works by manipulating TAR files without touching the +/// host's file system in order to preserve file permissions & ownership on +/// Windows. +async fn archive_oci_bundle( + task: task::TaskCtx, + image_tag: &str, + allow_root: bool, +) -> Result { + // Create OCI bundle + let oci_bundle_tar_file = tempfile::NamedTempFile::new()?; + let mut oci_bundle_archive = tar::Builder::new(oci_bundle_tar_file); + + // Create container and copy files to rootfs + let image_tag_clone = image_tag.to_owned(); + let ( + mut oci_bundle_archive, + CopyContainerToRootFsOutput { + passwd_file, + group_file, + }, + ) = tokio::task::spawn_blocking(move || -> Result<_> { + let output = copy_container_to_rootfs(&mut oci_bundle_archive, &image_tag_clone)?; + Result::Ok((oci_bundle_archive, output)) + }) + .await??; + + // Convert Docker image to OCI bundle + // + // See umoci implementation: https://github.com/opencontainers/umoci/blob/312b2db3028f823443d6a74d86b05f65701b0d0e/oci/config/convert/runtime.go#L183 + { + #[derive(Debug, Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerImage { + config: DockerImageConfig, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerImageConfig { + cmd: Option>, + entrypoint: Option>, + env: Vec, + user: String, + #[serde(default)] + working_dir: String, + } + + // Inspect image + let mut inspect_cmd = shell_cmd("docker"); + inspect_cmd.arg("image").arg("inspect").arg(&image_tag); + let inspect_output = cmd::execute_docker_cmd_silent_fallible(inspect_cmd).await?; + let image = serde_json::from_slice::>(&inspect_output.stdout)?; + let image = image.into_iter().next().context("no image")?; + + // Read config + let mut config = serde_json::from_slice::(include_bytes!( + "../../../static/oci-bundle-config.base.json" + ))?; + + // WORKDIR + // + // https://github.com/opencontainers/umoci/blob/312b2db3028f823443d6a74d86b05f65701b0d0e/oci/config/convert/runtime.go#L144 + if image.config.working_dir != "" { + config["process"]["cwd"] = json!(image.config.working_dir); + } else { + config["process"]["cwd"] = json!("/"); + } + + // ENV + // + // https://github.com/opencontainers/umoci/blob/312b2db3028f823443d6a74d86b05f65701b0d0e/oci/config/convert/runtime.go#L149 + config["process"]["env"] = json!(image.config.env); + + // ENTRYPOINT + CMD + // + // https://github.com/opencontainers/umoci/blob/312b2db3028f823443d6a74d86b05f65701b0d0e/oci/config/convert/runtime.go#L157 + let args = std::iter::empty::() + .chain(image.config.entrypoint.into_iter().flatten()) + .chain(image.config.cmd.into_iter().flatten()) + .collect::>(); + config["process"]["args"] = json!(args); + + // USER + // + // https://github.com/opencontainers/umoci/blob/312b2db3028f823443d6a74d86b05f65701b0d0e/oci/config/convert/runtime.go#L174 + // + // Moby passwd parser: https://github.com/moby/sys/blob/c0711cde08c8fa33857a2c28721659267f49b5e2/user/user.go + // + // If you're you're the guy at Docker who decided to reimplement passwd in Go for funzies, please reconsider next time. + { + // If Docker did not specify a user + let no_dockerfile_user = image.config.user.is_empty(); + + // Parse user and group from the Dockerfile + let (dockerfile_user, dockerfile_group) = + if let Some((u, g)) = image.config.user.split_once(":") { + (u, Some(g)) + } else { + (image.config.user.as_str(), None) + }; + + // Attempt to parse the user and group as integers. If the uid is + // not specified explicitly, the uid and associated gid will be + // looked up in the passwd & groups file below. + let dockerfile_user_int = dockerfile_user.parse::().ok(); + let dockerfile_group_int = dockerfile_group.and_then(|x| x.parse::().ok()); + + // Parse passwd file and find user + let users = super::users::read_passwd_file( + passwd_file.as_ref().map(|x| x.as_str()).unwrap_or_default(), + )?; + let exec_user = users.iter().find(|x| { + dockerfile_user_int.map_or(false, |uid| x.uid == uid) || x.name == dockerfile_user + }); + + // Determine uid + let uid = if no_dockerfile_user { + 0 + } else if let Some(exec_user) = exec_user { + exec_user.uid + } else if let Some(uid) = dockerfile_user_int { + uid + } else { + task.log(format!("Warning: Cannot determine uid {} not in passwd file. Please specify a raw uid like `USER 1000:1000`.", image.config.user)); + 0 + }; + + // Parse group file and find group + let groups = super::users::read_group_file( + group_file.as_ref().map(|x| x.as_str()).unwrap_or_default(), + )?; + let exec_group = groups.iter().find(|x| { + if let Some(group) = dockerfile_group { + // Group specified explicitly in Dockerfile + + if let Some(gid) = dockerfile_group_int { + return x.gid == gid; + } else { + x.name == group + } + } else if let Some(exec_user) = &exec_user { + // Group not specified, assume by the gid in the passwd or if the user is in the user list + exec_user.gid == x.gid || x.user_list.contains(&exec_user.name) + } else { + false + } + }); + + // Determine gid + let gid = if no_dockerfile_user { + 0 + } else if let Some(exec_group) = exec_group { + exec_group.gid + } else if let Some(gid) = dockerfile_group_int { + gid + } else { + task.log(format!("Warning: Cannot determine gid. {} not in group file, please specify a raw uid & gid like `USER 1000:1000`", image.config.user)); + + 0 + }; + + // Validate not running as root + // + // See Kubernetes implementation https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/pkg/kubelet/kuberuntime/security_context_others.go#L44C4-L44C4 + if !allow_root { + if uid == 0 { + bail!("cannot run Docker container as root user (i.e. uid 0) for security. see https://rivet.gg/docs/dynamic-servers/concepts/docker-root-user") + } + if gid == 0 { + bail!("cannot run Docker container as root group (i.e. gid 0) for security. see https://rivet.gg/docs/dynamic-servers/concepts/docker-root-user") + } + } + + // Specify user + config["process"]["user"]["uid"] = json!(uid); + config["process"]["user"]["gid"] = json!(gid); + + // Add home if needed + if let Some(home) = exec_user.as_ref().map(|x| x.home.as_str()) { + if !home.is_empty() { + config["process"]["env"] + .as_array_mut() + .unwrap() + .push(json!(format!("HOME={home}"))); + } + } + } + + // Write config.json + { + let config_buf = serde_json::to_vec_pretty(&config)?; + + let mut header = tar::Header::new_gnu(); + header.set_path("config.json")?; + header.set_size(config_buf.len() as u64); + header.set_cksum(); + + oci_bundle_archive.append(&header, config_buf.as_slice())?; + } + } + + // Finish archive + let oci_bundle_tar_file = oci_bundle_archive.into_inner()?; + + Ok(oci_bundle_tar_file.into_temp_path()) +} + +struct CopyContainerToRootFsOutput { + passwd_file: Option, + group_file: Option, +} + +/// Copies files out of the container in to a TAR builder for the OCI bundle +/// under `rootfs`. +fn copy_container_to_rootfs( + oci_bundle_archive: &mut tar::Builder, + image_tag: &str, +) -> Result { + let container_name = format!("rivet-game-{}", Uuid::new_v4()); + + // Files that we need to scrape from the bundle for metadata + let mut passwd_file = Option::::None; + let mut group_file = Option::::None; + + // Create container for the image so we can copy files out of it + let create_cmd = shell_cmd_std("docker") + .arg("container") + .arg("create") + .arg("--name") + .arg(&container_name) + .arg(&image_tag) + .output()?; + ensure!( + create_cmd.status.success(), + "failed to create container:\n{}", + String::from_utf8_lossy(create_cmd.stderr.as_slice()) + ); + + // Copy files out of container as a TAR stream + let mut cp_cmd = shell_cmd_std("docker"); + cp_cmd + .arg("container") + .arg("cp") + .arg("--archive") + .arg(format!("{container_name}:/")) + .arg("-") + .stdout(std::process::Stdio::piped()); + let mut cp_child = cp_cmd.spawn()?; + + let root_path = UnixPath::new("rootfs"); + + // Read TAR stream and copy in to OCI bundle + let rootfs_stream = cp_child.stdout.take().context("cp_child.stdout.take()")?; + let mut archive = tar::Archive::new(rootfs_stream); + for entry in archive.entries()? { + let mut entry = entry?; + let mut header = entry.header().clone(); + + // Update headers to point to rootfs + // + // Don't use `header.set_path` because `builder.append_data` will handle long path names for + // us + let old_path_bytes = header.path_bytes(); + let old_path = UnixPath::new(old_path_bytes.as_ref() as &[u8]); + let old_path_relative = old_path.strip_prefix("/")?; + let new_path = root_path.join(old_path_relative); + let new_path_std = new_path.try_as_ref().context(format!( + "failed to convert unix path to os path: {new_path:?}" + ))?; + + // Read any files that we need to scrape for metadata + // + // We return the data we read to memory so we can write it to the OCI + // bundle archive. If we tried to run `read_to_string` on the entry + // again, it would return nothing. + let read_data = match old_path + .to_str() + .context("failed to match old_path as str")? + { + "/etc/passwd" => { + let mut s = String::new(); + entry.read_to_string(&mut s)?; + passwd_file = Some(s.clone()); + Some(s) + } + "/etc/group" => { + let mut s = String::new(); + entry.read_to_string(&mut s)?; + group_file = Some(s.clone()); + Some(s) + } + _ => None, + }; + + // Update hard links to point to new rootfs. + // + // Hard links are resolved when TAR extracts the archive, so these need to point to the new + // location within the archive. + // + // Soft links don't need to be updated (specifically for absolute paths). This is because + // runc will chroot in to the rootfs and resolve the soft link once the container is running from there. For example, + // an absolute symlink to `/foo/bar` within runc (i.e. chroot) will + // resolve to `./rootfs/bin/foo`. + if header.entry_type().is_hard_link() { + if let Some(old_link) = header.link_name_bytes() { + let old_link = UnixPath::new(old_link.as_ref() as &[u8]); + + if old_link.is_absolute() { + let old_link_relative = old_link.strip_prefix("/")?; + let new_link = root_path.join(old_link_relative); + let new_link_std = new_link.try_as_ref().context(format!( + "failed to convert unix path to os path: {new_link:?}" + ))?; + + // We have to use `append_link` because it allows us to pass long + // paths. + oci_bundle_archive.append_link(&mut header, new_path_std, new_link_std)?; + + continue; + } + } + } + + // Write entry data to TAR + // + // We have to use `append_data` specifically because it allows us to pass long paths. + if let Some(read_data) = read_data { + oci_bundle_archive.append_data( + &mut header, + new_path_std, + std::io::Cursor::new(read_data), + )?; + } else { + oci_bundle_archive.append_data(&mut header, new_path_std, &mut entry)?; + } + } + + // Wait for cp to finish + let cp_output = cp_child.wait_with_output()?; + ensure!( + cp_output.status.success(), + "failed to copy files out of container:\n{}", + String::from_utf8_lossy(cp_output.stderr.as_slice()) + ); + + // Clean up container + let rm_cmd = shell_cmd_std("docker") + .arg("container") + .arg("rm") + .arg("--force") + .arg(&container_name) + .output()?; + ensure!( + rm_cmd.status.success(), + "failed to remove container:\n{}", + String::from_utf8_lossy(rm_cmd.stderr.as_slice()) + ); + + Result::Ok(CopyContainerToRootFsOutput { + passwd_file, + group_file, + }) +} diff --git a/packages/toolchain/toolchain/src/util/docker/build.rs b/packages/toolchain/toolchain/src/util/docker/build.rs new file mode 100644 index 0000000000..fb343b8513 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/docker/build.rs @@ -0,0 +1,196 @@ +use anyhow::*; +use std::{collections::HashMap, path::Path}; + +use crate::{ + config::{self}, + toolchain_ctx::ToolchainCtx, + util::{ + cmd::{self, shell_cmd}, + task, + }, +}; + +pub struct BuildImageOutput { + pub tag: String, + pub path: tempfile::TempPath, +} + +/// Builds an image and archives it to a path. +pub async fn build_image( + ctx: &ToolchainCtx, + task: task::TaskCtx, + build_path: &Path, + dockerfile: &Path, + build_method: config::build::docker::BuildMethod, + build_kind: config::build::docker::BundleKind, + build_compression: config::build::Compression, + build_args: Option<&[String]>, + build_target: Option<&str>, + allow_root: bool, +) -> Result { + // Determine build method + let build_method = if build_method == config::build::docker::BuildMethod::Buildx { + // Validate that Buildx is installed + let mut buildx_version_cmd = shell_cmd("docker"); + buildx_version_cmd.args(&["buildx", "version"]); + let buildx_version = cmd::execute_docker_cmd_silent_fallible(buildx_version_cmd).await?; + + if buildx_version.status.success() { + config::build::docker::BuildMethod::Buildx + } else { + task.log("Docker Buildx not installed. Falling back to native build method.\n\nPlease install Buildx here: https://github.com/docker/buildx#installing"); + config::build::docker::BuildMethod::Native + } + } else { + build_method + }; + + let buildx_info = match build_method { + config::build::docker::BuildMethod::Native => " (with native)", + config::build::docker::BuildMethod::Buildx => " (with buildx)", + }; + task.log(format!("[Building] {}{buildx_info}", dockerfile.display())); + + // Build args + let mut build_arg_flags = HashMap::::new(); + if let Some(build_args) = build_args { + for item in build_args { + let (k, v) = item + .split_once('=') + .context("Build arg missing '=': {item}")?; + ensure!( + !k.starts_with("RIVET_"), + "Build arg must not start with 'RIVET_': {k}" + ); + build_arg_flags.insert(k.into(), v.into()); + } + } + build_arg_flags.insert("RIVET_API_ENDPOINT".into(), ctx.api_endpoint.clone()); + + // Build image + let image_tag = super::generate_unique_image_tag(); + match build_method { + config::build::docker::BuildMethod::Native => { + let mut build_cmd = shell_cmd("docker"); + build_cmd + .arg("build") + .arg("--platform") + .arg("linux/amd64") + .arg("--file") + .arg(dockerfile) + .arg("--tag") + .arg(&image_tag) + .args( + &build_arg_flags + .iter() + .map(|(k, v)| format!("--build-arg={}={}", k, v)) + .collect::>(), + ); + if let Some(build_target) = build_target { + build_cmd.arg("--target").arg(build_target); + } + build_cmd.arg(build_path); + cmd::execute_docker_cmd( + task.clone(), + build_cmd, + "Docker image failed to build (native)", + ) + .await?; + } + config::build::docker::BuildMethod::Buildx => { + let builder_name = "rivet_toolchain"; + + // Determine if needs to create a new builder + let mut inspect_cmd = shell_cmd("docker"); + inspect_cmd.arg("buildx").arg("inspect").arg(builder_name); + let inspect_output = cmd::execute_docker_cmd_silent_fallible(inspect_cmd).await?; + + if !inspect_output.status.success() + && String::from_utf8(inspect_output.stderr.clone())? + .contains(&format!("no builder \"{builder_name}\" found")) + { + // Create new builder + + let mut build_cmd = shell_cmd("docker"); + build_cmd + .arg("buildx") + .arg("create") + .arg("--name") + .arg(builder_name) + .arg("--driver") + .arg("docker-container") + .arg("--platform") + .arg("linux/amd64"); + cmd::execute_docker_cmd( + task.clone(), + build_cmd, + "Failed to create Docker Buildx builder", + ) + .await?; + } else { + // Builder exists + + cmd::error_for_output_failure( + &inspect_output, + "Failed to inspect Docker Buildx runner", + )?; + } + + // Build image + let mut build_cmd = shell_cmd("docker"); + build_cmd + .arg("buildx") + .arg("build") + .arg("--builder") + .arg(builder_name) + .arg("--platform") + .arg("linux/amd64") + .arg("--file") + .arg(dockerfile) + .arg("--tag") + .arg(&image_tag) + .args( + &build_arg_flags + .iter() + .map(|(k, v)| format!("--build-arg={}={}", k, v)) + .collect::>(), + ) + .arg("--output") + .arg("type=docker"); + if let Some(build_target) = build_target { + build_cmd.arg("--target").arg(build_target); + } + build_cmd.arg(build_path); + cmd::execute_docker_cmd( + task.clone(), + build_cmd, + "Docker image failed to build (buildx)", + ) + .await?; + } + } + + // Build archive + let build_tar_path = super::archive::create_archive( + task.clone(), + &image_tag, + build_kind, + build_compression, + allow_root, + ) + .await?; + + // Clean up image from the registry + let mut remove_img_cmd = shell_cmd("docker"); + remove_img_cmd + .arg("image") + .arg("rm") + .arg("--force") + .arg(&image_tag); + cmd::execute_docker_cmd_silent_fallible(remove_img_cmd).await?; + + Ok(BuildImageOutput { + tag: image_tag, + path: build_tar_path, + }) +} diff --git a/packages/toolchain/toolchain/src/util/docker/mod.rs b/packages/toolchain/toolchain/src/util/docker/mod.rs new file mode 100644 index 0000000000..666f2fb929 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/docker/mod.rs @@ -0,0 +1,11 @@ +pub mod archive; +pub mod build; +pub mod push; +pub mod users; + +use uuid::Uuid; + +/// Generates a unique image tag for the image being pushed or built. +pub fn generate_unique_image_tag() -> String { + format!("rivet-game:{}", Uuid::new_v4()) +} diff --git a/packages/toolchain/toolchain/src/util/docker/push.rs b/packages/toolchain/toolchain/src/util/docker/push.rs new file mode 100644 index 0000000000..07ea81ac92 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/docker/push.rs @@ -0,0 +1,137 @@ +use anyhow::*; +use futures_util::stream::{StreamExt, TryStreamExt}; +use rivet_api::{apis, models}; +use serde::Serialize; +use std::{path::PathBuf, sync::Arc}; +use tokio::fs; +use uuid::Uuid; + +use crate::{ + config, + project::environment::TEMPEnvironment, + toolchain_ctx::ToolchainCtx, + util::{net::upload, task, term}, +}; + +pub struct PushOpts { + pub config: config::Config, + pub env: TEMPEnvironment, + + /// Path to already created tar. + pub path: PathBuf, + + /// Docker inside the image. + pub docker_tag: String, + + /// Name of the image + pub name: Option, + + pub bundle: config::build::docker::BundleKind, + + pub compression: config::build::Compression, +} + +#[derive(Serialize)] +pub struct PushOutput { + pub build_id: Uuid, +} + +pub async fn push_tar( + ctx: &ToolchainCtx, + task: task::TaskCtx, + push_opts: &PushOpts, +) -> Result { + let reqwest_client = Arc::new(reqwest::Client::new()); + + // Inspect the image + let image_file_meta = fs::metadata(&push_opts.path) + .await + .with_context(|| anyhow!("failed to open image file: {}", push_opts.path.display()))?; + ensure!(image_file_meta.len() > 0, "docker image archive is empty"); + + // Create image + let display_name = push_opts + .name + .clone() + .unwrap_or_else(|| push_opts.docker_tag.clone()); + let content_type = "binary/octet-stream"; + + task.log(format!( + "[Uploading] {name} ({size})", + name = display_name, + size = upload::format_file_size(image_file_meta.len())? + )); + + let build_kind = match push_opts.bundle { + config::build::docker::BundleKind::DockerImage => models::ActorBuildKind::DockerImage, + config::build::docker::BundleKind::OciBundle => models::ActorBuildKind::OciBundle, + }; + + let build_compression = match push_opts.compression { + config::build::Compression::None => models::ActorBuildCompression::None, + config::build::Compression::Lz4 => models::ActorBuildCompression::Lz4, + }; + + let build_res = apis::actor_builds_api::actor_builds_prepare( + &ctx.openapi_config_cloud, + models::ActorPrepareBuildRequest { + name: display_name.clone(), + image_tag: Some(push_opts.docker_tag.clone()), + image_file: Box::new(models::UploadPrepareFile { + path: crate::util::build::file_name(build_kind, build_compression), + content_type: Some(content_type.into()), + content_length: image_file_meta.len() as i64, + }), + kind: Some(build_kind), + compression: Some(build_compression), + }, + Some(&ctx.project.name_id), + Some(&push_opts.env.slug), + ) + .await; + if let Err(err) = build_res.as_ref() { + task.log(format!("{err:?}")) + } + let build_res = build_res.context("build_res")?; + let build_id = build_res.build; + let pb = term::EitherProgressBar::Multi(term::multi_progress_bar(task.clone())); + + // Upload chunks in parallel + futures_util::stream::iter(build_res.presigned_requests) + .map(|presigned_request| { + let task = task.clone(); + let reqwest_client = reqwest_client.clone(); + let pb = pb.clone(); + + async move { + upload::upload_file( + task.clone(), + &reqwest_client, + &presigned_request, + &push_opts.path, + Some(content_type), + pb, + ) + .await + } + }) + .buffer_unordered(8) + .try_collect::>() + .await?; + + let complete_res = apis::actor_builds_api::actor_builds_complete( + &ctx.openapi_config_cloud, + &build_res.build.to_string(), + Some(&ctx.project.name_id), + Some(&push_opts.env.slug), + ) + .await; + if let Err(err) = complete_res.as_ref() { + task.log(format!("{err:?}")); + } + complete_res.context("complete_res")?; + + Ok(PushOutput { + build_id: build_id.to_owned(), + }) +} diff --git a/packages/toolchain/toolchain/src/util/docker/users.rs b/packages/toolchain/toolchain/src/util/docker/users.rs new file mode 100644 index 0000000000..705b000901 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/docker/users.rs @@ -0,0 +1,77 @@ +// See https://github.com/moby/sys/blob/c0711cde08c8fa33857a2c28721659267f49b5e2/user/user.go + +use anyhow::*; + +// MARK: passwd +#[derive(Debug)] +pub struct User { + pub name: String, + pub uid: u32, + pub gid: u32, + pub home: String, + pub shell: String, +} + +pub fn read_passwd_file(passwd_file: &str) -> Result> { + let mut users = Vec::new(); + for line in passwd_file.lines() { + if let Some(user) = parse_passwd_line(&line) { + users.push(user); + } + } + + Ok(users) +} + +fn parse_passwd_line(line: &str) -> Option { + let fields: Vec<&str> = line.split(':').collect(); + if fields.len() < 7 { + return None; + } + + let uid: u32 = fields[2].parse().ok()?; + let gid: u32 = fields[3].parse().ok()?; + + Some(User { + name: fields[0].to_string(), + uid, + gid, + home: fields[5].to_string(), + shell: fields[6].to_string(), + }) +} + +// MARK: groups +#[derive(Debug)] +pub struct Group { + pub name: String, + pub gid: u32, + pub user_list: Vec, +} + +pub fn read_group_file(group_file: &str) -> Result> { + let mut groups = Vec::new(); + for line in group_file.lines() { + if let Some(group) = parse_group_line(&line) { + groups.push(group); + } + } + + Ok(groups) +} + +fn parse_group_line(line: &str) -> Option { + let fields: Vec<&str> = line.split(':').collect(); + if fields.len() < 4 { + return None; + } + + let gid: u32 = fields[2].parse().ok()?; + let user_list: Vec = fields[3].split(',').map(|s| s.to_string()).collect(); + + Some(Group { + name: fields[0].to_string(), + gid, + user_list, + }) +} diff --git a/packages/toolchain/toolchain/src/util/js_utils/mod.rs b/packages/toolchain/toolchain/src/util/js_utils/mod.rs new file mode 100644 index 0000000000..c087ab278f --- /dev/null +++ b/packages/toolchain/toolchain/src/util/js_utils/mod.rs @@ -0,0 +1,134 @@ +use anyhow::*; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::{collections::HashMap, path::PathBuf}; +use tokio::process::Command; + +use crate::{paths, util::task}; + +pub mod schemas; + +pub struct CommandOpts { + pub task_path: &'static str, + pub input: serde_json::Value, + pub env: HashMap, +} + +async fn base_url() -> Result { + // Attempt to read from user or default + let base_url = if let Some(url) = std::env::var("_RIVET_JS_UTILS_SRC_DIR").ok() { + url + } else { + rivet_js_utils_embed::src_path(&paths::data_dir()?) + .await? + .display() + .to_string() + }; + + let base_url = base_url.trim_end_matches('/').to_string(); + Ok(base_url) +} + +pub struct CommandRaw { + pub command: PathBuf, + pub args: Vec, + pub envs: HashMap, + pub current_dir: PathBuf, +} + +pub async fn build_backend_command_raw(opts: CommandOpts) -> Result { + let base_url = base_url().await?; + + // Get Deno executable + let deno = deno_embed::get_executable(&crate::paths::data_dir()?).await?; + + // Serialize command + let input_json = serde_json::to_string(&opts.input)?; + + // Run backend + Ok(CommandRaw { + command: deno.executable_path, + args: vec![ + "run".into(), + "--quiet".into(), + "--no-check".into(), + "--allow-net".into(), + "--allow-read".into(), + "--allow-env".into(), + "--allow-run".into(), + "--allow-write".into(), + "--allow-sys".into(), + "--config".into(), + format!("{base_url}/deno.jsonc"), + "--lock".into(), + format!("{base_url}/deno.lock"), + format!("{base_url}/{}", opts.task_path), + "--input".into(), + input_json, + ], + envs: opts.env, + current_dir: paths::project_root()?, + }) +} + +pub async fn build_backend_command(opts: CommandOpts) -> Result { + let cmd_raw = build_backend_command_raw(opts).await?; + let mut cmd = Command::new(cmd_raw.command); + cmd.kill_on_drop(true); + cmd.args(cmd_raw.args) + .envs(cmd_raw.envs) + .current_dir(cmd_raw.current_dir); + + Ok(cmd) +} + +pub async fn run_backend_command_from_task(task: task::TaskCtx, opts: CommandOpts) -> Result { + let cmd = build_backend_command(opts).await?; + let exit_code = task.spawn_cmd(cmd).await?; + Ok(exit_code.code().unwrap_or(0)) +} + +pub async fn run_command_and_parse_output( + js_task_path: &'static str, + input: &Input, +) -> Result { + let input_json = + serde_json::to_value(input).map_err(|err| anyhow!("Failed to serialize input: {err}"))?; + + let mut cmd = build_backend_command(CommandOpts { + task_path: js_task_path, + input: input_json, + env: HashMap::new(), + }) + .await + .map_err(|err| anyhow!("Failed to build command: {err}"))?; + + let output = cmd + .output() + .await + .map_err(|err| anyhow!("Failed to run command: {err}"))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + if let Some(last_line) = stdout.lines().rev().find(|line| !line.trim().is_empty()) { + serde_json::from_str(last_line).map_err(|err| { + anyhow!("Failed to parse JSON from output: {err}\nOutput: {last_line}") + }) + } else { + Err(anyhow!( + "No non-blank lines in output\nStdout: {stdout}\nStderr: {stderr}" + )) + } + } else { + let mut error_message = format!("Command failed with status: {}", output.status); + if !stdout.is_empty() { + error_message.push_str(&format!("\nstdout:\n{stdout}")); + } + if !stderr.is_empty() { + error_message.push_str(&format!("\nstderr:\n{stderr}")); + } + Err(anyhow!(error_message)) + } +} diff --git a/packages/toolchain/toolchain/src/util/js_utils/schemas.rs b/packages/toolchain/toolchain/src/util/js_utils/schemas.rs new file mode 100644 index 0000000000..2c4bd14129 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/js_utils/schemas.rs @@ -0,0 +1,39 @@ +pub mod build { + use serde::{Deserialize, Serialize}; + use std::path::PathBuf; + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Input { + pub entry_point: PathBuf, + pub out_dir: PathBuf, + pub deno: Deno, + pub bundle: Bundle, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Deno { + #[serde(skip_serializing_if = "Option::is_none")] + pub config_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub import_map_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lock_path: Option, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Bundle { + pub minify: bool, + pub analyze_result: bool, + pub log_level: String, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Output { + pub files: Vec, + pub analyzed_metafile: Option, + } +} diff --git a/packages/toolchain/toolchain/src/util/lz4.rs b/packages/toolchain/toolchain/src/util/lz4.rs new file mode 100644 index 0000000000..d9095594ce --- /dev/null +++ b/packages/toolchain/toolchain/src/util/lz4.rs @@ -0,0 +1,23 @@ +use anyhow::*; +use std::{ + fs::File, + io::{BufReader, BufWriter}, + path::Path, +}; + +pub fn compress(input_path: &Path, output_path: &Path) -> Result<()> { + let input_file = File::open(&input_path)?; + let mut reader = BufReader::new(input_file); + + let output_file = File::create(&output_path)?; + let writer = BufWriter::new(output_file); + + let mut encoder = lz4::EncoderBuilder::new().level(1).build(writer)?; + + // Pipe the bytes through the encoder + std::io::copy(&mut reader, &mut encoder)?; + + encoder.finish().1?; + + Ok(()) +} diff --git a/packages/toolchain/toolchain/src/util/mod.rs b/packages/toolchain/toolchain/src/util/mod.rs new file mode 100644 index 0000000000..9a365c6690 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/mod.rs @@ -0,0 +1,11 @@ +pub mod api; +pub mod build; +pub mod cmd; +pub mod docker; +pub mod js_utils; +pub mod lz4; +pub mod net; +pub mod os; +pub mod show_term; +pub mod task; +pub mod term; diff --git a/packages/toolchain/toolchain/src/util/net/download.rs b/packages/toolchain/toolchain/src/util/net/download.rs new file mode 100644 index 0000000000..5c71afc539 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/net/download.rs @@ -0,0 +1,90 @@ +use anyhow::*; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::Path, +}; +use tempfile::TempDir; +use zip::ZipArchive; + +/// Downloads a ZIP file, extracts a directory from it, and copies it to the destination. +pub fn zip(url: &str, src_dir_relative: &Path, dest_dir: &Path) -> Result<()> { + ensure!(src_dir_relative.is_relative(), "src_dir must be relative"); + + let temp_dir = TempDir::new()?; + let temp_path = temp_dir.path().join("archive.zip"); + + // Download the zip + download(url, &temp_path)?; + + // Delete destination if it exists + if dest_dir.is_dir() { + fs::remove_dir_all(&dest_dir)?; + } + + // Unzip the file + extract(&temp_path, &src_dir_relative, &dest_dir)?; + + Ok(()) +} + +/// Downloads the ZIP file to a temp path. +fn download(url: &str, dest: &Path) -> Result<()> { + let response = reqwest::blocking::get(url) + .context("error fetching zip")? + .error_for_status() + .context("error status fetching zip")?; + let bytes = response.bytes().context("failed to get zip body")?; + let mut out = File::create(dest).with_context(|| { + anyhow!( + "failed to create download destionatin file: {}", + dest.display() + ) + })?; + out.write_all(&bytes)?; + + Ok(()) +} + +/// Extracts the contents of the directory. +/// +/// # Arguments +/// +/// * `archive_path` - The path to the archive file. +/// * `inner_dir` - The directory inside the archive to copy from. +/// * `extract_to` - The path to extract the directory to. +fn extract(archive_path: &Path, inner_dir: &Path, extract_to: &Path) -> Result<()> { + let file = fs::File::open(archive_path) + .with_context(|| anyhow!("failed to open archive: {}", archive_path.display()))?; + let mut archive = ZipArchive::new(file)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let file_name = file.enclosed_name().context("unenclosed file name")?; + + // Filter files that are not in the src_dir + let Result::Ok(file_name) = file_name.strip_prefix(inner_dir) else { + continue; + }; + + // Build dest path + let outpath = extract_to.join(file_name); + + // Copy file + if file.name().ends_with('/') { + fs::create_dir_all(&outpath) + .with_context(|| anyhow!("failed create dir: {}", outpath.display()))?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(&p) + .with_context(|| anyhow!("failed to create dir: {}", outpath.display()))?; + } + } + let mut outfile = fs::File::create(&outpath)?; + io::copy(&mut file, &mut outfile)?; + } + } + + Ok(()) +} diff --git a/packages/toolchain/toolchain/src/util/net/mod.rs b/packages/toolchain/toolchain/src/util/net/mod.rs new file mode 100644 index 0000000000..bba5af3992 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/net/mod.rs @@ -0,0 +1,2 @@ +pub mod download; +pub mod upload; diff --git a/packages/toolchain/toolchain/src/util/net/upload.rs b/packages/toolchain/toolchain/src/util/net/upload.rs new file mode 100644 index 0000000000..7f046c3ca4 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/net/upload.rs @@ -0,0 +1,263 @@ +use anyhow::*; +use console::style; +use futures_util::stream::StreamExt; +use rivet_api::models; +use std::{ + path::{Component, Path, PathBuf}, + time::{Duration, Instant}, +}; +use tokio::{ + fs::File, + io::{AsyncReadExt, AsyncSeekExt}, +}; +use tokio_util::io::ReaderStream; + +use crate::util::{task, term}; + +/// Prepared file that will be uploaded to S3. +#[derive(Clone)] +pub struct UploadFile { + pub absolute_path: PathBuf, + pub prepared: models::UploadPrepareFile, +} + +pub fn format_file_size(bytes: u64) -> Result { + use humansize::FileSize; + + let size = format!( + "{}", + bytes + .file_size(humansize::file_size_opts::BINARY) + .ok() + .context("bytes.file_size(...)")? + ); + Ok(size) +} + +/// Lists all files in a directory and returns the data required to upload them. +pub fn prepare_upload_dir(base_path: &Path) -> Result> { + let mut files = Vec::::new(); + + // Walk files while respecting .rivet-cdn-ignore + let walk = ignore::WalkBuilder::new(base_path) + .standard_filters(false) + .add_custom_ignore_filename(".rivet-cdn-ignore") + .parents(true) + .build(); + for entry in walk { + let entry = entry?; + let file_meta = entry.metadata()?; + + if file_meta.is_file() { + let file_path = entry.path(); + + files.push(prepare_upload_file( + file_path, + file_path.strip_prefix(base_path)?, + file_meta, + )?); + } + } + + Ok(files) +} + +pub fn prepare_upload_file, Q: AsRef>( + absolute_path: P, + upload_path: Q, + metadata: std::fs::Metadata, +) -> Result { + let absolute_path = absolute_path.as_ref(); + + // Convert path to Unix-style string + let path_str = upload_path + .as_ref() + .components() + .filter_map(|c| match c { + Component::Normal(name) => name.to_str().map(str::to_string), + _ => None, + }) + .collect::>() + .join("/"); + + // Attempt to guess the MIME type + let content_type = mime_guess::from_path(&absolute_path) + .first_raw() + .map(str::to_string); + + Ok(UploadFile { + absolute_path: absolute_path.to_path_buf(), + prepared: models::UploadPrepareFile { + path: path_str, + content_type, + content_length: metadata.len() as i64, + }, + }) +} + +/// Uploads a file to a given URL. +pub async fn upload_file( + task: task::TaskCtx, + reqwest_client: &reqwest::Client, + presigned_req: &models::UploadPresignedRequest, + file_path: impl AsRef, + content_type: Option, + main_pb: term::EitherProgressBar, +) -> Result<()> { + let content_type = content_type.map(|x| x.to_string()); + let path = presigned_req.path.clone(); + + let is_tty = console::Term::buffered_stderr().is_term(); + let mut pb_added = false; + let pb = match &main_pb { + term::EitherProgressBar::Single(pb) => pb.clone(), + term::EitherProgressBar::Multi(_) => term::progress_bar(task.clone()), + }; + + // Try the upload multiple times since DigitalOcean spaces is incredibly + // buggy and spotty internet connections may cause issues. This is + // especially important since we have files that we need to batch upload, so + // one failing request is bad. + let mut attempts = 0; + let (upload_time, total_size) = 'upload: loop { + let pb = pb.clone(); + + // Read file + let mut file = File::open(file_path.as_ref()).await.with_context(|| { + anyhow!( + "failed to open file to upload: {}", + file_path.as_ref().display() + ) + })?; + let file_meta = file.metadata().await?; + let file_len = file_meta.len(); + + let total_size = presigned_req.content_length as u64; + let is_multipart = total_size != file_len; + + let msg = if is_multipart { + format!("{path} {}", style("[CHUNK]").dim().blue(),) + } else { + path.clone() + }; + + // Add progress bar + match &main_pb { + term::EitherProgressBar::Single(_) => {} + term::EitherProgressBar::Multi(mpb) => { + pb.reset(); + pb.set_message(msg); + pb.set_length(total_size); + + // Hack to fix weird bug with `MultiProgress` where it renders an empty progress bar and leaves + // it there + if !pb_added { + // pb.set_draw_target(term::get_pb_draw_target(task.clone())); + mpb.add(pb.clone()); + + pb_added = true; + + if !is_tty { + task.log(format!( + "Uploading {path} ({})", + format_file_size(total_size)? + )); + } + } + } + } + + // Create a reader for the slice of the file we need to read + file.seek(tokio::io::SeekFrom::Start(presigned_req.byte_offset as u64)) + .await?; + let handle = file.take(presigned_req.content_length as u64); + + // Default buffer size is optimized for memory usage. Increase buffer for perf. + let mut reader_stream = ReaderStream::with_capacity(handle, 1024 * 1024); + + let start = Instant::now(); + + // Process the stream with upload progress + let pb2 = pb.clone(); + let async_stream = async_stream::stream! { + while let Some(chunk) = reader_stream.next().await { + if let Result::Ok(chunk) = &chunk { + pb2.inc(chunk.len() as u64); + } + + yield chunk; + } + }; + + let body = reqwest::Body::wrap_stream(async_stream); + + // Upload file + let mut req = reqwest_client + .put(&presigned_req.url) + .header("content-length", presigned_req.content_length); + if let Some(content_type) = &content_type { + req = req.header("content-type", content_type.to_string()); + } + let res = req.body(body).send().await?; + if res.status().is_success() { + let upload_time = start.elapsed(); + break 'upload (upload_time, total_size); + } else { + if attempts > 4 { + let response_status = res.status(); + let response_text = res.text().await?; + bail!( + "failed to upload file: {}\n{:?}", + response_status, + response_text + ); + } else { + attempts += 1; + + let status = res.status(); + let body_text = res.text().await.context("res.text()")?; + + pb.set_style(term::pb_style_error()); + pb.set_message(format!( + "{}{}{} {path} {retry_and_body}", + style("[").bold().red(), + style(status).bold().red(), + style("]").bold().red(), + path = style(&path).red(), + retry_and_body = + style(format!("will retry (attempt #{attempts}): {body_text:?}")) + .dim() + .red(), + )); + + if !is_tty { + task.log( + "Error uploading {path} [{status}] (attempt #{attempts}): {body_text:?}", + ); + } + + tokio::time::sleep(Duration::from_secs(5)).await; + continue 'upload; + } + } + }; + + match &main_pb { + term::EitherProgressBar::Single(pb) => { + pb.set_message(format!("Uploaded {path}")); + } + term::EitherProgressBar::Multi(_) => { + pb.set_position(total_size); + pb.finish(); + } + } + + if !is_tty { + task.log(format!( + "Finished uploading {path} ({:.3}s)", + upload_time.as_secs_f64() + )); + } + + Ok(()) +} diff --git a/packages/toolchain/toolchain/src/util/os.rs b/packages/toolchain/toolchain/src/util/os.rs new file mode 100644 index 0000000000..ed2d64e270 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/os.rs @@ -0,0 +1,18 @@ +pub fn is_linux() -> bool { + std::env::consts::OS == "linux" +} + +#[cfg(target_os = "linux")] +pub fn is_root() -> bool { + nix::unistd::Uid::current().is_root() +} + +#[cfg(not(target_os = "linux"))] +pub fn is_root() -> bool { + false +} + +/// There are a lot of edge cases for Linux root that we need to frequently handle. +pub fn is_linux_and_root() -> bool { + is_linux() && is_root() +} diff --git a/packages/toolchain/toolchain/src/util/show_term.rs b/packages/toolchain/toolchain/src/util/show_term.rs new file mode 100644 index 0000000000..b952abafb8 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/show_term.rs @@ -0,0 +1,240 @@ +use anyhow::*; +use std::process::{Child, Command}; + +#[cfg(target_os = "linux")] +struct Terminal { + /// The name of the terminal emulator command + name: &'static str, + /// The flag to pass the command to the terminal emulator + prompt_str: &'static [&'static str], +} + +/// Terminals that don't work (note, more work might make them work): +/// +/// - guake (runs the whole window, doesn't handle closing) +/// - upterm (doesn't have an arg to pass a command it) +/// - x-terminal-emulator +/// - tilda (doesn't show automatically) +/// - terminator (issues running the command) +/// - xfce4-terminal (issues running the command) +#[cfg(target_os = "linux")] +const TERMINALS: [Terminal; 7] = [ + Terminal { + name: "kitty", + prompt_str: &["-e"], + }, + Terminal { + name: "konsole", + prompt_str: &["-e"], + }, + Terminal { + name: "gnome-terminal", + prompt_str: &["--"], + }, + Terminal { + name: "st", + prompt_str: &["-e"], + }, + Terminal { + name: "tilix", + prompt_str: &["-e"], + }, + Terminal { + name: "urxvt", + prompt_str: &["-e"], + }, + Terminal { + name: "xterm", + prompt_str: &["-e"], + }, +]; + +pub async fn show_term(args: &[String]) -> Result { + #[cfg(target_os = "windows")] + let child: Child = Command::new("cmd.exe") + .arg("/C") + .arg("start") + .arg("cmd.exe") + .arg("/K") + .args(args) + .spawn() + .expect("cmd.exe failed to start"); + + #[cfg(target_os = "macos")] + let child: Child = { + // This script will run from home, so we need top change to the + // project directory before running the command. + let current_dir = std::env::current_dir()? + .into_os_string() + .into_string() + .unwrap(); + + // Create script tempfile + let script_temp_file = tempfile::Builder::new() + .prefix("rivet_term_") + .suffix(".command") + .tempfile()?; + let script_path = script_temp_file.path().to_path_buf(); + script_temp_file.keep()?; + + // Write the script content to the script file + let command_to_run = format!( + "cd \"{}\" && {} && rm \"{}\"", + shell_escape(¤t_dir), + args.iter() + .map(|x| shell_escape(x)) + .collect::>() + .join(" "), + shell_escape(&script_path.display().to_string()), + ); + std::fs::write(&script_path, format!("#!/bin/bash\n{}", command_to_run))?; + std::fs::set_permissions( + &script_path, + std::os::unix::fs::PermissionsExt::from_mode(0o755), + )?; + + // Use `open` to run the script + Command::new("open") + .arg(&script_path) + .spawn() + .expect("Failed to open script") + }; + + #[cfg(target_os = "linux")] + let child: Child = { + let mut args = args.to_vec(); + + // TODO(forest): For Linux, the code is trying to find an + // available terminal emulator from a predefined list and + // then run the command in it. However, the way to run a + // command in a terminal emulator can vary between different + // emulators. The -e flag used here might not work for all + // of them. + let mut command = None; + + for terminal in TERMINALS { + if which::which(terminal.name).is_ok() { + command = Some(terminal); + break; + } + } + + match command { + Some(terminal) => { + // See if they have bash installed. If not, fallback to sh + let shell = if which::which("bash").is_ok() { + "bash" + } else { + "sh" + }; + + // Insert the flag --inside-terminal right after `sidekick` + // in the args. The only args before it are the binary path + // to the binary and `sidekick` itself, so it can go at the + // 2nd index. + args.insert(2, "--inside-terminal".to_string()); + + // Add a "press any key to continue" message to the end of + // the arguments to be run + args.append( + vec![ + "&&", + "read", + "-n", + "1", + "-s", + "-r", + "-p", + "\"Press any key to continue\"", + ] + .iter() + .map(|x| x.to_string()) + .collect::>() + .as_mut(), + ); + + args = vec![args.join(" ")]; + + Command::new(terminal.name) + // This is the flag to run a command in the + // terminal. Most will use -e, but some might use + // something different. + .args(terminal.prompt_str) + // We pass everything to a shell manually so that we can + // pass an entire string of the rest of the commands. + // This is more consistent across terminals on linux. + .arg(shell) + .arg("-c") + .args(&args) + .spawn() + .expect("Terminal emulator failed to start") + } + None => { + panic!("No terminal emulator found"); + } + } + }; + + Ok(child) +} + +#[cfg(target_os = "macos")] +fn shell_escape(s: &str) -> String { + if s.is_empty() { + return String::from("''"); + } + if !s.contains(|c: char| c.is_whitespace() || "[]{}()*;'\"\\|<>~&^$?!`".contains(c)) { + return s.to_string(); + } + let mut result = String::with_capacity(s.len() + 2); + result.push('\''); + for c in s.chars() { + if c == '\'' { + result.push_str("'\\''"); + } else { + result.push(c); + } + } + result.push('\''); + result +} + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + use std::process::Command; + + use super::TERMINALS; + + #[test] + #[ignore] + /// This test makes sure that the configuration to run a command in each + /// terminal works. It shouldn't run in CI, since it would be difficult to + /// configure. It can be run locally if each terminal in the const is + /// installed. + fn test_terminals() { + for terminal in TERMINALS { + let file_name = format!("{}.txt", terminal.name); + + let mut args = Vec::new(); + + args.push(format!("touch {}", file_name)); + + let output = Command::new(terminal.name) + .args(terminal.prompt_str) + .args(&args) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success(), "Command failed: {}", terminal.name); + + let file_path = Path::new(&file_name); + assert!(file_path.exists(), "File does not exist: {}", file_name); + + // Clean up the file + fs::remove_file(file_path).expect("Failed to remove file"); + } + } +} diff --git a/packages/toolchain/toolchain/src/util/task/ctx.rs b/packages/toolchain/toolchain/src/util/task/ctx.rs new file mode 100644 index 0000000000..277fc31b20 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/ctx.rs @@ -0,0 +1,105 @@ +use anyhow::*; +use std::{process::Stdio, sync::Arc}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command, + sync::{broadcast, mpsc}, +}; + +use super::TaskEvent; + +// HACK: Tokio bug drops the channel using the native `UnboundedSender::clone`, so we have to use +// `Arc`. +pub type TaskCtx = Arc; + +/// Logs to arbitrary files asynchronously. +/// +/// Allows us to store separate log files for different tasks that are running in a headless +/// context outside of a CLI. +pub struct TaskCtxInner { + log_tx: mpsc::UnboundedSender, + #[allow(dead_code)] + shutdown_rx: broadcast::Receiver<()>, +} + +impl TaskCtxInner { + pub fn new( + log_tx: mpsc::UnboundedSender, + shutdown_rx: broadcast::Receiver<()>, + ) -> Arc { + Arc::new(Self { + log_tx, + shutdown_rx, + }) + } + + pub fn log(&self, message: impl ToString) { + let _ = self.log_tx.send(TaskEvent::Log(message.to_string())); + } + + pub fn result(&self, result: &Result) -> Result<()> { + let result_serialized = result.as_ref().map_err(|x| x.to_string()); + let result_json = serde_json::to_string(&result_serialized)?; + let result_raw_value = serde_json::value::RawValue::from_string(result_json)?; + ensure!( + self.log_tx + .send(TaskEvent::Result { + result: result_raw_value + }) + .is_ok(), + "failed to write result" + ); + Ok(()) + } + + pub fn event(&self, event: TaskEvent) { + if matches!(event, TaskEvent::Log(_) | TaskEvent::Result { .. }) { + eprintln!("don't directly call event with logs or results, use .log() or .result()"); + return; + } + + let _ = self.log_tx.send(event); + } + + pub async fn spawn_cmd(self: &Arc, mut cmd: Command) -> Result { + // Required in case this task is cancelled + cmd.kill_on_drop(true); + + // Configure the command to pipe stdout and stderr + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + // Spawn the command + let mut child = cmd.spawn()?; + + // Take ownership of the stdout and stderr handles + let stdout = child.stdout.take().expect("Failed to capture stdout"); + let stderr = child.stderr.take().expect("Failed to capture stderr"); + + // Create buffered readers + let mut stdout_reader = BufReader::new(stdout).lines(); + let mut stderr_reader = BufReader::new(stderr).lines(); + + // Clone the logger for use in the spawned tasks + let stdout_logger = self.clone(); + let stderr_logger = self.clone(); + + // Spawn tasks to handle stdout and stderr + tokio::spawn(async move { + while let Result::Ok(Some(line)) = stdout_reader.next_line().await { + stdout_logger.log(line); + } + }); + + tokio::spawn(async move { + while let Result::Ok(Some(line)) = stderr_reader.next_line().await { + stderr_logger.log(line); + } + }); + + // Wait for the command to finish and get its exit status + let status = child.wait().await?; + + Ok(status) + } +} diff --git a/packages/toolchain/toolchain/src/util/task/event.rs b/packages/toolchain/toolchain/src/util/task/event.rs new file mode 100644 index 0000000000..963f3cf9ee --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/event.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +pub enum TaskEvent { + #[serde(rename = "log")] + Log(String), + #[serde(rename = "result")] + Result { + result: Box, + }, +} diff --git a/packages/toolchain/toolchain/src/util/task/mod.rs b/packages/toolchain/toolchain/src/util/task/mod.rs new file mode 100644 index 0000000000..622ce374c1 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/mod.rs @@ -0,0 +1,11 @@ +mod ctx; +pub mod event; +mod registry; +mod run; +mod task; + +pub use ctx::*; +pub use event::*; +pub use registry::*; +pub use run::*; +pub use task::*; diff --git a/packages/toolchain/toolchain/src/util/task/registry.rs b/packages/toolchain/toolchain/src/util/task/registry.rs new file mode 100644 index 0000000000..ebb273dcda --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/registry.rs @@ -0,0 +1,28 @@ +pub struct RunTaskJsonOutput { + pub success: bool, +} + +/// Used to auto-generate code for each task in order to dynamically dispatch. +/// +/// Used to run tasks with raw input/output string. This is useful for binding tasks to non-Rust +/// environments, such as raw dylibs or odd engines. +#[macro_export] +macro_rules! task_registry { + ( $( $task:ty ),* $(,)? ) => { + pub async fn run_task_json(run_config: $crate::util::task::RunConfig, name: &str, input_json: &str) -> $crate::util::task::RunTaskJsonOutput { + $( + if name == <$task as $crate::util::task::Task>::name() { + let input = serde_json::from_str::<<$task as $crate::util::task::Task>::Input>(&input_json) + .expect("deserialize task input"); + let output = $crate::util::task::run_task::<$task>(run_config, input).await; + let success = output.is_ok(); + return $crate::util::task::RunTaskJsonOutput { + success, + }; + } + )* + + panic!("unknown task {name}") + } + }; +} diff --git a/packages/toolchain/toolchain/src/util/task/run.rs b/packages/toolchain/toolchain/src/util/task/run.rs new file mode 100644 index 0000000000..2f1f84ccea --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/run.rs @@ -0,0 +1,79 @@ +use anyhow::*; +use tokio::sync::{broadcast, mpsc}; + +use super::{Task, TaskCtxInner, TaskEvent}; + +use crate::errors; + +/// Run config passed to the task. +pub struct RunConfig { + pub abort_rx: mpsc::Receiver<()>, + pub shutdown_tx: broadcast::Sender<()>, + pub shutdown_rx: broadcast::Receiver<()>, + pub event_tx: mpsc::UnboundedSender, +} + +/// Handlers used to interface with the task while running. +pub struct RunHandles { + pub abort_tx: mpsc::Sender<()>, + pub shutdown_rx: broadcast::Receiver<()>, + pub event_rx: mpsc::UnboundedReceiver, +} + +impl RunConfig { + pub fn build() -> (RunConfig, RunHandles) { + let (abort_tx, abort_rx) = mpsc::channel(1); + let (shutdown_tx, shutdown_rx) = broadcast::channel(1); + let shutdown_rx2 = shutdown_tx.subscribe(); + let (event_tx, event_rx) = mpsc::unbounded_channel(); + + ( + RunConfig { + abort_rx, + shutdown_tx, + shutdown_rx: shutdown_rx2, + event_tx, + }, + RunHandles { + abort_tx, + shutdown_rx, + event_rx, + }, + ) + } +} + +pub async fn run_task(run_config: RunConfig, input: T::Input) -> Result +where + T: Task, +{ + // Setup config + let RunConfig { + mut abort_rx, + shutdown_tx, + shutdown_rx, + event_tx, + } = run_config; + + // Create context + let task_ctx = TaskCtxInner::new(event_tx, shutdown_rx); + + // Run task or wait for abort + tokio::select! { + result = T::run(task_ctx.clone(), input) => { + // Write result + task_ctx.result(&result)?; + + // Shutdown + shutdown_tx.send(())?; + + result + }, + _ = abort_rx.recv() => { + // Shutdown + shutdown_tx.send(())?; + + Err(errors::GracefulExit.into()) + }, + } +} diff --git a/packages/toolchain/toolchain/src/util/task/task.rs b/packages/toolchain/toolchain/src/util/task/task.rs new file mode 100644 index 0000000000..aa1e050313 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/task/task.rs @@ -0,0 +1,13 @@ +use anyhow::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::future::Future; + +use super::TaskCtx; + +pub trait Task { + type Input: DeserializeOwned; + type Output: Serialize; + + fn name() -> &'static str; + fn run(ctx: TaskCtx, input: Self::Input) -> impl Future>; +} diff --git a/packages/toolchain/toolchain/src/util/term.rs b/packages/toolchain/toolchain/src/util/term.rs new file mode 100644 index 0000000000..87f93f2cd3 --- /dev/null +++ b/packages/toolchain/toolchain/src/util/term.rs @@ -0,0 +1,130 @@ +use console::{style, StyledObject}; +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; +use std::{fmt, io, time::Duration}; + +use crate::util::task; + +pub fn link(msg: impl ToString) -> StyledObject { + style(msg.to_string()).italic().underlined() +} + +#[derive(Clone)] +pub enum EitherProgressBar { + Single(ProgressBar), + Multi(MultiProgress), +} + +pub fn multi_progress_bar(_task: task::TaskCtx) -> MultiProgress { + let pb = MultiProgress::new(); + // pb.set_draw_target(get_pb_draw_target(task)); + pb +} + +pub fn progress_bar(_task: task::TaskCtx) -> ProgressBar { + // Don't draw the first iteration until the pb is styled + let pb = ProgressBar::hidden(); + pb.set_style(pb_style_file(false)); + // pb.set_draw_target(get_pb_draw_target(task)); + pb.enable_steady_tick(Duration::from_millis(1000)); + pb +} + +pub fn pb_style_file(include_spinner: bool) -> ProgressStyle { + ProgressStyle::default_bar() + .progress_chars("=> ") + .template(&if include_spinner { + format!( + "{{spinner:.dim}} {}{{eta:.dim}}{} [{{bar:23}}] {{percent:.bold}}{} {}{{bytes:.dim}}{}{{total_bytes:.dim}}{} {{binary_bytes_per_sec:.dim}}{} {{wide_msg}}", + style("(T-").dim(), + style(")").dim(), + style("%").bold(), + style("(").dim(), + style("/").dim(), + style(",").dim(), + style(")").dim(), + ) + } else { + format!( + "{}{{eta:3.dim}} [{{bar:12}}] {{percent:.bold}}{} {}{{bytes:.dim}}{}{{total_bytes:.dim}}{} {{binary_bytes_per_sec:.dim}}{} {{wide_msg}}", + style("T-").dim(), + style("%").bold(), + style("(").dim(), + style("/").dim(), + style(",").dim(), + style(")").dim(), + ) + }) + .expect("invalid progress bar style") +} + +pub fn pb_style_error() -> ProgressStyle { + ProgressStyle::default_bar() + .template(&format!("{} {{wide_msg:.red}}", style("!").bold().red())) + .expect("invalid progress bar style") +} + +pub fn get_pb_draw_target(task: task::TaskCtx) -> ProgressDrawTarget { + indicatif::ProgressDrawTarget::term_like(Box::new(TaskDrawTarget::new(task))) +} + +pub struct TaskDrawTarget { + task: task::TaskCtx, +} + +impl fmt::Debug for TaskDrawTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TaskDrawTarget").finish_non_exhaustive() + } +} + +impl TaskDrawTarget { + pub fn new(task: task::TaskCtx) -> TaskDrawTarget { + TaskDrawTarget { task } + } +} + +impl indicatif::TermLike for TaskDrawTarget { + fn width(&self) -> u16 { + 80 + } + + fn move_cursor_up(&self, _n: usize) -> io::Result<()> { + Ok(()) + } + + fn move_cursor_down(&self, _n: usize) -> io::Result<()> { + Ok(()) + } + + fn move_cursor_right(&self, _n: usize) -> io::Result<()> { + Ok(()) + } + + fn move_cursor_left(&self, _n: usize) -> io::Result<()> { + Ok(()) + } + + fn write_line(&self, s: &str) -> io::Result<()> { + let s = s.trim(); + if !s.is_empty() { + self.task.log(s); + } + Ok(()) + } + + fn write_str(&self, s: &str) -> io::Result<()> { + let s = s.trim(); + if !s.is_empty() { + self.task.log(s); + } + Ok(()) + } + + fn clear_line(&self) -> io::Result<()> { + Ok(()) + } + + fn flush(&self) -> io::Result<()> { + Ok(()) + } +} diff --git a/packages/toolchain/toolchain/static/oci-bundle-config.base.json b/packages/toolchain/toolchain/static/oci-bundle-config.base.json new file mode 100644 index 0000000000..f257c2481c --- /dev/null +++ b/packages/toolchain/toolchain/static/oci-bundle-config.base.json @@ -0,0 +1,178 @@ +{ + "ociVersion": "1.0.2-dev", + "process": { + "terminal": true, + "user": { + "uid": 0, + "gid": 0 + }, + "args": [ + "sh" + ], + "env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm" + ], + "cwd": "/", + "capabilities": { + "bounding": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "effective": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "permitted": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "ambient": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ] + }, + "rlimits": [ + { + "type": "RLIMIT_NOFILE", + "hard": 1024, + "soft": 1024 + } + ], + "noNewPrivileges": true + }, + "root": { + "path": "rootfs", + "readonly": true + }, + "hostname": "runc", + "mounts": [ + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + { + "destination": "/dev", + "type": "tmpfs", + "source": "tmpfs", + "options": [ + "nosuid", + "strictatime", + "mode=755", + "size=65536k" + ] + }, + { + "destination": "/dev/pts", + "type": "devpts", + "source": "devpts", + "options": [ + "nosuid", + "noexec", + "newinstance", + "ptmxmode=0666", + "mode=0620", + "gid=5" + ] + }, + { + "destination": "/dev/shm", + "type": "tmpfs", + "source": "shm", + "options": [ + "nosuid", + "noexec", + "nodev", + "mode=1777", + "size=65536k" + ] + }, + { + "destination": "/dev/mqueue", + "type": "mqueue", + "source": "mqueue", + "options": [ + "nosuid", + "noexec", + "nodev" + ] + }, + { + "destination": "/sys", + "type": "sysfs", + "source": "sysfs", + "options": [ + "nosuid", + "noexec", + "nodev", + "ro" + ] + }, + { + "destination": "/sys/fs/cgroup", + "type": "cgroup", + "source": "cgroup", + "options": [ + "nosuid", + "noexec", + "nodev", + "relatime", + "ro" + ] + } + ], + "linux": { + "resources": { + "devices": [ + { + "allow": false, + "access": "rwm" + } + ] + }, + "namespaces": [ + { + "type": "pid" + }, + { + "type": "network" + }, + { + "type": "ipc" + }, + { + "type": "uts" + }, + { + "type": "mount" + }, + { + "type": "cgroup" + } + ], + "maskedPaths": [ + "/proc/acpi", + "/proc/asound", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/sys/firmware", + "/proc/scsi" + ], + "readonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + } +} \ No newline at end of file diff --git a/scripts/cargo/update_workspace.ts b/scripts/cargo/update_workspace.ts index 282463b67a..7b04623db1 100755 --- a/scripts/cargo/update_workspace.ts +++ b/scripts/cargo/update_workspace.ts @@ -15,6 +15,7 @@ async function updateCargoToml() { for await (const entry of walk(join(rootDir, "packages"), { includeDirs: false, exts: ["toml"], + skip: [/node_modules/], })) { yield entry; } @@ -53,6 +54,9 @@ async function updateCargoToml() { // Hardcode extra workspace members members.push("sdks/api/full/rust"); + // Sort deps + members.sort(); + // Remove path dependencies, since we'll replace these. This lets us // preserve existing external dependencies. const existingDependencies = workspaceToml.workspace?.dependencies || {};