diff --git a/Cargo.lock b/Cargo.lock index 528e919..9049f15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,18 +14,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.7", - "generic-array 0.14.7", -] - -[[package]] -name = "aead" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" -dependencies = [ - "crypto-common 0.2.2", - "inout 0.2.2", + "crypto-common", + "generic-array", ] [[package]] @@ -35,20 +25,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aes" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" -dependencies = [ - "cipher 0.5.2", - "cpubits", - "cpufeatures 0.3.1", - "zeroize", + "cipher", + "cpufeatures", ] [[package]] @@ -57,35 +35,20 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", + "aead", + "aes", + "cipher", + "ctr", + "ghash", "subtle", ] -[[package]] -name = "aes-gcm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" -dependencies = [ - "aead 0.6.1", - "aes 0.9.3", - "cipher 0.5.2", - "ctr 0.10.1", - "ctutils", - "ghash 0.6.0", - "zeroize", -] - [[package]] name = "agent-browser" version = "0.25.0" source = "git+https://github.com/steel-dev/agent-browser?branch=as-lib#eb8e722560432a2ad0229604d3db82c16cda1253" dependencies = [ - "aes-gcm 0.10.3", + "aes-gcm", "async-trait", "base64", "chrono", @@ -93,14 +56,14 @@ dependencies = [ "futures-util", "getrandom 0.2.17", "hex", - "hmac 0.12.1", + "hmac", "image", "libc", "reqwest", "rust-embed", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "similar", "socket2", "time", @@ -222,18 +185,6 @@ dependencies = [ "syn", ] -[[package]] -name = "argon2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.3.1", - "password-hash", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -325,35 +276,12 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bcrypt-pbkdf" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" -dependencies = [ - "blowfish", - "pbkdf2 0.13.0", - "sha2 0.11.0", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -390,32 +318,13 @@ dependencies = [ "no_std_io2", ] -[[package]] -name = "blake2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" -dependencies = [ - "digest 0.11.3", -] - [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", - "zeroize", + "generic-array", ] [[package]] @@ -424,26 +333,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "block-padding" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "blowfish" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" -dependencies = [ - "byteorder", - "cipher 0.5.2", + "generic-array", ] [[package]] @@ -488,16 +378,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "cbc" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" -dependencies = [ - "cipher 0.5.2", + "cipher", ] [[package]] @@ -524,19 +405,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cipher 0.5.2", - "cpufeatures 0.3.1", - "rand_core 0.10.1", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.44" @@ -556,20 +424,8 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.7", - "inout 0.1.4", -] - -[[package]] -name = "cipher" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", - "inout 0.2.2", - "zeroize", + "crypto-common", + "inout", ] [[package]] @@ -621,12 +477,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - [[package]] name = "color_quant" version = "1.1.0" @@ -651,24 +501,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -678,15 +516,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -727,109 +556,24 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" -dependencies = [ - "cpubits", - "ctutils", - "getrandom 0.4.2", - "hybrid-array", - "num-traits", - "rand_core 0.10.1", - "serdect", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "getrandom 0.4.2", - "hybrid-array", - "rand_core 0.10.1", -] - -[[package]] -name = "crypto-primes" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" -dependencies = [ - "crypto-bigint", - "rand_core 0.10.1", -] - [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "ctr" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" -dependencies = [ - "cipher 0.5.2", -] - -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", - "subtle", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "curve25519-dalek-derive", - "digest 0.11.3", - "fiat-crypto", - "rand_core 0.10.1", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "cipher", ] [[package]] @@ -856,28 +600,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "delegate" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "der" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.8" @@ -887,15 +609,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "des" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "dialoguer" version = "0.12.0" @@ -914,23 +627,11 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", + "block-buffer", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid", - "crypto-common 0.2.2", - "ctutils", -] - [[package]] name = "dirs" version = "5.0.1" @@ -984,93 +685,18 @@ dependencies = [ "syn", ] -[[package]] -name = "ecdsa" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" -dependencies = [ - "der", - "digest 0.11.3", - "elliptic-curve", - "rfc6979", - "signature", - "spki", - "zeroize", -] - -[[package]] -name = "ed25519" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.10.1", - "serde", - "sha2 0.11.0", - "signature", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "elliptic-curve" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" -dependencies = [ - "base16ct", - "crypto-bigint", - "crypto-common 0.2.2", - "digest 0.11.3", - "ff", - "group", - "hkdf", - "hybrid-array", - "pem-rfc7468", - "pkcs8", - "rand_core 0.10.1", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equator" version = "0.4.2" @@ -1169,22 +795,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "ff" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" -dependencies = [ - "rand_core 0.10.1", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1327,17 +937,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "generic-array" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" -dependencies = [ - "generic-array 0.14.7", - "rustversion", - "typenum", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1372,13 +971,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 6.0.0", - "rand_core 0.10.1", "wasip2", "wasip3", - "wasm-bindgen", ] [[package]] @@ -1388,17 +984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval 0.6.2", -] - -[[package]] -name = "ghash" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" -dependencies = [ - "polyval 0.7.3", - "zeroize", + "polyval", ] [[package]] @@ -1411,17 +997,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "group" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" -dependencies = [ - "ff", - "rand_core 0.10.1", - "subtle", -] - [[package]] name = "h2" version = "0.4.19" @@ -1497,37 +1072,13 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" - -[[package]] -name = "hkdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" -dependencies = [ - "hmac 0.13.0", -] - [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -1575,18 +1126,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hybrid-array" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" -dependencies = [ - "ctutils", - "subtle", - "typenum", - "zeroize", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1840,18 +1379,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding 0.3.3", - "generic-array 0.14.7", -] - -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "block-padding 0.4.2", - "hybrid-array", + "block-padding", + "generic-array", ] [[package]] @@ -1982,26 +1511,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", -] - -[[package]] -name = "kem" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" -dependencies = [ - "crypto-common 0.2.2", - "rand_core 0.10.1", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2108,12 +1617,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "md5" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" - [[package]] name = "memchr" version = "2.8.0" @@ -2157,31 +1660,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "ml-kem" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" -dependencies = [ - "hybrid-array", - "kem", - "module-lattice", - "pkcs8", - "rand_core 0.10.1", - "sha3 0.11.0", -] - -[[package]] -name = "module-lattice" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" -dependencies = [ - "ctutils", - "hybrid-array", - "num-traits", -] - [[package]] name = "moxcms" version = "0.8.1" @@ -2198,18 +1676,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "no_std_io2" version = "0.9.4" @@ -2244,18 +1710,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.10.2", - "rand_core 0.10.1", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -2288,7 +1742,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", ] @@ -2347,67 +1801,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "p256" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primefield", - "primeorder", - "sha2 0.11.0", -] - -[[package]] -name = "p384" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" -dependencies = [ - "ecdsa", - "elliptic-curve", - "fiat-crypto", - "primefield", - "primeorder", - "sha2 0.11.0", -] - -[[package]] -name = "p521" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" -dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primefield", - "primeorder", - "sha2 0.11.0", -] - -[[package]] -name = "pageant" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d8eab09a361a4afe0b1668be978cd97e4f052e927a92b0b608cf902965d49ce" -dependencies = [ - "base16ct", - "byteorder", - "bytes", - "delegate", - "futures", - "log", - "rand 0.10.2", - "sha2 0.11.0", - "thiserror 2.0.18", - "tokio", - "windows", - "windows-strings", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -2431,15 +1824,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" -dependencies = [ - "phc", -] - [[package]] name = "paste" version = "1.0.15" @@ -2464,27 +1848,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - -[[package]] -name = "pbkdf2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" -dependencies = [ - "digest 0.11.3", - "hmac 0.13.0", -] - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", + "digest", + "hmac", ] [[package]] @@ -2493,16 +1858,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phc" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" -dependencies = [ - "base64ct", - "ctutils", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2515,45 +1870,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.8.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkcs5" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" -dependencies = [ - "aes 0.9.3", - "aes-gcm 0.11.1", - "cbc 0.2.1", - "der", - "pbkdf2 0.13.0", - "rand_core 0.10.1", - "scrypt", - "sha2 0.11.0", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der", - "pkcs5", - "rand_core 0.10.1", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2573,17 +1889,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "poly1305" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" -dependencies = [ - "cpufeatures 0.3.1", - "universal-hash 0.6.1", - "zeroize", -] - [[package]] name = "polyval" version = "0.6.2" @@ -2591,21 +1896,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "opaque-debug", - "universal-hash 0.5.1", -] - -[[package]] -name = "polyval" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" -dependencies = [ - "cpubits", - "cpufeatures 0.3.1", - "universal-hash 0.6.1", - "zeroize", + "universal-hash", ] [[package]] @@ -2657,33 +1950,6 @@ dependencies = [ "syn", ] -[[package]] -name = "primefield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" -dependencies = [ - "crypto-bigint", - "crypto-common 0.2.2", - "ff", - "rand_core 0.10.1", - "subtle", - "zeroize", -] - -[[package]] -name = "primeorder" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" -dependencies = [ - "elliptic-curve", - "once_cell", - "primefield", - "serdect", - "wnaf", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2866,17 +2132,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -2915,12 +2170,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rand_xorshift" version = "0.4.0" @@ -3102,16 +2351,6 @@ dependencies = [ "webpki-roots 1.0.6", ] -[[package]] -name = "rfc6979" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" -dependencies = [ - "crypto-bigint", - "hmac 0.13.0", -] - [[package]] name = "rgb" version = "0.8.53" @@ -3132,25 +2371,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.10.0-rc.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" -dependencies = [ - "const-oid", - "crypto-bigint", - "crypto-primes", - "digest 0.11.3", - "pkcs1", - "pkcs8", - "rand_core 0.10.1", - "sha2 0.11.0", - "signature", - "spki", - "zeroize", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -3176,101 +2396,6 @@ dependencies = [ "sqlite-wasm-rs", ] -[[package]] -name = "russh" -version = "0.63.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "036204edbd199552a5b3832f63c60dcdf395dc44c7f06b4af1c0e8139cc11bce" -dependencies = [ - "aes 0.9.3", - "bitflags", - "block-padding 0.4.2", - "byteorder", - "bytes", - "cbc 0.2.1", - "cipher 0.5.2", - "crypto-bigint", - "ctr 0.10.1", - "curve25519-dalek", - "data-encoding", - "delegate", - "der", - "digest 0.11.3", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", - "enum_dispatch", - "flate2", - "futures", - "generic-array 1.4.5", - "getrandom 0.4.2", - "ghash 0.6.0", - "hex-literal", - "hmac 0.13.0", - "inout 0.2.2", - "keccak", - "log", - "md5", - "ml-kem", - "module-lattice", - "num-bigint 0.5.1", - "p256", - "p384", - "p521", - "pageant", - "pbkdf2 0.13.0", - "pkcs1", - "pkcs5", - "pkcs8", - "polyval 0.7.3", - "rand 0.10.2", - "rand_core 0.10.1", - "ring", - "rsa", - "russh-cryptovec", - "russh-util", - "salsa20", - "scrypt", - "sec1", - "sha1 0.11.0", - "sha2 0.11.0", - "sha3 0.12.0", - "signature", - "spki", - "ssh-encoding", - "ssh-key", - "subtle", - "thiserror 2.0.18", - "tokio", - "typenum", - "universal-hash 0.6.1", - "zeroize", -] - -[[package]] -name = "russh-cryptovec" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" -dependencies = [ - "log", - "nix", - "ssh-encoding", - "windows-sys 0.61.2", -] - -[[package]] -name = "russh-util" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" -dependencies = [ - "chrono", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", -] - [[package]] name = "rust-embed" version = "8.11.0" @@ -3301,7 +2426,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2 0.10.9", + "sha2", "walkdir", ] @@ -3311,15 +2436,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.4" @@ -3392,16 +2508,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "salsa20" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" -dependencies = [ - "cfg-if", - "cipher 0.5.2", -] - [[package]] name = "same-file" version = "1.0.6" @@ -3417,32 +2523,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" -dependencies = [ - "cfg-if", - "pbkdf2 0.13.0", - "salsa20", - "sha2 0.11.0", -] - -[[package]] -name = "sec1" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" -dependencies = [ - "base16ct", - "ctutils", - "der", - "hybrid-array", - "subtle", - "zeroize", -] - [[package]] name = "semver" version = "1.0.27" @@ -3517,16 +2597,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serdect" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" -dependencies = [ - "base16ct", - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -3534,19 +2604,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -3556,40 +2615,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "sha3" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" -dependencies = [ - "digest 0.11.3", - "keccak", -] - -[[package]] -name = "sha3" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" -dependencies = [ - "digest 0.11.3", - "keccak", - "sponge-cursor", + "cpufeatures", + "digest", ] [[package]] @@ -3614,16 +2641,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" -dependencies = [ - "digest 0.11.3", - "rand_core 0.10.1", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -3667,22 +2684,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "spki" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sponge-cursor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" - [[package]] name = "sqlite-wasm-rs" version = "0.5.2" @@ -3695,65 +2696,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ssh-cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" -dependencies = [ - "aead 0.6.1", - "aes 0.9.3", - "aes-gcm 0.11.1", - "chacha20", - "cipher 0.5.2", - "ctutils", - "des", - "poly1305", - "ssh-encoding", - "zeroize", -] - -[[package]] -name = "ssh-encoding" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" -dependencies = [ - "base64ct", - "bytes", - "crypto-bigint", - "ctutils", - "digest 0.11.3", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "ssh-key" -version = "0.7.0-rc.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" -dependencies = [ - "argon2", - "bcrypt-pbkdf", - "ctutils", - "ed25519-dalek", - "hex", - "hmac 0.13.0", - "p256", - "p384", - "p521", - "rand_core 0.10.1", - "rsa", - "sec1", - "sha1 0.11.0", - "sha2 0.11.0", - "signature", - "ssh-cipher", - "ssh-encoding", - "zeroize", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3764,35 +2706,33 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "steel-cli" version = "0.5.0-preview.6" dependencies = [ - "aes 0.8.4", - "aes-gcm 0.10.3", + "aes", + "aes-gcm", "agent-browser", "anyhow", "base64", - "cbc 0.1.2", + "cbc", "clap", "clap_complete", "dialoguer", "dirs 6.0.0", "futures-util", "getrandom 0.4.2", - "hmac 0.12.1", + "hmac", "jiff", "libc", "open", "parking_lot", - "pbkdf2 0.12.2", + "pbkdf2", "proptest", "proptest-derive", "reqwest", "rusqlite", - "russh", - "rustix", "serde", "serde_json", "serde_yaml", - "sha1 0.10.6", - "sha2 0.10.9", + "sha1", + "sha2", "tempfile", "thiserror 2.0.18", "tokio", @@ -4137,7 +3077,7 @@ dependencies = [ "rand 0.8.5", "rustls", "rustls-pki-types", - "sha1 0.10.6", + "sha1", "thiserror 1.0.69", "utf-8", ] @@ -4156,7 +3096,7 @@ dependencies = [ "rand 0.9.2", "rustls", "rustls-pki-types", - "sha1 0.10.6", + "sha1", "thiserror 2.0.18", ] @@ -4208,20 +3148,10 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "subtle", ] -[[package]] -name = "universal-hash" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" -dependencies = [ - "crypto-common 0.2.2", - "ctutils", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -4515,27 +3445,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4549,17 +3458,6 @@ dependencies = [ "windows-strings", ] -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -4588,16 +3486,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -4700,15 +3588,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4958,18 +3837,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "wnaf" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" -dependencies = [ - "ff", - "group", - "hybrid-array", - "primefield", -] - [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 999f7b4..9bd0288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ aes-gcm = "0.10" getrandom = "0.4" libc = "0.2" -rustix = { version = "1", features = ["termios"] } tempfile = "3" jiff = "0.2" parking_lot = "0.12" @@ -52,7 +51,6 @@ parking_lot = "0.12" serde_yaml = "0.9" tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-webpki-roots"] } futures-util = "0.3.32" -russh = { version = "0.63", default-features = false, features = ["ring", "flate2", "rsa"] } [dev-dependencies] wiremock = "0.6" diff --git a/README.md b/README.md index ec7e324..ac203a0 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,6 @@ Supported shells: `bash`, `zsh`, `fish`, `powershell`, `elvish`. | API tools | `scrape`, `screenshot`, `pdf` | | Local runtime | `dev install`, `dev start`, `dev stop` | | Credentials | `credentials list`, `credentials create`, `credentials update`, `credentials delete` | -| Cloud computers | `computer create`, `computer exec`, `computer ssh`, `computer list`, `computer use` | -| Secrets | `secret list`, `secret create`, `secret get`, `secret update`, `secret delete` | -| Environments | `environment list`, `environment create`, `environment update`, `environment delete` | | Account and utility | `login`, `logout`, `config`, `doctor`, `cache`, `update`, `completion` | Full flags and schemas: [CLI reference](docs/cli-reference.md). diff --git a/deny.toml b/deny.toml index 559217d..60d6aa9 100644 --- a/deny.toml +++ b/deny.toml @@ -10,7 +10,6 @@ targets = [ # Transitive deps from agent-browser; we can't upgrade them directly. ignore = [ "RUSTSEC-2024-0436", # paste unmaintained - "RUSTSEC-2023-0071", # rsa Marvin timing attack; russh pulls rsa for host-key verification only, the CLI never holds an RSA private key ] [licenses] diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 2938656..04b1d4d 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -91,38 +91,6 @@ Steel CLI - browser automation for AI agents. This file is generated from `steel - [steel sessions logs](#steel-sessions-logs) - [steel sessions agent-logs](#steel-sessions-agent-logs) - [steel sessions traces](#steel-sessions-traces) -- [steel computer](#steel-computer) -- [steel computer create](#steel-computer-create) -- [steel computer list](#steel-computer-list) -- [steel computer get](#steel-computer-get) -- [steel computer delete](#steel-computer-delete) -- [steel computer pause](#steel-computer-pause) -- [steel computer resume](#steel-computer-resume) -- [steel computer stop](#steel-computer-stop) -- [steel computer start](#steel-computer-start) -- [steel computer restart](#steel-computer-restart) -- [steel computer use](#steel-computer-use) -- [steel computer exec](#steel-computer-exec) -- [steel computer ssh](#steel-computer-ssh) -- [steel computer checkpoint](#steel-computer-checkpoint) -- [steel computer quota](#steel-computer-quota) -- [steel checkpoint](#steel-checkpoint) -- [steel checkpoint list](#steel-checkpoint-list) -- [steel checkpoint get](#steel-checkpoint-get) -- [steel checkpoint delete](#steel-checkpoint-delete) -- [steel checkpoint restore](#steel-checkpoint-restore) -- [steel secret](#steel-secret) -- [steel secret list](#steel-secret-list) -- [steel secret create](#steel-secret-create) -- [steel secret get](#steel-secret-get) -- [steel secret update](#steel-secret-update) -- [steel secret delete](#steel-secret-delete) -- [steel environment](#steel-environment) -- [steel environment list](#steel-environment-list) -- [steel environment create](#steel-environment-create) -- [steel environment get](#steel-environment-get) -- [steel environment update](#steel-environment-update) -- [steel environment delete](#steel-environment-delete) - [steel init](#steel-init) - [steel login](#steel-login) - [steel logout](#steel-logout) @@ -1551,539 +1519,6 @@ steel sessions traces Example: `steel sessions traces --follow` Streaming: websocket `/v1/sessions/{id}/agent-traces` -## steel computer - -Cloud computers: create, run commands, ssh - -### Usage - -```bash -steel computer -``` - -### Subcommands - -- `create`: Create a computer -- `list`: List computers -- `get`: Get one computer -- `delete`: Delete a computer -- `pause`: Pause a running computer -- `resume`: Resume a paused computer -- `stop`: Stop a computer but keep its disk -- `start`: Start a stopped computer -- `restart`: Reboot a running computer -- `use`: Remember a computer as the default for other commands -- `exec`: Run one command in a computer -- `ssh`: Open an SSH session to a computer -- `checkpoint`: Save a computer as a checkpoint -- `quota`: Show how many computers and checkpoints you can have - -## steel computer create - -Create a computer - -### Usage - -```bash -steel computer create -``` - -### Parameters - -- `--template` (string, optional): Template name (defaults to the only one the API offers) -- `--vcpu` (string, optional): Number of vCPUs -- `--memory` (string, optional): Memory in MiB -- `--timeout` (string, optional): Delete the computer after this many seconds of running time -- `--auto-pause` (boolean, optional): Pause instead of deleting when the timeout is reached -- `--idle-timeout` (string, optional): Pause after this many seconds without incoming traffic (0 disables it) -- `--env` (string[], optional): Environment variable for the computer, repeatable -- `--secret` (string[], optional): Attach a stored secret as an environment variable, repeatable -- `--environment` (string, optional): Start from an environment; its spec and secrets apply unless overridden here -- `--project` (string, optional): Project to create the computer in (defaults to the API key's project) -- `--wait` (boolean, optional): Wait until the computer is running -- `--use` (boolean, optional): Make the new computer the default for other commands - -## steel computer list - -List computers - -### Usage - -```bash -steel computer list -``` - -## steel computer get - -Get one computer - -### Usage - -```bash -steel computer get -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - -## steel computer delete - -Delete a computer - -### Usage - -```bash -steel computer delete -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - -## steel computer pause - -Pause a running computer - -### Usage - -```bash -steel computer pause -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - -## steel computer resume - -Resume a paused computer - -### Usage - -```bash -steel computer resume -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `--wait` (boolean, optional): Wait until the computer reaches its new state - -## steel computer stop - -Stop a computer but keep its disk - -### Usage - -```bash -steel computer stop -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `--wait` (boolean, optional): Wait until the computer reaches its new state - -## steel computer start - -Start a stopped computer - -### Usage - -```bash -steel computer start -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `--wait` (boolean, optional): Wait until the computer reaches its new state - -## steel computer restart - -Reboot a running computer - -### Usage - -```bash -steel computer restart -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `--wait` (boolean, optional): Wait until the computer reaches its new state - -## steel computer use - -Remember a computer as the default for other commands - -### Usage - -```bash -steel computer use -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID to remember -- `--clear` (boolean, optional): Forget the remembered computer - -## steel computer exec - -Run one command in a computer - -### Usage - -```bash -steel computer exec -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `-c, --command` (string, optional): Shell command string, run by /bin/sh -c -- `--cwd` (string, optional): Working directory inside the computer -- `--env` (string[], optional): Environment variable for the command, repeatable -- `--timeout` (string, optional): Kill the command after this many seconds (at most 3600) -- `argv` (string[], optional): Program and arguments, given after `--` - -### API Operations - -- `exec_computer` (implemented): `POST /v1/computers/{id}/exec` - Example: `steel computer exec -- ` - -## steel computer ssh - -Open an SSH session to a computer - -### Usage - -```bash -steel computer ssh -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `command` (string[], optional): Run this command instead of opening a shell, given after `--` - -### API Operations - -- `attach_computer_ssh` (implemented): `GET /v1/computers/{id}/ssh` - Example: `steel computer ssh ` - Streaming: websocket `/v1/computers/{id}/ssh` - -## steel computer checkpoint - -Save a computer as a checkpoint - -### Usage - -```bash -steel computer checkpoint -``` - -### Parameters - -- `computer_id` (string, optional): Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) -- `--name` (string, optional): Name for the checkpoint -- `--wait` (boolean, optional): Wait until the checkpoint is ready - -## steel computer quota - -Show how many computers and checkpoints you can have - -### Usage - -```bash -steel computer quota -``` - -## steel checkpoint - -Computer checkpoints: list, restore, delete - -### Usage - -```bash -steel checkpoint -``` - -### Subcommands - -- `list`: List checkpoints -- `get`: Get one checkpoint -- `delete`: Delete a checkpoint -- `restore`: Start a new computer from a checkpoint - -## steel checkpoint list - -List checkpoints - -### Usage - -```bash -steel checkpoint list -``` - -## steel checkpoint get - -Get one checkpoint - -### Usage - -```bash -steel checkpoint get -``` - -### Parameters - -- `checkpoint_id` (string, required): Checkpoint ID - -## steel checkpoint delete - -Delete a checkpoint - -### Usage - -```bash -steel checkpoint delete -``` - -### Parameters - -- `checkpoint_id` (string, required): Checkpoint ID - -## steel checkpoint restore - -Start a new computer from a checkpoint - -### Usage - -```bash -steel checkpoint restore -``` - -### Parameters - -- `checkpoint_id` (string, required): Checkpoint ID -- `--timeout` (string, optional): Delete the computer after this many seconds of running time -- `--auto-pause` (boolean, optional): Pause instead of deleting when the timeout is reached -- `--idle-timeout` (string, optional): Pause after this many seconds without incoming traffic (0 disables it) -- `--wait` (boolean, optional): Wait until the computer is running -- `--use` (boolean, optional): Make the new computer the default for other commands - -## steel secret - -Secrets for computers: list, create, update, delete - -### Usage - -```bash -steel secret -``` - -### Subcommands - -- `list`: List secrets -- `create`: Store a new secret -- `get`: Get one secret's metadata -- `update`: Rename a secret or replace its value -- `delete`: Delete a secret - -## steel secret list - -List secrets - -### Usage - -```bash -steel secret list -``` - -### Parameters - -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel secret create - -Store a new secret - -### Usage - -```bash -steel secret create -``` - -### Parameters - -- `name` (string, required): Secret name, also the environment variable name inside computers -- `--value` (string, optional): The secret value -- `--value-stdin` (boolean, optional): Read the secret value from stdin -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel secret get - -Get one secret's metadata - -### Usage - -```bash -steel secret get -``` - -### Parameters - -- `secret_id` (string, required): Secret ID -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel secret update - -Rename a secret or replace its value - -### Usage - -```bash -steel secret update -``` - -### Parameters - -- `secret_id` (string, required): Secret ID -- `--name` (string, optional): New name -- `--value` (string, optional): New value -- `--value-stdin` (boolean, optional): Read the new value from stdin -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel secret delete - -Delete a secret - -### Usage - -```bash -steel secret delete -``` - -### Parameters - -- `secret_id` (string, required): Secret ID -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel environment - -Reusable computer environments: a spec plus secrets - -### Usage - -```bash -steel environment -``` - -### Subcommands - -- `list`: List environments -- `create`: Create an environment -- `get`: Get one environment -- `update`: Change an environment's name, spec or secrets -- `delete`: Delete an environment - -## steel environment list - -List environments - -### Usage - -```bash -steel environment list -``` - -### Parameters - -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel environment create - -Create an environment - -### Usage - -```bash -steel environment create -``` - -### Parameters - -- `name` (string, required): Environment name -- `--template` (string, optional): Template name -- `--vcpu` (string, optional): Number of vCPUs -- `--memory` (string, optional): Memory in MiB -- `--disk` (string, optional): Disk in MiB -- `--timeout` (string, optional): Delete computers after this many seconds of running time -- `--auto-pause` (boolean, optional): Pause instead of deleting when the timeout is reached -- `--env` (string[], optional): Environment variable for computers, repeatable -- `--secret` (string[], optional): Attach a stored secret as an environment variable, repeatable -- `--secret-value` (string[], optional): Store a new secret and attach it, repeatable -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel environment get - -Get one environment - -### Usage - -```bash -steel environment get -``` - -### Parameters - -- `environment_id` (string, required): Environment ID -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel environment update - -Change an environment's name, spec or secrets - -### Usage - -```bash -steel environment update -``` - -### Parameters - -- `environment_id` (string, required): Environment ID -- `--name` (string, optional): New name -- `--template` (string, optional): Template name -- `--vcpu` (string, optional): Number of vCPUs -- `--memory` (string, optional): Memory in MiB -- `--disk` (string, optional): Disk in MiB -- `--timeout` (string, optional): Delete computers after this many seconds of running time -- `--auto-pause` (boolean, optional): Pause instead of deleting when the timeout is reached -- `--env` (string[], optional): Environment variable for computers, repeatable -- `--secret` (string[], optional): Attach a stored secret as an environment variable, repeatable -- `--secret-value` (string[], optional): Store a new secret and attach it, repeatable -- `--unset-secret` (string[], optional): Detach a secret, repeatable -- `--project` (string, optional): Project to use (defaults to the API key's project) - -## steel environment delete - -Delete an environment - -### Usage - -```bash -steel environment delete -``` - -### Parameters - -- `environment_id` (string, required): Environment ID -- `--project` (string, optional): Project to use (defaults to the API key's project) - ## steel init One-command onboarding: login + verify + install agent skills diff --git a/docs/references/steel-cli.md b/docs/references/steel-cli.md index d41d89e..d45673f 100644 --- a/docs/references/steel-cli.md +++ b/docs/references/steel-cli.md @@ -26,22 +26,6 @@ For generated flags and argument schemas, use [../cli-reference.md](../cli-refer - `steel profile list`: list all saved Steel browser profiles. - `steel profile delete`: delete a saved Steel profile (local metadata only). -### Computer Commands - -- `steel computer create`: create a cloud computer from a template. -- `steel computer list`, `steel computer get`, `steel computer delete`: inspect and remove computers. -- `steel computer pause`, `steel computer resume`: pause and wake a computer. -- `steel computer use`: remember a default computer so other commands need no id. -- `steel computer exec -- `: run one command; output streams and the exit code is returned. -- `steel computer ssh`: open an SSH shell, or run a command over SSH with `-- `. -- `steel computer checkpoint`: save a computer's disk and memory as a checkpoint. -- `steel computer quota`: show how many computers and checkpoints the account can have. - -### Checkpoint Commands - -- `steel checkpoint list`, `steel checkpoint get`, `steel checkpoint delete`: inspect and remove checkpoints. -- `steel checkpoint restore`: start a new computer from a ready checkpoint. - ### Credentials Commands - `steel credentials create`: store a new credential for a given origin. diff --git a/scripts/generate-api.mjs b/scripts/generate-api.mjs index 964b4cf..c04069f 100644 --- a/scripts/generate-api.mjs +++ b/scripts/generate-api.mjs @@ -58,6 +58,8 @@ function pascalCase(value) { .join(""); } +const EXCLUDED_COMMAND_GROUPS = new Set(["computer", "checkpoint", "secret", "environment"]); + function stripV1(path) { return path.startsWith("/v1/") ? path.slice(3) : path; } @@ -68,6 +70,7 @@ function collectOperations(spec) { for (const [method, operation] of Object.entries(pathItem || {})) { const cli = operation["x-steel-cli"]; if (!operation.operationId || !cli?.command) continue; + if (EXCLUDED_COMMAND_GROUPS.has(cli.command[0])) continue; operations.push({ id: operation.operationId, method: method.toUpperCase(), diff --git a/src/api/checkpoints.rs b/src/api/checkpoints.rs deleted file mode 100644 index bd1e49a..0000000 --- a/src/api/checkpoints.rs +++ /dev/null @@ -1,154 +0,0 @@ -use serde_json::{Value, json}; - -use crate::api::client::{ApiError, SteelClient}; -use crate::api::computers::computer_path; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; - -pub fn checkpoint_path(id: &str) -> String { - format!("/checkpoints/{}", urlencoding::encode(id)) -} - -#[derive(Debug, Default, Clone)] -pub struct RestoreCheckpoint { - pub timeout_seconds: Option, - pub auto_pause: Option, - pub idle_timeout_seconds: Option, -} - -impl RestoreCheckpoint { - pub fn body(&self) -> Value { - let mut body = json!({}); - if let Some(timeout) = self.timeout_seconds { - body["timeoutSeconds"] = json!(timeout); - } - if let Some(auto_pause) = self.auto_pause { - body["autoPause"] = json!(auto_pause); - } - if let Some(idle_timeout) = self.idle_timeout_seconds { - body["idleTimeoutSeconds"] = json!(idle_timeout); - } - body - } -} - -impl SteelClient { - pub async fn create_checkpoint( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - computer_id: &str, - name: Option<&str>, - ) -> Result { - let mut body = json!({}); - if let Some(name) = name { - body["name"] = json!(name); - } - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/checkpoints", computer_path(computer_id)), - Some(body), - auth, - ) - .await - } - - pub async fn list_checkpoints( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - "/checkpoints", - None, - auth, - ) - .await - } - - pub async fn get_checkpoint( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &checkpoint_path(id), - None, - auth, - ) - .await - } - - pub async fn delete_checkpoint( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::DELETE, - &checkpoint_path(id), - None, - auth, - ) - .await - } - - pub async fn restore_checkpoint( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - request: &RestoreCheckpoint, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/computers", checkpoint_path(id)), - Some(request.body()), - auth, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn paths_are_encoded() { - assert_eq!(checkpoint_path("ckpt a"), "/checkpoints/ckpt%20a"); - } - - #[test] - fn restore_body_only_carries_given_options() { - assert_eq!(RestoreCheckpoint::default().body(), json!({})); - let request = RestoreCheckpoint { - timeout_seconds: Some(120), - auto_pause: Some(true), - idle_timeout_seconds: Some(60), - }; - assert_eq!( - request.body(), - json!({ "timeoutSeconds": 120, "autoPause": true, "idleTimeoutSeconds": 60 }) - ); - } -} diff --git a/src/api/computers.rs b/src/api/computers.rs deleted file mode 100644 index ba385fd..0000000 --- a/src/api/computers.rs +++ /dev/null @@ -1,326 +0,0 @@ -use std::collections::BTreeMap; - -use serde_json::{Value, json}; - -use crate::api::client::{ApiError, SteelClient}; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; - -pub fn computer_path(id: &str) -> String { - format!("/computers/{}", urlencoding::encode(id)) -} - -#[derive(Debug, Default, Clone)] -pub struct CreateComputer { - pub template: Option, - pub vcpu: Option, - pub memory_mib: Option, - pub timeout_seconds: Option, - pub auto_pause: Option, - pub idle_timeout_seconds: Option, - pub env: BTreeMap, - pub secrets: BTreeMap, - pub environment_id: Option, - pub project_id: Option, -} - -impl CreateComputer { - pub fn body(&self) -> Value { - let mut body = json!({}); - if let Some(template) = &self.template { - body["template"] = json!(template); - } - if let Some(vcpu) = self.vcpu { - body["vcpu"] = json!(vcpu); - } - if let Some(memory) = self.memory_mib { - body["memoryMib"] = json!(memory); - } - if let Some(timeout) = self.timeout_seconds { - body["timeoutSeconds"] = json!(timeout); - } - if let Some(auto_pause) = self.auto_pause { - body["autoPause"] = json!(auto_pause); - } - if let Some(idle_timeout) = self.idle_timeout_seconds { - body["idleTimeoutSeconds"] = json!(idle_timeout); - } - if !self.env.is_empty() { - body["env"] = json!(self.env); - } - if !self.secrets.is_empty() { - body["secrets"] = json!(self.secrets); - } - if let Some(environment) = &self.environment_id { - body["environmentId"] = json!(environment); - } - if let Some(project) = &self.project_id { - body["projectId"] = json!(project); - } - body - } -} - -impl SteelClient { - pub async fn list_computers( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - "/computers", - None, - auth, - ) - .await - } - - pub async fn get_computer_quota( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - "/computers/quota", - None, - auth, - ) - .await - } - - pub async fn get_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &computer_path(id), - None, - auth, - ) - .await - } - - pub async fn create_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - request: &CreateComputer, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - "/computers", - Some(request.body()), - auth, - ) - .await - } - - pub async fn delete_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::DELETE, - &computer_path(id), - None, - auth, - ) - .await - } - - pub async fn pause_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/pause", computer_path(id)), - None, - auth, - ) - .await - } - - pub async fn resume_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/resume", computer_path(id)), - None, - auth, - ) - .await - } - - pub async fn stop_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/stop", computer_path(id)), - None, - auth, - ) - .await - } - - pub async fn start_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/start", computer_path(id)), - None, - auth, - ) - .await - } - - pub async fn restart_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/restart", computer_path(id)), - None, - auth, - ) - .await - } - - pub async fn exec_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - body: Value, - ) -> Result { - self.request_raw( - base_url, - mode, - reqwest::Method::POST, - &format!("{}/exec", computer_path(id)), - Some(body), - auth, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn create_body_only_carries_given_fields() { - assert_eq!(CreateComputer::default().body(), json!({})); - - let named = CreateComputer { - template: Some("steel".into()), - ..Default::default() - }; - assert_eq!(named.body(), json!({ "template": "steel" })); - - let full = CreateComputer { - template: Some("steel".into()), - vcpu: Some(4), - memory_mib: Some(4096), - timeout_seconds: Some(600), - auto_pause: Some(true), - idle_timeout_seconds: Some(90), - env: BTreeMap::from([("TOKEN".into(), "abc".into())]), - secrets: BTreeMap::from([( - "API_KEY".into(), - "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31".into(), - )]), - environment_id: Some("9b2d1c5e-1b2a-4c3d-8e4f-5a6b7c8d9e0f".into()), - project_id: Some("0c1d2e3f-4a5b-4c6d-8e7f-8091a2b3c4d5".into()), - }; - assert_eq!( - full.body(), - json!({ - "template": "steel", - "vcpu": 4, - "memoryMib": 4096, - "timeoutSeconds": 600, - "autoPause": true, - "idleTimeoutSeconds": 90, - "env": { "TOKEN": "abc" }, - "secrets": { "API_KEY": "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31" }, - "environmentId": "9b2d1c5e-1b2a-4c3d-8e4f-5a6b7c8d9e0f", - "projectId": "0c1d2e3f-4a5b-4c6d-8e7f-8091a2b3c4d5", - }) - ); - } - - #[test] - fn zero_idle_timeout_is_sent_and_empty_env_is_not() { - let request = CreateComputer { - idle_timeout_seconds: Some(0), - ..Default::default() - }; - assert_eq!(request.body(), json!({ "idleTimeoutSeconds": 0 })); - } - - #[test] - fn computer_paths_escape_ids() { - assert_eq!( - computer_path("cmp_0000123456789abcdefghjkmnpqrs"), - "/computers/cmp_0000123456789abcdefghjkmnpqrs" - ); - assert_eq!(computer_path("a/b"), "/computers/a%2Fb"); - } -} diff --git a/src/api/environments.rs b/src/api/environments.rs deleted file mode 100644 index 7669cac..0000000 --- a/src/api/environments.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::collections::BTreeMap; - -use serde_json::{Map, Value, json}; - -use crate::api::client::{ApiError, SteelClient}; -use crate::api::secrets::with_project; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; - -pub fn environment_path(id: &str, project_id: Option<&str>) -> String { - with_project( - &format!("/environments/{}", urlencoding::encode(id)), - project_id, - ) -} - -#[derive(Debug, Default, Clone)] -pub struct EnvironmentSpec { - pub template: Option, - pub vcpu: Option, - pub memory_mib: Option, - pub disk_mib: Option, - pub timeout_seconds: Option, - pub auto_pause: Option, - pub env: BTreeMap, -} - -impl EnvironmentSpec { - pub fn is_empty(&self) -> bool { - self.template.is_none() - && self.vcpu.is_none() - && self.memory_mib.is_none() - && self.disk_mib.is_none() - && self.timeout_seconds.is_none() - && self.auto_pause.is_none() - && self.env.is_empty() - } - - pub fn apply_to(&self, current: Value) -> Value { - let mut spec = match current { - Value::Object(fields) => fields, - _ => Map::new(), - }; - if let Some(template) = &self.template { - spec.insert("template".into(), json!(template)); - } - if let Some(vcpu) = self.vcpu { - spec.insert("vcpu".into(), json!(vcpu)); - } - if let Some(memory) = self.memory_mib { - spec.insert("memoryMib".into(), json!(memory)); - } - if let Some(disk) = self.disk_mib { - spec.insert("diskMib".into(), json!(disk)); - } - if let Some(timeout) = self.timeout_seconds { - spec.insert("timeoutSeconds".into(), json!(timeout)); - } - if let Some(auto_pause) = self.auto_pause { - spec.insert("autoPause".into(), json!(auto_pause)); - } - if !self.env.is_empty() { - let mut env = match spec.remove("env") { - Some(Value::Object(fields)) => fields, - _ => Map::new(), - }; - for (name, value) in &self.env { - env.insert(name.clone(), json!(value)); - } - spec.insert("env".into(), Value::Object(env)); - } - Value::Object(spec) - } - - pub fn body(&self) -> Value { - self.apply_to(json!({})) - } -} - -#[derive(Debug, Default, Clone)] -pub struct CreateEnvironment { - pub name: String, - pub project_id: Option, - pub spec: Option, - pub secrets: BTreeMap, -} - -impl CreateEnvironment { - pub fn body(&self) -> Value { - let mut body = json!({ "name": self.name }); - if let Some(project) = &self.project_id { - body["projectId"] = json!(project); - } - if let Some(spec) = &self.spec { - body["spec"] = spec.clone(); - } - if !self.secrets.is_empty() { - body["secrets"] = json!(self.secrets); - } - body - } -} - -#[derive(Debug, Default, Clone)] -pub struct UpdateEnvironment { - pub name: Option, - pub spec: Option, - pub secrets: BTreeMap, -} - -impl UpdateEnvironment { - pub fn is_empty(&self) -> bool { - self.name.is_none() && self.spec.is_none() && self.secrets.is_empty() - } - - pub fn body(&self) -> Value { - let mut body = json!({}); - if let Some(name) = &self.name { - body["name"] = json!(name); - } - if let Some(spec) = &self.spec { - body["spec"] = spec.clone(); - } - if !self.secrets.is_empty() { - body["secrets"] = json!(self.secrets); - } - body - } -} - -impl SteelClient { - pub async fn list_environments( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &with_project("/environments", project_id), - None, - auth, - ) - .await - } - - pub async fn create_environment( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - request: &CreateEnvironment, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - "/environments", - Some(request.body()), - auth, - ) - .await - } - - pub async fn get_environment( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &environment_path(id, project_id), - None, - auth, - ) - .await - } - - pub async fn update_environment( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - request: &UpdateEnvironment, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::PATCH, - &environment_path(id, project_id), - Some(request.body()), - auth, - ) - .await - } - - pub async fn delete_environment( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::DELETE, - &environment_path(id, project_id), - None, - auth, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn paths_encode_ids_and_carry_the_project() { - assert_eq!(environment_path("env 1", None), "/environments/env%201"); - assert_eq!( - environment_path("env", Some("proj")), - "/environments/env?projectId=proj" - ); - } - - #[test] - fn spec_merges_into_the_current_one() { - let spec = EnvironmentSpec { - memory_mib: Some(4096), - auto_pause: Some(true), - env: BTreeMap::from([("B".into(), "2".into())]), - ..Default::default() - }; - assert!(!spec.is_empty()); - assert!(EnvironmentSpec::default().is_empty()); - let merged = spec.apply_to(json!({ - "template": "steel", - "vcpu": 2, - "env": { "A": "1" } - })); - assert_eq!( - merged, - json!({ - "template": "steel", - "vcpu": 2, - "memoryMib": 4096, - "autoPause": true, - "env": { "A": "1", "B": "2" } - }) - ); - assert_eq!( - spec.body(), - json!({ "memoryMib": 4096, "autoPause": true, "env": { "B": "2" } }) - ); - } - - #[test] - fn bodies_only_carry_given_fields() { - let create = CreateEnvironment { - name: "dev".into(), - ..Default::default() - }; - assert_eq!(create.body(), json!({ "name": "dev" })); - let full = CreateEnvironment { - name: "dev".into(), - project_id: Some("proj".into()), - spec: Some(json!({ "template": "steel" })), - secrets: BTreeMap::from([ - ("TOKEN".into(), json!({ "secretId": "id" })), - ("KEY".into(), json!({ "value": "abc" })), - ]), - }; - assert_eq!( - full.body(), - json!({ - "name": "dev", - "projectId": "proj", - "spec": { "template": "steel" }, - "secrets": { "TOKEN": { "secretId": "id" }, "KEY": { "value": "abc" } } - }) - ); - assert!(UpdateEnvironment::default().is_empty()); - let update = UpdateEnvironment { - secrets: BTreeMap::from([("OLD".into(), Value::Null)]), - ..Default::default() - }; - assert_eq!(update.body(), json!({ "secrets": { "OLD": null } })); - } -} diff --git a/src/api/generated.rs b/src/api/generated.rs index 68b8a3c..93fd984 100644 --- a/src/api/generated.rs +++ b/src/api/generated.rs @@ -31,29 +31,6 @@ pub struct StreamingMetadata { } pub const CLI_OPERATION_METADATA: &[OperationMetadata] = &[ - OperationMetadata { - id: "exec_computer", - command: "computer exec", - status: "implemented", - method: "POST", - path: "/v1/computers/{id}/exec", - summary: "Run one command in a computer", - example: "steel computer exec -- ", - streaming: None, - }, - OperationMetadata { - id: "attach_computer_ssh", - command: "computer ssh", - status: "implemented", - method: "GET", - path: "/v1/computers/{id}/ssh", - summary: "Open an SSH connection to a computer", - example: "steel computer ssh ", - streaming: Some(StreamingMetadata { - transport: "websocket", - path: "/v1/computers/{id}/ssh", - }), - }, OperationMetadata { id: "get_session_agent_logs", command: "sessions agent-logs", @@ -357,40 +334,6 @@ fn build_get_session_agent_traces_path( } impl SteelClient { - pub async fn cli_exec_computer( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - "/computers/{id}/exec", - None, - auth, - ) - .await - } - - pub async fn cli_attach_computer_ssh( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - "/computers/{id}/ssh", - None, - auth, - ) - .await - } - pub async fn cli_get_session_agent_logs( &self, base_url: &str, diff --git a/src/api/mod.rs b/src/api/mod.rs index 493cd75..00ceb84 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,9 +1,5 @@ -pub mod checkpoints; pub mod client; -pub mod computers; -pub mod environments; pub mod generated; pub mod projects; -pub mod secrets; pub mod session; pub mod top_level; diff --git a/src/api/secrets.rs b/src/api/secrets.rs deleted file mode 100644 index 7f89630..0000000 --- a/src/api/secrets.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Result, bail}; -use serde_json::{Value, json}; - -use crate::api::client::{ApiError, SteelClient}; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; -use crate::util::pairs::parse_pairs; - -pub fn with_project(path: &str, project_id: Option<&str>) -> String { - match project_id.map(str::trim).filter(|id| !id.is_empty()) { - Some(project) => format!("{path}?projectId={}", urlencoding::encode(project)), - None => path.to_string(), - } -} - -pub fn secret_path(id: &str, project_id: Option<&str>) -> String { - with_project(&format!("/secrets/{}", urlencoding::encode(id)), project_id) -} - -pub fn is_secret_id(value: &str) -> bool { - let parts: Vec<&str> = value.split('-').collect(); - parts.len() == 5 - && parts.iter().zip([8, 4, 4, 4, 12]).all(|(part, len)| { - part.len() == len && part.bytes().all(|byte| byte.is_ascii_hexdigit()) - }) -} - -pub fn parse_secret_ids(pairs: &[String]) -> Result> { - let bindings = parse_pairs("--secret", "NAME=SECRET_ID", pairs)?; - for (name, id) in &bindings { - if !is_secret_id(id) { - bail!( - "--secret {name} wants a secret id, got {id:?}. Store the value with `steel secret create` first." - ); - } - } - Ok(bindings) -} - -#[derive(Debug, Default, Clone)] -pub struct CreateSecret { - pub name: String, - pub value: String, - pub project_id: Option, -} - -impl CreateSecret { - pub fn body(&self) -> Value { - let mut body = json!({ "name": self.name, "value": self.value }); - if let Some(project) = &self.project_id { - body["projectId"] = json!(project); - } - body - } -} - -#[derive(Debug, Default, Clone)] -pub struct UpdateSecret { - pub name: Option, - pub value: Option, -} - -impl UpdateSecret { - pub const fn is_empty(&self) -> bool { - self.name.is_none() && self.value.is_none() - } - - pub fn body(&self) -> Value { - let mut body = json!({}); - if let Some(name) = &self.name { - body["name"] = json!(name); - } - if let Some(value) = &self.value { - body["value"] = json!(value); - } - body - } -} - -impl SteelClient { - pub async fn list_secrets( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &with_project("/secrets", project_id), - None, - auth, - ) - .await - } - - pub async fn create_secret( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - request: &CreateSecret, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::POST, - "/secrets", - Some(request.body()), - auth, - ) - .await - } - - pub async fn get_secret( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::GET, - &secret_path(id, project_id), - None, - auth, - ) - .await - } - - pub async fn update_secret( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - request: &UpdateSecret, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::PATCH, - &secret_path(id, project_id), - Some(request.body()), - auth, - ) - .await - } - - pub async fn delete_secret( - &self, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - project_id: Option<&str>, - ) -> Result { - self.request( - base_url, - mode, - reqwest::Method::DELETE, - &secret_path(id, project_id), - None, - auth, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const ID: &str = "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31"; - - #[test] - fn paths_encode_ids_and_carry_the_project() { - assert_eq!(secret_path("a b", None), "/secrets/a%20b"); - assert_eq!( - secret_path(ID, Some("proj 1")), - format!("/secrets/{ID}?projectId=proj%201") - ); - assert_eq!(with_project("/secrets", Some(" ")), "/secrets"); - } - - #[test] - fn secret_ids_are_uuids() { - assert!(is_secret_id(ID)); - assert!(is_secret_id("00000000-0000-0000-0000-000000000000")); - assert!(!is_secret_id("sk-live-abc")); - assert!(!is_secret_id("2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b3")); - assert!(!is_secret_id("2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b3g")); - } - - #[test] - fn secret_bindings_refuse_plain_values() { - let bindings = parse_secret_ids(&[format!("TOKEN={ID}")]).unwrap(); - assert_eq!(bindings["TOKEN"], ID); - let err = parse_secret_ids(&["TOKEN=sk-live-abc".into()]).unwrap_err(); - assert!(err.to_string().contains("steel secret create")); - } - - #[test] - fn bodies_only_carry_given_fields() { - let create = CreateSecret { - name: "TOKEN".into(), - value: "abc".into(), - project_id: None, - }; - assert_eq!(create.body(), json!({ "name": "TOKEN", "value": "abc" })); - let scoped = CreateSecret { - project_id: Some("proj".into()), - ..create - }; - assert_eq!( - scoped.body(), - json!({ "name": "TOKEN", "value": "abc", "projectId": "proj" }) - ); - assert!(UpdateSecret::default().is_empty()); - let update = UpdateSecret { - name: Some("API_TOKEN".into()), - value: None, - }; - assert_eq!(update.body(), json!({ "name": "API_TOKEN" })); - } -} diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs deleted file mode 100644 index d403da9..0000000 --- a/src/commands/checkpoint.rs +++ /dev/null @@ -1,220 +0,0 @@ -use anyhow::{Result, bail}; -use clap::{Parser, Subcommand}; -use serde_json::Value; - -use crate::api::checkpoints::RestoreCheckpoint; -use crate::api::client::SteelClient; -use crate::commands::computer; -use crate::status; -use crate::util::{api, output}; - -#[derive(Subcommand)] -pub enum Command { - /// List checkpoints - List, - - /// Get one checkpoint - Get(IdArgs), - - /// Delete a checkpoint - Delete(IdArgs), - - /// Start a new computer from a checkpoint - Restore(RestoreArgs), -} - -impl Command { - pub const fn telemetry_name(&self) -> &'static str { - match self { - Self::List => "list", - Self::Get(_) => "get", - Self::Delete(_) => "delete", - Self::Restore(_) => "restore", - } - } -} - -#[derive(Parser)] -pub struct IdArgs { - /// Checkpoint ID - pub checkpoint_id: String, -} - -#[derive(Parser)] -pub struct RestoreArgs { - /// Checkpoint ID - pub checkpoint_id: String, - - /// Delete the computer after this many seconds of running time - #[arg(long = "timeout", value_name = "SECONDS")] - pub timeout_seconds: Option, - - /// Pause instead of deleting when the timeout is reached - #[arg(long = "auto-pause")] - pub auto_pause: bool, - - /// Pause after this many seconds without incoming traffic (0 disables it) - #[arg(long = "idle-timeout", value_name = "SECONDS")] - pub idle_timeout_seconds: Option, - - /// Wait until the computer is running - #[arg(long)] - pub wait: bool, - - /// Make the new computer the default for other commands - #[arg(long = "use")] - pub use_as_default: bool, -} - -pub async fn run(command: Command) -> Result<()> { - match command { - Command::List => run_list().await, - Command::Get(args) => run_get(args).await, - Command::Delete(args) => run_delete(args).await, - Command::Restore(args) => run_restore(args).await, - } -} - -pub fn status_of(checkpoint: &Value) -> &str { - checkpoint["status"].as_str().unwrap_or("unknown") -} - -async fn run_list() -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.list_checkpoints(&base_url, mode, &auth).await?; - if output::is_json() { - output::success_data(data); - } else { - print_checkpoints(&data); - } - Ok(()) -} - -async fn run_get(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .get_checkpoint(&base_url, mode, &auth, &args.checkpoint_id) - .await?; - output::success_data(data); - Ok(()) -} - -async fn run_delete(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .delete_checkpoint(&base_url, mode, &auth, &args.checkpoint_id) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Deleted {}.", args.checkpoint_id); - } - Ok(()) -} - -async fn run_restore(args: RestoreArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let request = RestoreCheckpoint { - timeout_seconds: args.timeout_seconds, - auto_pause: args.auto_pause.then_some(true), - idle_timeout_seconds: args.idle_timeout_seconds, - }; - let restored = client - .restore_checkpoint(&base_url, mode, &auth, &args.checkpoint_id, &request) - .await?; - let id = computer::id_of(&restored)?; - let computer = if args.wait { - status!("Restored {id}, waiting until it is running."); - computer::wait_for(&client, &base_url, mode, &auth, &id, "running").await? - } else { - restored - }; - if args.use_as_default { - computer::remember_computer(Some(&id))?; - } - if output::is_json() { - output::success_data(computer); - } else { - println!("{id} is {}.", computer::status_of(&computer)); - if args.use_as_default { - println!("It is now the default computer."); - } - } - Ok(()) -} - -pub async fn wait_until_ready( - client: &SteelClient, - base_url: &str, - mode: crate::config::settings::ApiMode, - auth: &crate::config::auth::Auth, - id: &str, -) -> Result { - let started = std::time::Instant::now(); - loop { - let checkpoint = client.get_checkpoint(base_url, mode, auth, id).await?; - match status_of(&checkpoint) { - "ready" => return Ok(checkpoint), - "creating" => {} - status => bail!("{id} is {status}, it will not become ready."), - } - if started.elapsed() > computer::WAIT_TIMEOUT { - bail!( - "{id} is still creating after {}s.", - computer::WAIT_TIMEOUT.as_secs() - ); - } - tokio::time::sleep(computer::WAIT_POLL_INTERVAL).await; - } -} - -fn print_checkpoints(data: &Value) { - let checkpoints = data["checkpoints"].as_array().cloned().unwrap_or_default(); - if checkpoints.is_empty() { - println!("No checkpoints."); - return; - } - let rows: Vec<[String; 5]> = checkpoints - .iter() - .map(|checkpoint| { - [ - checkpoint["id"].as_str().unwrap_or("").to_string(), - status_of(checkpoint).to_string(), - checkpoint["name"].as_str().unwrap_or("-").to_string(), - checkpoint["computerId"].as_str().unwrap_or("-").to_string(), - match checkpoint["sizeBytes"].as_u64() { - Some(bytes) => format!("{:.1}G", bytes as f64 / 1_073_741_824.0), - None => "-".to_string(), - }, - ] - }) - .collect(); - let header = ["ID", "STATUS", "NAME", "COMPUTER", "SIZE"]; - let widths: Vec = (0..header.len()) - .map(|column| { - rows.iter() - .map(|row| row[column].len()) - .chain(std::iter::once(header[column].len())) - .max() - .unwrap_or(0) - }) - .collect(); - let line = |cells: [&str; 5]| { - cells - .iter() - .enumerate() - .map(|(column, cell)| format!("{cell:>() - .join(" ") - .trim_end() - .to_string() - }; - println!("{}", line(header)); - for row in &rows { - println!("{}", line([&row[0], &row[1], &row[2], &row[3], &row[4]])); - } -} diff --git a/src/commands/computer/exec.rs b/src/commands/computer/exec.rs deleted file mode 100644 index 19cbbe0..0000000 --- a/src/commands/computer/exec.rs +++ /dev/null @@ -1,329 +0,0 @@ -use std::collections::BTreeMap; -use std::time::Duration; - -use anyhow::{Context, Result, bail}; -use clap::Parser; -use serde::Deserialize; -use serde_json::{Value, json}; -use tokio::io::AsyncWriteExt; - -use super::resolve_computer_id; -use crate::api::client::{ApiError, SteelClient}; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; -use crate::status; -use crate::util::api; -use crate::util::output::{self, SilentExit}; - -pub const TIMEOUT_EXIT_CODE: i32 = 124; -const MAX_WAKE_ATTEMPTS: u32 = 8; -const MAX_WAKE_DELAY: Duration = Duration::from_secs(10); - -#[derive(Parser)] -pub struct Args { - /// Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - pub computer_id: Option, - - /// Shell command string, run by /bin/sh -c - #[arg(short = 'c', long = "command", conflicts_with = "argv")] - pub command: Option, - - /// Working directory inside the computer - #[arg(long)] - pub cwd: Option, - - /// Environment variable for the command, repeatable - #[arg(long = "env", value_name = "KEY=VALUE")] - pub env: Vec, - - /// Kill the command after this many seconds (at most 3600) - #[arg(long = "timeout", value_name = "SECONDS")] - pub timeout_seconds: Option, - - /// Program and arguments, given after `--` - #[arg(last = true, value_name = "ARGV")] - pub argv: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "event", rename_all = "lowercase")] -pub enum Event { - Start, - Output { - data: String, - }, - Keepalive, - Exit { - #[serde(rename = "exitCode")] - exit_code: i32, - #[serde(rename = "timedOut")] - timed_out: bool, - }, -} - -#[derive(Debug, Deserialize)] -pub struct ExecResult { - pub output: String, - #[serde(rename = "exitCode")] - pub exit_code: i32, - #[serde(rename = "timedOut")] - pub timed_out: bool, - #[serde(default)] - pub truncated: bool, -} - -pub fn build_body(args: &Args, stream: bool) -> Result { - let mut body = match (&args.command, args.argv.is_empty()) { - (Some(command), true) => json!({ "command": command }), - (None, false) => json!({ "argv": args.argv }), - (None, true) => bail!("Give a command after `--` or with -c."), - (Some(_), false) => bail!("Give either a command after `--` or -c, not both."), - }; - if let Some(cwd) = &args.cwd { - body["cwd"] = json!(cwd); - } - if !args.env.is_empty() { - body["env"] = json!(parse_env(&args.env)?); - } - if let Some(timeout) = args.timeout_seconds { - if timeout == 0 || timeout > 3600 { - bail!("--timeout must be between 1 and 3600 seconds."); - } - body["timeoutSeconds"] = json!(timeout); - } - body["stream"] = json!(stream); - Ok(body) -} - -pub fn parse_env(entries: &[String]) -> Result> { - let mut env = BTreeMap::new(); - for entry in entries { - let Some((key, value)) = entry.split_once('=') else { - bail!("--env expects KEY=VALUE, got {entry:?}."); - }; - if key.is_empty() { - bail!("--env expects KEY=VALUE, got {entry:?}."); - } - env.insert(key.to_string(), value.to_string()); - } - Ok(env) -} - -pub fn parse_event(line: &[u8]) -> Result> { - let text = std::str::from_utf8(line).context("the computer sent a non-UTF-8 event")?; - if text.trim().is_empty() { - return Ok(None); - } - let event = serde_json::from_str(text).context("the computer sent a malformed event")?; - Ok(Some(event)) -} - -pub const fn exit_code(code: i32, timed_out: bool) -> i32 { - if timed_out { - TIMEOUT_EXIT_CODE - } else if code < 0 { - 1 - } else { - code - } -} - -pub async fn run(args: Args) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let stream = !output::is_json(); - let body = build_body(&args, stream)?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let response = send(&client, &base_url, mode, &auth, &id, body).await?; - let code = if stream { - stream_output(response).await? - } else { - collect_output(response).await? - }; - if code != 0 { - return Err(SilentExit(code).into()); - } - Ok(()) -} - -async fn send( - client: &SteelClient, - base_url: &str, - mode: ApiMode, - auth: &Auth, - id: &str, - body: Value, -) -> Result { - let mut attempt = 0; - loop { - match client - .exec_computer(base_url, mode, auth, id, body.clone()) - .await - { - Ok(response) => return Ok(response), - Err(ApiError::RetryLater { seconds, .. }) if attempt < MAX_WAKE_ATTEMPTS => { - attempt += 1; - let delay = Duration::from_secs(seconds.max(1)).min(MAX_WAKE_DELAY); - status!( - "Computer {id} is not ready yet, retrying in {}s.", - delay.as_secs() - ); - tokio::time::sleep(delay).await; - } - Err(err) => return Err(err.into()), - } - } -} - -async fn stream_output(mut response: reqwest::Response) -> Result { - let mut stdout = tokio::io::stdout(); - let mut buffer: Vec = Vec::new(); - let mut result: Option = None; - while let Some(chunk) = response - .chunk() - .await - .context("the connection to the computer dropped")? - { - buffer.extend_from_slice(&chunk); - while let Some(end) = buffer.iter().position(|byte| *byte == b'\n') { - let line: Vec = buffer.drain(..=end).collect(); - if let Some(event) = parse_event(&line[..line.len() - 1])? { - handle_event(event, &mut stdout, &mut result).await?; - } - } - } - if let Some(event) = parse_event(&buffer)? { - handle_event(event, &mut stdout, &mut result).await?; - } - match result { - Some(code) => Ok(code), - None => bail!("The connection closed before the command finished."), - } -} - -async fn handle_event( - event: Event, - stdout: &mut tokio::io::Stdout, - result: &mut Option, -) -> Result<()> { - match event { - Event::Output { data } => { - stdout.write_all(data.as_bytes()).await?; - stdout.flush().await?; - } - Event::Exit { - exit_code: code, - timed_out, - } => *result = Some(exit_code(code, timed_out)), - Event::Start | Event::Keepalive => {} - } - Ok(()) -} - -async fn collect_output(response: reqwest::Response) -> Result { - let text = response - .text() - .await - .context("the connection to the computer dropped")?; - let value: Value = serde_json::from_str(text.trim_start()) - .context("the computer returned a malformed result")?; - let result: ExecResult = serde_json::from_value(value.clone()) - .context("the computer returned a malformed result")?; - output::success_data(value); - Ok(exit_code(result.exit_code, result.timed_out)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn args(command: Option<&str>, argv: &[&str]) -> Args { - Args { - computer_id: None, - command: command.map(str::to_string), - cwd: None, - env: vec![], - timeout_seconds: None, - argv: argv.iter().map(|s| s.to_string()).collect(), - } - } - - #[test] - fn body_takes_exactly_one_command_form() { - assert_eq!( - build_body(&args(Some("echo hi"), &[]), true).unwrap(), - json!({ "command": "echo hi", "stream": true }) - ); - assert_eq!( - build_body(&args(None, &["ls", "-la"]), false).unwrap(), - json!({ "argv": ["ls", "-la"], "stream": false }) - ); - assert!(build_body(&args(None, &[]), true).is_err()); - assert!(build_body(&args(Some("id"), &["id"]), true).is_err()); - } - - #[test] - fn body_carries_cwd_env_and_timeout() { - let mut a = args(Some("id"), &[]); - a.cwd = Some("/tmp".into()); - a.env = vec!["A=1".into(), "B=x=y".into()]; - a.timeout_seconds = Some(30); - assert_eq!( - build_body(&a, true).unwrap(), - json!({ - "command": "id", - "cwd": "/tmp", - "env": { "A": "1", "B": "x=y" }, - "timeoutSeconds": 30, - "stream": true - }) - ); - a.timeout_seconds = Some(0); - assert!(build_body(&a, true).is_err()); - a.timeout_seconds = Some(3601); - assert!(build_body(&a, true).is_err()); - } - - #[test] - fn env_entries_need_a_key() { - assert!(parse_env(&["=1".into()]).is_err()); - assert!(parse_env(&["novalue".into()]).is_err()); - assert_eq!(parse_env(&["K=".into()]).unwrap()["K"], ""); - } - - #[test] - fn events_parse_and_map_to_exit_codes() { - assert!(matches!( - parse_event(br#"{"event":"start"}"#).unwrap(), - Some(Event::Start) - )); - assert!(matches!( - parse_event(br#"{"event":"output","data":"hi\n"}"#).unwrap(), - Some(Event::Output { data }) if data == "hi\n" - )); - assert!(matches!( - parse_event(br#"{"event":"exit","exitCode":3,"timedOut":false}"#).unwrap(), - Some(Event::Exit { - exit_code: 3, - timed_out: false - }) - )); - assert!(parse_event(b" ").unwrap().is_none()); - assert!(parse_event(b"not json").is_err()); - - assert_eq!(exit_code(0, false), 0); - assert_eq!(exit_code(3, false), 3); - assert_eq!(exit_code(-1, false), 1); - assert_eq!(exit_code(0, true), TIMEOUT_EXIT_CODE); - } - - #[test] - fn collected_results_deserialize() { - let result: ExecResult = serde_json::from_str( - r#"{"output":"hi\n","exitCode":0,"timedOut":false,"truncated":false}"#, - ) - .unwrap(); - assert_eq!(result.output, "hi\n"); - assert!(!result.truncated); - } -} diff --git a/src/commands/computer/mod.rs b/src/commands/computer/mod.rs deleted file mode 100644 index 0cd2e85..0000000 --- a/src/commands/computer/mod.rs +++ /dev/null @@ -1,592 +0,0 @@ -pub mod exec; -pub mod ssh; -pub mod wsio; - -use std::collections::BTreeMap; -use std::time::{Duration, Instant}; - -use anyhow::{Context, Result, bail}; -use clap::{Parser, Subcommand}; -use serde_json::Value; - -use crate::api::client::SteelClient; -use crate::api::computers::CreateComputer; -use crate::api::secrets::parse_secret_ids; -use crate::commands::checkpoint; -use crate::config::settings::{self, ComputerConfig}; -use crate::status; -use crate::util::pairs::parse_pairs; -use crate::util::{api, output}; - -pub const WAIT_POLL_INTERVAL: Duration = Duration::from_secs(1); -pub const WAIT_TIMEOUT: Duration = Duration::from_secs(180); - -#[derive(Subcommand)] -pub enum Command { - /// Create a computer - Create(CreateArgs), - - /// List computers - List, - - /// Get one computer - Get(IdArgs), - - /// Delete a computer - Delete(IdArgs), - - /// Pause a running computer - Pause(IdArgs), - - /// Resume a paused computer - Resume(LifecycleArgs), - - /// Stop a computer but keep its disk - Stop(LifecycleArgs), - - /// Start a stopped computer - Start(LifecycleArgs), - - /// Reboot a running computer - Restart(LifecycleArgs), - - /// Remember a computer as the default for other commands - Use(UseArgs), - - /// Run one command in a computer - Exec(exec::Args), - - /// Open an SSH session to a computer - Ssh(ssh::Args), - - /// Save a computer as a checkpoint - Checkpoint(CheckpointArgs), - - /// Show how many computers and checkpoints you can have - Quota, -} - -impl Command { - pub const fn telemetry_name(&self) -> &'static str { - match self { - Self::Create(_) => "create", - Self::List => "list", - Self::Get(_) => "get", - Self::Delete(_) => "delete", - Self::Pause(_) => "pause", - Self::Resume(_) => "resume", - Self::Stop(_) => "stop", - Self::Start(_) => "start", - Self::Restart(_) => "restart", - Self::Use(_) => "use", - Self::Exec(_) => "exec", - Self::Ssh(_) => "ssh", - Self::Checkpoint(_) => "checkpoint", - Self::Quota => "quota", - } - } -} - -#[derive(Parser)] -pub struct CreateArgs { - /// Template name (defaults to the only one the API offers) - #[arg(long)] - pub template: Option, - - /// Number of vCPUs - #[arg(long)] - pub vcpu: Option, - - /// Memory in MiB - #[arg(long = "memory", value_name = "MIB")] - pub memory_mib: Option, - - /// Delete the computer after this many seconds of running time - #[arg(long = "timeout", value_name = "SECONDS")] - pub timeout_seconds: Option, - - /// Pause instead of deleting when the timeout is reached - #[arg(long = "auto-pause")] - pub auto_pause: bool, - - /// Pause after this many seconds without incoming traffic (0 disables it) - #[arg(long = "idle-timeout", value_name = "SECONDS")] - pub idle_timeout_seconds: Option, - - /// Environment variable for the computer, repeatable - #[arg(long = "env", value_name = "KEY=VALUE")] - pub env: Vec, - - /// Attach a stored secret as an environment variable, repeatable - #[arg(long = "secret", value_name = "NAME=SECRET_ID")] - pub secrets: Vec, - - /// Start from an environment; its spec and secrets apply unless overridden here - #[arg(long = "environment", value_name = "ENVIRONMENT_ID")] - pub environment_id: Option, - - /// Project to create the computer in (defaults to the API key's project) - #[arg(long = "project", value_name = "PROJECT_ID")] - pub project_id: Option, - - /// Wait until the computer is running - #[arg(long)] - pub wait: bool, - - /// Make the new computer the default for other commands - #[arg(long = "use")] - pub use_as_default: bool, -} - -#[derive(Parser)] -pub struct IdArgs { - /// Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - pub computer_id: Option, -} - -#[derive(Parser)] -pub struct LifecycleArgs { - /// Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - pub computer_id: Option, - - /// Wait until the computer reaches its new state - #[arg(long)] - pub wait: bool, -} - -#[derive(Parser)] -pub struct CheckpointArgs { - /// Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - pub computer_id: Option, - - /// Name for the checkpoint - #[arg(long)] - pub name: Option, - - /// Wait until the checkpoint is ready - #[arg(long)] - pub wait: bool, -} - -#[derive(Parser)] -pub struct UseArgs { - /// Computer ID to remember - pub computer_id: Option, - - /// Forget the remembered computer - #[arg(long, conflicts_with = "computer_id")] - pub clear: bool, -} - -pub async fn run(command: Command) -> Result<()> { - match command { - Command::Create(args) => run_create(args).await, - Command::List => run_list().await, - Command::Get(args) => run_get(args).await, - Command::Delete(args) => run_delete(args).await, - Command::Pause(args) => run_pause(args).await, - Command::Resume(args) => run_resume(args).await, - Command::Stop(args) => run_stop(args).await, - Command::Start(args) => run_start(args).await, - Command::Restart(args) => run_restart(args).await, - Command::Use(args) => run_use(args), - Command::Exec(args) => exec::run(args).await, - Command::Ssh(args) => ssh::run(args).await, - Command::Checkpoint(args) => run_checkpoint(args).await, - Command::Quota => run_quota().await, - } -} - -pub fn resolve_computer_id(explicit: Option<&str>) -> Result { - let env = std::env::var("STEEL_COMPUTER_ID").ok(); - let stored = settings::read_config() - .ok() - .and_then(|config| config.default_computer_id().map(str::to_string)); - choose_computer_id(explicit, env.as_deref(), stored.as_deref()) -} - -pub fn choose_computer_id( - explicit: Option<&str>, - env: Option<&str>, - stored: Option<&str>, -) -> Result { - for candidate in [explicit, env, stored] { - if let Some(id) = candidate.map(str::trim).filter(|id| !id.is_empty()) { - return Ok(id.to_string()); - } - } - bail!("No computer given. Pass an id, set STEEL_COMPUTER_ID, or run `steel computer use `.") -} - -pub fn remember_computer(id: Option<&str>) -> Result<()> { - let mut config = settings::read_config().unwrap_or_default(); - config.computer = id.map(|id| ComputerConfig { - default_id: Some(id.to_string()), - }); - settings::write_config(&config).context("Failed to save the default computer") -} - -pub fn parse_env(pairs: &[String]) -> Result> { - parse_pairs("--env", "KEY=VALUE", pairs) -} - -pub fn status_of(computer: &Value) -> &str { - computer["status"].as_str().unwrap_or("unknown") -} - -pub fn id_of(computer: &Value) -> Result { - computer["id"] - .as_str() - .map(str::to_string) - .context("the API returned a computer without an id") -} - -async fn run_create(args: CreateArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let request = CreateComputer { - template: args.template, - vcpu: args.vcpu, - memory_mib: args.memory_mib, - timeout_seconds: args.timeout_seconds, - auto_pause: args.auto_pause.then_some(true), - idle_timeout_seconds: args.idle_timeout_seconds, - env: parse_env(&args.env)?, - secrets: parse_secret_ids(&args.secrets)?, - environment_id: args.environment_id, - project_id: args.project_id, - }; - let created = client - .create_computer(&base_url, mode, &auth, &request) - .await?; - let id = id_of(&created)?; - let computer = if args.wait { - status!("Created {id}, waiting until it is running."); - wait_for(&client, &base_url, mode, &auth, &id, "running").await? - } else { - created - }; - if args.use_as_default { - remember_computer(Some(&id))?; - } - if output::is_json() { - output::success_data(computer); - } else { - println!("{id} is {}.", status_of(&computer)); - if args.use_as_default { - println!("It is now the default computer."); - } - } - Ok(()) -} - -async fn run_list() -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.list_computers(&base_url, mode, &auth).await?; - if output::is_json() { - output::success_data(data); - } else { - print_computers(&data); - } - Ok(()) -} - -async fn run_get(args: IdArgs) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.get_computer(&base_url, mode, &auth, &id).await?; - output::success_data(data); - Ok(()) -} - -async fn run_delete(args: IdArgs) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.delete_computer(&base_url, mode, &auth, &id).await?; - if settings::read_config() - .ok() - .and_then(|config| config.default_computer_id().map(str::to_string)) - .is_some_and(|default| default == id) - { - remember_computer(None)?; - } - if output::is_json() { - output::success_data(data); - } else { - println!("Deleted {id}."); - } - Ok(()) -} - -async fn run_pause(args: IdArgs) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.pause_computer(&base_url, mode, &auth, &id).await?; - if output::is_json() { - output::success_data(data); - } else { - println!("{id} is {}.", status_of(&data)); - } - Ok(()) -} - -async fn run_resume(args: LifecycleArgs) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let resumed = client.resume_computer(&base_url, mode, &auth, &id).await?; - let data = if args.wait { - wait_for(&client, &base_url, mode, &auth, &id, "running").await? - } else { - resumed - }; - if output::is_json() { - output::success_data(data); - } else { - println!("{id} is {}.", status_of(&data)); - } - Ok(()) -} - -#[derive(Clone, Copy)] -enum Lifecycle { - Stop, - Start, - Restart, -} - -impl Lifecycle { - const fn settled(self) -> &'static str { - match self { - Self::Stop => "stopped", - Self::Start | Self::Restart => "running", - } - } -} - -async fn run_stop(args: LifecycleArgs) -> Result<()> { - run_lifecycle(args, Lifecycle::Stop).await -} - -async fn run_start(args: LifecycleArgs) -> Result<()> { - run_lifecycle(args, Lifecycle::Start).await -} - -async fn run_restart(args: LifecycleArgs) -> Result<()> { - run_lifecycle(args, Lifecycle::Restart).await -} - -async fn run_lifecycle(args: LifecycleArgs, verb: Lifecycle) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let sent = match verb { - Lifecycle::Stop => client.stop_computer(&base_url, mode, &auth, &id).await?, - Lifecycle::Start => client.start_computer(&base_url, mode, &auth, &id).await?, - Lifecycle::Restart => client.restart_computer(&base_url, mode, &auth, &id).await?, - }; - let data = if args.wait { - wait_for(&client, &base_url, mode, &auth, &id, verb.settled()).await? - } else { - sent - }; - if output::is_json() { - output::success_data(data); - } else { - println!("{id} is {}.", status_of(&data)); - } - Ok(()) -} - -async fn run_quota() -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client.get_computer_quota(&base_url, mode, &auth).await?; - if output::is_json() { - output::success_data(data); - } else { - let count = |key: &str| data[key].as_u64().unwrap_or(0); - println!( - "Computers {}/{}", - count("computerCount"), - count("computerLimit") - ); - println!( - "Running {}/{}", - count("runningCount"), - count("runningLimit") - ); - println!( - "Checkpoints {}/{}", - count("checkpointCount"), - count("checkpointLimit") - ); - } - Ok(()) -} - -async fn run_checkpoint(args: CheckpointArgs) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let created = client - .create_checkpoint(&base_url, mode, &auth, &id, args.name.as_deref()) - .await?; - let checkpoint_id = created["id"] - .as_str() - .map(str::to_string) - .context("the API returned a checkpoint without an id")?; - let checkpoint = if args.wait { - status!("Saving {id} as {checkpoint_id}, this takes a while."); - checkpoint::wait_until_ready(&client, &base_url, mode, &auth, &checkpoint_id).await? - } else { - created - }; - if output::is_json() { - output::success_data(checkpoint); - } else { - println!("{checkpoint_id} is {}.", checkpoint::status_of(&checkpoint)); - } - Ok(()) -} - -fn run_use(args: UseArgs) -> Result<()> { - if args.clear { - remember_computer(None)?; - if output::is_json() { - output::success_silent(); - } else { - println!("Forgot the default computer."); - } - return Ok(()); - } - let Some(id) = args - .computer_id - .as_deref() - .map(str::trim) - .filter(|id| !id.is_empty()) - else { - bail!("Give a computer id, or --clear to forget the current one."); - }; - remember_computer(Some(id))?; - if output::is_json() { - output::success_data(serde_json::json!({ "computerId": id })); - } else { - println!("{id} is now the default computer."); - } - Ok(()) -} - -pub async fn wait_for( - client: &SteelClient, - base_url: &str, - mode: crate::config::settings::ApiMode, - auth: &crate::config::auth::Auth, - id: &str, - target: &str, -) -> Result { - let started = Instant::now(); - loop { - let computer = client.get_computer(base_url, mode, auth, id).await?; - let status = status_of(&computer); - if status == target { - return Ok(computer); - } - if matches!(status, "failed" | "deleted" | "deleting" | "stopped") { - bail!("{id} is {status}, it will not become {target}."); - } - if started.elapsed() > WAIT_TIMEOUT { - bail!("{id} is still {status} after {}s.", WAIT_TIMEOUT.as_secs()); - } - tokio::time::sleep(WAIT_POLL_INTERVAL).await; - } -} - -fn print_computers(data: &Value) { - let computers = data["computers"].as_array().cloned().unwrap_or_default(); - if computers.is_empty() { - println!("No computers."); - return; - } - let rows: Vec<[String; 5]> = computers - .iter() - .map(|computer| { - [ - computer["id"].as_str().unwrap_or("").to_string(), - status_of(computer).to_string(), - computer["template"].as_str().unwrap_or("-").to_string(), - format!( - "{}/{}", - computer["vcpu"].as_u64().unwrap_or(0), - computer["memoryMib"].as_u64().unwrap_or(0) - ), - computer["statusChangedAt"] - .as_str() - .unwrap_or("") - .to_string(), - ] - }) - .collect(); - let header = ["ID", "STATUS", "TEMPLATE", "VCPU/MIB", "SINCE"]; - let widths: Vec = (0..header.len()) - .map(|column| { - rows.iter() - .map(|row| row[column].len()) - .chain(std::iter::once(header[column].len())) - .max() - .unwrap_or(0) - }) - .collect(); - let line = |cells: [&str; 5]| { - cells - .iter() - .enumerate() - .map(|(column, cell)| format!("{cell:>() - .join(" ") - .trim_end() - .to_string() - }; - println!("{}", line(header)); - for row in &rows { - println!("{}", line([&row[0], &row[1], &row[2], &row[3], &row[4]])); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_env_reads_pairs_and_refuses_bad_ones() { - assert!(parse_env(&[]).unwrap().is_empty()); - let parsed = parse_env(&["A=1".into(), "B=x=y".into(), "C=".into()]).unwrap(); - assert_eq!(parsed["A"], "1"); - assert_eq!(parsed["B"], "x=y"); - assert_eq!(parsed["C"], ""); - assert!(parse_env(&["NOPE".into()]).is_err()); - assert!(parse_env(&["=1".into()]).is_err()); - assert!(parse_env(&["A=1".into(), "A=2".into()]).is_err()); - } - - #[test] - fn explicit_id_wins_and_blank_ids_are_ignored() { - assert_eq!( - choose_computer_id(Some(" cmp_a "), Some("cmp_env"), Some("cmp_cfg")).unwrap(), - "cmp_a" - ); - assert_eq!( - choose_computer_id(Some(" "), Some("cmp_env"), Some("cmp_cfg")).unwrap(), - "cmp_env" - ); - assert_eq!( - choose_computer_id(None, Some(""), Some("cmp_cfg")).unwrap(), - "cmp_cfg" - ); - assert!(choose_computer_id(None, None, None).is_err()); - } -} diff --git a/src/commands/computer/ssh.rs b/src/commands/computer/ssh.rs deleted file mode 100644 index 0230f91..0000000 --- a/src/commands/computer/ssh.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::io::IsTerminal; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{Context, Result, bail}; -use clap::Parser; -use russh::client::{self, AuthResult, Handler}; -use russh::keys::PublicKeyOrCertificate; -use russh::{Channel, ChannelMsg, Disconnect}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio_tungstenite::MaybeTlsStream; -use tokio_tungstenite::connect_async; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::http::HeaderValue; - -use super::resolve_computer_id; -use super::wsio::WsIo; -use crate::api::client::ApiError; -use crate::config::auth::Auth; -use crate::config::settings::ApiMode; -use crate::util::api; -use crate::util::output::SilentExit; - -pub const SUBPROTOCOL: &str = "steel-ssh-v1"; -const REMOTE_USER: &str = "root"; - -#[derive(Parser)] -pub struct Args { - /// Computer ID (defaults to STEEL_COMPUTER_ID or `steel computer use`) - pub computer_id: Option, - - /// Run this command instead of opening a shell, given after `--` - #[arg(last = true, value_name = "COMMAND")] - pub command: Vec, -} - -struct Client; - -impl Handler for Client { - type Error = russh::Error; - - async fn check_server_key( - &mut self, - _key: &PublicKeyOrCertificate, - ) -> Result { - Ok(true) - } -} - -pub async fn run(args: Args) -> Result<()> { - let id = resolve_computer_id(args.computer_id.as_deref())?; - let (mode, base_url, auth) = api::resolve_with_auth(); - if mode == ApiMode::Cloud && auth.api_key.is_none() { - return Err(ApiError::MissingAuth.into()); - } - let url = ssh_url(&base_url, &id)?; - let transport = connect(&url, &auth).await?; - - let config = Arc::new(client::Config { - keepalive_interval: Some(Duration::from_secs(15)), - ..Default::default() - }); - let mut handle = client::connect_stream(config, transport, Client) - .await - .context("The SSH handshake with the computer failed")?; - let mut auth_result = handle.authenticate_password(REMOTE_USER, "").await?; - if !matches!(auth_result, AuthResult::Success) { - auth_result = handle.authenticate_none(REMOTE_USER).await?; - } - if !matches!(auth_result, AuthResult::Success) { - bail!("The computer refused the SSH login."); - } - - let mut channel = handle.channel_open_session().await?; - let code = if args.command.is_empty() { - shell(&mut channel).await? - } else { - exec(&mut channel, &args.command).await? - }; - let _ = handle.disconnect(Disconnect::ByApplication, "", "en").await; - if code != 0 { - return Err(SilentExit(code).into()); - } - Ok(()) -} - -pub fn ssh_url(base_url: &str, id: &str) -> Result { - let mut url = - url::Url::parse(base_url).with_context(|| format!("Invalid API URL: {base_url}"))?; - let scheme = match url.scheme() { - "https" | "wss" => "wss", - "http" | "ws" => "ws", - other => bail!("Unsupported API URL scheme for SSH: {other}"), - }; - url.set_scheme(scheme) - .map_err(|_| anyhow::anyhow!("Failed to set the WebSocket scheme"))?; - let base_path = url.path().trim_end_matches('/').to_string(); - let base_path = if base_path.ends_with("/v1") { - base_path - } else { - format!("{base_path}/v1") - }; - url.set_path(&format!( - "{base_path}/computers/{}/ssh", - urlencoding::encode(id) - )); - url.set_query(None); - Ok(url.to_string()) -} - -async fn connect(url: &str, auth: &Auth) -> Result>> { - let mut request = url - .into_client_request() - .with_context(|| format!("Invalid SSH URL: {url}"))?; - request.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_static(SUBPROTOCOL), - ); - if let Some(key) = auth.api_key.as_deref().filter(|key| !key.trim().is_empty()) { - request.headers_mut().insert( - "Steel-Api-Key", - HeaderValue::from_str(key.trim()).context("The API key is not a valid header value")?, - ); - } - let (stream, response) = connect_async(request) - .await - .context("Failed to open the SSH bridge to the computer")?; - let accepted = response - .headers() - .get("Sec-WebSocket-Protocol") - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value.eq_ignore_ascii_case(SUBPROTOCOL)); - if !accepted { - bail!("The gateway did not accept the {SUBPROTOCOL} subprotocol."); - } - Ok(WsIo::new(stream)) -} - -async fn shell(channel: &mut Channel) -> Result { - let interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal(); - if interactive { - let (cols, rows) = terminal::size().unwrap_or((80, 24)); - let term = std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()); - channel - .request_pty(true, &term, cols, rows, 0, 0, &[]) - .await?; - } - channel.request_shell(true).await?; - let _raw = if interactive { - Some(terminal::RawMode::enable()?) - } else { - None - }; - pump(channel, interactive).await -} - -async fn exec(channel: &mut Channel, command: &[String]) -> Result { - channel.exec(true, shell_join(command)).await?; - pump(channel, false).await -} - -pub fn shell_join(argv: &[String]) -> String { - argv.iter() - .map(|arg| format!("'{}'", arg.replace('\'', "'\\''"))) - .collect::>() - .join(" ") -} - -async fn pump(channel: &mut Channel, forward_resize: bool) -> Result { - let mut stdin = tokio::io::stdin(); - let mut stdout = tokio::io::stdout(); - let mut stderr = tokio::io::stderr(); - let mut resize = terminal::resize_signal(forward_resize)?; - let mut buffer = [0u8; 8192]; - let mut code = 0; - let mut stdin_open = true; - loop { - tokio::select! { - message = channel.wait() => match message { - None | Some(ChannelMsg::Close) | Some(ChannelMsg::Eof) => break, - Some(ChannelMsg::Data { data }) => { - stdout.write_all(&data).await?; - stdout.flush().await?; - } - Some(ChannelMsg::ExtendedData { data, .. }) => { - stderr.write_all(&data).await?; - stderr.flush().await?; - } - Some(ChannelMsg::ExitStatus { exit_status }) => code = exit_status as i32, - Some(_) => {} - }, - read = stdin.read(&mut buffer), if stdin_open => match read? { - 0 => { - stdin_open = false; - channel.eof().await?; - } - n => channel.data(&buffer[..n]).await?, - }, - _ = terminal::resized(&mut resize) => { - if let Some((cols, rows)) = terminal::size() { - channel.window_change(cols, rows, 0, 0).await?; - } - } - } - } - Ok(code) -} - -#[cfg(unix)] -mod terminal { - use std::io; - - use rustix::termios::{self, OptionalActions, Termios}; - use tokio::signal::unix::{Signal, SignalKind, signal}; - - pub struct RawMode { - original: Termios, - } - - impl RawMode { - pub fn enable() -> io::Result { - let stdin = io::stdin(); - let original = termios::tcgetattr(&stdin)?; - let mut raw = original.clone(); - raw.make_raw(); - termios::tcsetattr(&stdin, OptionalActions::Now, &raw)?; - Ok(Self { original }) - } - } - - impl Drop for RawMode { - fn drop(&mut self) { - let _ = termios::tcsetattr(io::stdin(), OptionalActions::Now, &self.original); - } - } - - pub fn size() -> Option<(u32, u32)> { - let size = termios::tcgetwinsize(io::stdout()).ok()?; - (size.ws_col > 0).then(|| (u32::from(size.ws_col), u32::from(size.ws_row))) - } - - pub fn resize_signal(enabled: bool) -> io::Result> { - if enabled { - Ok(Some(signal(SignalKind::window_change())?)) - } else { - Ok(None) - } - } - - pub async fn resized(signal: &mut Option) { - match signal { - Some(signal) => { - signal.recv().await; - } - None => std::future::pending().await, - } - } -} - -#[cfg(not(unix))] -mod terminal { - use std::io; - - pub struct RawMode; - - impl RawMode { - pub fn enable() -> io::Result { - Ok(Self) - } - } - - pub fn size() -> Option<(u32, u32)> { - None - } - - pub fn resize_signal(_enabled: bool) -> io::Result> { - Ok(None) - } - - pub async fn resized(_signal: &mut Option<()>) { - std::future::pending().await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ssh_urls_follow_the_api_host() { - assert_eq!( - ssh_url("https://api.steel.dev/v1", "cmp_a").unwrap(), - "wss://api.steel.dev/v1/computers/cmp_a/ssh" - ); - assert_eq!( - ssh_url("http://localhost:3000", "cmp_a").unwrap(), - "ws://localhost:3000/v1/computers/cmp_a/ssh" - ); - assert!(ssh_url("ftp://x", "cmp_a").is_err()); - } - - #[test] - fn remote_commands_are_quoted_for_the_shell() { - let argv = vec!["echo".to_string(), "it's here".to_string()]; - assert_eq!(shell_join(&argv), "'echo' 'it'\\''s here'"); - } -} diff --git a/src/commands/computer/wsio.rs b/src/commands/computer/wsio.rs deleted file mode 100644 index 1574920..0000000 --- a/src/commands/computer/wsio.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use futures_util::{Sink, Stream}; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tokio_tungstenite::WebSocketStream; -use tokio_tungstenite::tungstenite::{Bytes, Message}; - -pub struct WsIo { - inner: WebSocketStream, - pending: Bytes, - closed: bool, -} - -impl WsIo { - pub const fn new(inner: WebSocketStream) -> Self { - Self { - inner, - pending: Bytes::new(), - closed: false, - } - } -} - -impl AsyncRead for WsIo { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - if !self.pending.is_empty() { - let n = buf.remaining().min(self.pending.len()); - let chunk = self.pending.split_to(n); - buf.put_slice(&chunk); - return Poll::Ready(Ok(())); - } - if self.closed { - return Poll::Ready(Ok(())); - } - if let Poll::Ready(Err(err)) = Pin::new(&mut self.inner).poll_flush(cx) { - return Poll::Ready(Err(io::Error::other(err))); - } - match Pin::new(&mut self.inner).poll_next(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => { - self.closed = true; - return Poll::Ready(Ok(())); - } - Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(io::Error::other(err))), - Poll::Ready(Some(Ok(message))) => match message { - Message::Binary(bytes) => self.pending = bytes, - Message::Text(text) => self.pending = Bytes::from(text), - Message::Close(_) => { - self.closed = true; - return Poll::Ready(Ok(())); - } - _ => {} - }, - } - } - } -} - -impl AsyncWrite for WsIo { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - data: &[u8], - ) -> Poll> { - match Pin::new(&mut self.inner).poll_ready(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(io::Error::other(err))), - Poll::Ready(Ok(())) => { - Pin::new(&mut self.inner) - .start_send(Message::Binary(Bytes::copy_from_slice(data))) - .map_err(io::Error::other)?; - if let Poll::Ready(Err(err)) = Pin::new(&mut self.inner).poll_flush(cx) { - return Poll::Ready(Err(io::Error::other(err))); - } - Poll::Ready(Ok(data.len())) - } - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.inner) - .poll_flush(cx) - .map_err(io::Error::other) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.inner) - .poll_close(cx) - .map_err(io::Error::other) - } -} diff --git a/src/commands/describe.rs b/src/commands/describe.rs index fd6d36c..e05fb50 100644 --- a/src/commands/describe.rs +++ b/src/commands/describe.rs @@ -580,29 +580,29 @@ mod tests { } #[test] - fn computer_exec_lists_its_api_operation() { - let v = describe_path(&["computer", "exec"]); + fn sessions_get_lists_its_api_operation() { + let v = describe_path(&["sessions", "get"]); let ops = v["api_operations"].as_array().unwrap(); assert_eq!(ops.len(), 1); - assert_eq!(ops[0]["operation_id"], "exec_computer"); - assert_eq!(ops[0]["method"], "POST"); - assert_eq!(ops[0]["path"], "/v1/computers/{id}/exec"); - assert_eq!( - ops[0]["example"], - "steel computer exec -- " - ); + assert_eq!(ops[0]["operation_id"], "get_session"); + assert_eq!(ops[0]["method"], "GET"); + assert_eq!(ops[0]["path"], "/v1/sessions/{id}"); + assert_eq!(ops[0]["example"], "steel sessions get "); assert!(ops[0].get("streaming").is_none()); } #[test] - fn computer_ssh_is_a_websocket_operation() { - let v = describe_path(&["computer", "ssh"]); + fn sessions_logs_is_a_websocket_operation() { + let v = describe_path(&["sessions", "logs"]); let ops = v["api_operations"].as_array().unwrap(); assert_eq!(ops.len(), 1); - assert_eq!(ops[0]["operation_id"], "attach_computer_ssh"); + assert_eq!(ops[0]["operation_id"], "get_session_logs"); assert_eq!(ops[0]["method"], "GET"); - assert_eq!(ops[0]["example"], "steel computer ssh "); + assert_eq!( + ops[0]["example"], + "steel sessions logs --follow" + ); assert_eq!(ops[0]["streaming"]["transport"], "websocket"); - assert_eq!(ops[0]["streaming"]["path"], "/v1/computers/{id}/ssh"); + assert_eq!(ops[0]["streaming"]["path"], "/v1/sessions/{id}/logs"); } } diff --git a/src/commands/environment.rs b/src/commands/environment.rs deleted file mode 100644 index 3aa434e..0000000 --- a/src/commands/environment.rs +++ /dev/null @@ -1,371 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Context, Result, bail}; -use clap::{Args, Parser, Subcommand}; -use serde_json::{Value, json}; - -use crate::api::client::SteelClient; -use crate::api::environments::{CreateEnvironment, EnvironmentSpec, UpdateEnvironment}; -use crate::api::secrets::parse_secret_ids; -use crate::commands::secret::ProjectArgs; -use crate::util::pairs::parse_pairs; -use crate::util::{api, output}; - -#[derive(Subcommand)] -pub enum Command { - /// List environments - List(ProjectArgs), - - /// Create an environment - Create(CreateArgs), - - /// Get one environment - Get(IdArgs), - - /// Change an environment's name, spec or secrets - Update(UpdateArgs), - - /// Delete an environment - Delete(IdArgs), -} - -impl Command { - pub const fn telemetry_name(&self) -> &'static str { - match self { - Self::List(_) => "list", - Self::Create(_) => "create", - Self::Get(_) => "get", - Self::Update(_) => "update", - Self::Delete(_) => "delete", - } - } -} - -#[derive(Parser)] -pub struct IdArgs { - /// Environment ID - pub environment_id: String, - - #[command(flatten)] - pub project: ProjectArgs, -} - -#[derive(Args)] -pub struct SpecArgs { - /// Template name - #[arg(long)] - pub template: Option, - - /// Number of vCPUs - #[arg(long)] - pub vcpu: Option, - - /// Memory in MiB - #[arg(long = "memory", value_name = "MIB")] - pub memory_mib: Option, - - /// Disk in MiB - #[arg(long = "disk", value_name = "MIB")] - pub disk_mib: Option, - - /// Delete computers after this many seconds of running time - #[arg(long = "timeout", value_name = "SECONDS")] - pub timeout_seconds: Option, - - /// Pause instead of deleting when the timeout is reached - #[arg(long = "auto-pause")] - pub auto_pause: bool, - - /// Environment variable for computers, repeatable - #[arg(long = "env", value_name = "KEY=VALUE")] - pub env: Vec, -} - -impl SpecArgs { - pub fn to_spec(&self) -> Result { - Ok(EnvironmentSpec { - template: self.template.clone(), - vcpu: self.vcpu, - memory_mib: self.memory_mib, - disk_mib: self.disk_mib, - timeout_seconds: self.timeout_seconds, - auto_pause: self.auto_pause.then_some(true), - env: parse_pairs("--env", "KEY=VALUE", &self.env)?, - }) - } -} - -#[derive(Args)] -pub struct SecretArgs { - /// Attach a stored secret as an environment variable, repeatable - #[arg(long = "secret", value_name = "NAME=SECRET_ID")] - pub secrets: Vec, - - /// Store a new secret and attach it, repeatable - #[arg(long = "secret-value", value_name = "NAME=VALUE")] - pub secret_values: Vec, -} - -#[derive(Parser)] -pub struct CreateArgs { - /// Environment name - pub name: String, - - #[command(flatten)] - pub spec: SpecArgs, - - #[command(flatten)] - pub secrets: SecretArgs, - - #[command(flatten)] - pub project: ProjectArgs, -} - -#[derive(Parser)] -pub struct UpdateArgs { - /// Environment ID - pub environment_id: String, - - /// New name - #[arg(long)] - pub name: Option, - - #[command(flatten)] - pub spec: SpecArgs, - - #[command(flatten)] - pub secrets: SecretArgs, - - /// Detach a secret, repeatable - #[arg(long = "unset-secret", value_name = "NAME")] - pub unset_secrets: Vec, - - #[command(flatten)] - pub project: ProjectArgs, -} - -pub async fn run(command: Command) -> Result<()> { - match command { - Command::List(args) => run_list(args).await, - Command::Create(args) => run_create(args).await, - Command::Get(args) => run_get(args).await, - Command::Update(args) => run_update(args).await, - Command::Delete(args) => run_delete(args).await, - } -} - -pub fn secret_declarations( - ids: &[String], - values: &[String], - unset: &[String], -) -> Result> { - let mut declarations = BTreeMap::new(); - for (name, id) in parse_secret_ids(ids)? { - declarations.insert(name, json!({ "secretId": id })); - } - for (name, value) in parse_pairs("--secret-value", "NAME=VALUE", values)? { - if value.is_empty() { - bail!("--secret-value {name} must not be empty."); - } - if declarations - .insert(name.clone(), json!({ "value": value })) - .is_some() - { - bail!("{name} was given more than once."); - } - } - for name in unset { - let name = name.trim(); - if name.is_empty() { - bail!("--unset-secret wants a name."); - } - if declarations.insert(name.to_string(), Value::Null).is_some() { - bail!("{name} was given more than once."); - } - } - Ok(declarations) -} - -pub fn name_of(environment: &Value) -> &str { - environment["name"].as_str().unwrap_or("-") -} - -pub fn id_of(environment: &Value) -> Result { - environment["id"] - .as_str() - .map(str::to_string) - .context("the API returned an environment without an id") -} - -async fn run_list(args: ProjectArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .list_environments(&base_url, mode, &auth, args.project_id.as_deref()) - .await?; - if output::is_json() { - output::success_data(data); - } else { - print_environments(&data); - } - Ok(()) -} - -async fn run_create(args: CreateArgs) -> Result<()> { - let spec = args.spec.to_spec()?; - let request = CreateEnvironment { - name: args.name, - project_id: args.project.project_id, - spec: (!spec.is_empty()).then(|| spec.body()), - secrets: secret_declarations(&args.secrets.secrets, &args.secrets.secret_values, &[])?, - }; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .create_environment(&base_url, mode, &auth, &request) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Created {} as {}.", name_of(&data), id_of(&data)?); - } - Ok(()) -} - -async fn run_get(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .get_environment( - &base_url, - mode, - &auth, - &args.environment_id, - args.project.project_id.as_deref(), - ) - .await?; - output::success_data(data); - Ok(()) -} - -async fn run_update(args: UpdateArgs) -> Result<()> { - let project = args.project.project_id.as_deref(); - let spec_changes = args.spec.to_spec()?; - let secrets = secret_declarations( - &args.secrets.secrets, - &args.secrets.secret_values, - &args.unset_secrets, - )?; - if args.name.is_none() && spec_changes.is_empty() && secrets.is_empty() { - bail!( - "Give something to change: --name, a spec flag, --secret, --secret-value or --unset-secret." - ); - } - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let spec = if spec_changes.is_empty() { - None - } else { - let current = client - .get_environment(&base_url, mode, &auth, &args.environment_id, project) - .await?; - Some(spec_changes.apply_to(current["spec"].clone())) - }; - let request = UpdateEnvironment { - name: args.name, - spec, - secrets, - }; - let data = client - .update_environment( - &base_url, - mode, - &auth, - &args.environment_id, - project, - &request, - ) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Updated {}.", args.environment_id); - } - Ok(()) -} - -async fn run_delete(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .delete_environment( - &base_url, - mode, - &auth, - &args.environment_id, - args.project.project_id.as_deref(), - ) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Deleted {}.", args.environment_id); - } - Ok(()) -} - -fn print_environments(data: &Value) { - let environments = data["environments"].as_array().cloned().unwrap_or_default(); - if environments.is_empty() { - println!("No environments."); - return; - } - let rows: Vec> = environments - .iter() - .map(|environment| { - vec![ - environment["id"].as_str().unwrap_or("").to_string(), - name_of(environment).to_string(), - environment["spec"]["template"] - .as_str() - .unwrap_or("-") - .to_string(), - environment["secrets"] - .as_object() - .map_or(0, serde_json::Map::len) - .to_string(), - environment["updatedAt"].as_str().unwrap_or("").to_string(), - ] - }) - .collect(); - output::print_table(&["ID", "NAME", "TEMPLATE", "SECRETS", "UPDATED"], &rows); -} - -#[cfg(test)] -mod tests { - use super::*; - - const ID: &str = "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31"; - - #[test] - fn declarations_cover_ids_values_and_unsets() { - let declarations = secret_declarations( - &[format!("TOKEN={ID}")], - &["KEY=abc".into()], - &["OLD".into()], - ) - .unwrap(); - assert_eq!(declarations["TOKEN"], json!({ "secretId": ID })); - assert_eq!(declarations["KEY"], json!({ "value": "abc" })); - assert_eq!(declarations["OLD"], Value::Null); - } - - #[test] - fn declarations_refuse_conflicts_and_blanks() { - assert!(secret_declarations(&["TOKEN=plain".into()], &[], &[]).is_err()); - assert!(secret_declarations(&[], &["KEY=".into()], &[]).is_err()); - assert!(secret_declarations(&[format!("A={ID}")], &["A=x".into()], &[]).is_err()); - assert!(secret_declarations(&[], &["A=x".into()], &["A".into()]).is_err()); - assert!(secret_declarations(&[], &[], &[" ".into()]).is_err()); - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d7357ed..af0a2d5 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,14 +1,11 @@ pub mod browser; pub mod cache; -pub mod checkpoint; pub mod completion; -pub mod computer; pub mod config; pub mod credentials; pub mod describe; pub mod dev; pub mod doctor; -pub mod environment; pub mod forge; pub mod init; pub mod login; @@ -17,7 +14,6 @@ pub mod pdf; pub mod profile; pub mod scrape; pub mod screenshot; -pub mod secret; pub mod sessions; pub mod skills; pub mod update; @@ -294,30 +290,6 @@ pub enum Command { command: sessions::Command, }, - /// Cloud computers: create, run commands, ssh - Computer { - #[command(subcommand)] - command: computer::Command, - }, - - /// Computer checkpoints: list, restore, delete - Checkpoint { - #[command(subcommand)] - command: checkpoint::Command, - }, - - /// Secrets for computers: list, create, update, delete - Secret { - #[command(subcommand)] - command: secret::Command, - }, - - /// Reusable computer environments: a spec plus secrets - Environment { - #[command(subcommand)] - command: environment::Command, - }, - /// One-command onboarding: login + verify + install agent skills Init(init::Args), @@ -379,12 +351,6 @@ fn telemetry_command_path(command: &Command) -> Option { Command::Pdf(_) => Some("pdf".to_string()), Command::Browser(args) => Some(format!("browser.{}", args.command.telemetry_name())), Command::Sessions { command } => Some(format!("sessions.{}", command.telemetry_name())), - Command::Computer { command } => Some(format!("computer.{}", command.telemetry_name())), - Command::Checkpoint { command } => Some(format!("checkpoint.{}", command.telemetry_name())), - Command::Secret { command } => Some(format!("secret.{}", command.telemetry_name())), - Command::Environment { command } => { - Some(format!("environment.{}", command.telemetry_name())) - } Command::Init(_) => Some("init".to_string()), Command::Login(_) => Some("login".to_string()), Command::Logout(_) => Some("logout".to_string()), @@ -432,10 +398,6 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> { Command::Pdf(args) => pdf::run(args).await, Command::Browser(args) => browser::run(args).await, Command::Sessions { command } => sessions::run(command).await, - Command::Computer { command } => computer::run(command).await, - Command::Checkpoint { command } => checkpoint::run(command).await, - Command::Secret { command } => secret::run(command).await, - Command::Environment { command } => environment::run(command).await, Command::Init(args) => init::run(args).await, Command::Login(args) => login::run(args).await, Command::Logout(args) => logout::run(args).await, diff --git a/src/commands/secret.rs b/src/commands/secret.rs deleted file mode 100644 index beee88b..0000000 --- a/src/commands/secret.rs +++ /dev/null @@ -1,282 +0,0 @@ -use std::io::Read; - -use anyhow::{Context, Result, bail}; -use clap::{Args, Parser, Subcommand}; -use serde_json::Value; - -use crate::api::client::SteelClient; -use crate::api::secrets::{CreateSecret, UpdateSecret}; -use crate::util::{api, output}; - -#[derive(Subcommand)] -pub enum Command { - /// List secrets - List(ProjectArgs), - - /// Store a new secret - Create(CreateArgs), - - /// Get one secret's metadata - Get(IdArgs), - - /// Rename a secret or replace its value - Update(UpdateArgs), - - /// Delete a secret - Delete(IdArgs), -} - -impl Command { - pub const fn telemetry_name(&self) -> &'static str { - match self { - Self::List(_) => "list", - Self::Create(_) => "create", - Self::Get(_) => "get", - Self::Update(_) => "update", - Self::Delete(_) => "delete", - } - } -} - -#[derive(Args)] -pub struct ProjectArgs { - /// Project to use (defaults to the API key's project) - #[arg(long = "project", value_name = "PROJECT_ID")] - pub project_id: Option, -} - -#[derive(Parser)] -pub struct IdArgs { - /// Secret ID - pub secret_id: String, - - #[command(flatten)] - pub project: ProjectArgs, -} - -#[derive(Parser)] -pub struct CreateArgs { - /// Secret name, also the environment variable name inside computers - pub name: String, - - /// The secret value - #[arg(long, value_name = "VALUE", conflicts_with = "value_stdin")] - pub value: Option, - - /// Read the secret value from stdin - #[arg(long = "value-stdin")] - pub value_stdin: bool, - - #[command(flatten)] - pub project: ProjectArgs, -} - -#[derive(Parser)] -pub struct UpdateArgs { - /// Secret ID - pub secret_id: String, - - /// New name - #[arg(long)] - pub name: Option, - - /// New value - #[arg(long, value_name = "VALUE", conflicts_with = "value_stdin")] - pub value: Option, - - /// Read the new value from stdin - #[arg(long = "value-stdin")] - pub value_stdin: bool, - - #[command(flatten)] - pub project: ProjectArgs, -} - -pub async fn run(command: Command) -> Result<()> { - match command { - Command::List(args) => run_list(args).await, - Command::Create(args) => run_create(args).await, - Command::Get(args) => run_get(args).await, - Command::Update(args) => run_update(args).await, - Command::Delete(args) => run_delete(args).await, - } -} - -pub fn trim_trailing_newline(text: &str) -> &str { - let text = text.strip_suffix('\n').unwrap_or(text); - text.strip_suffix('\r').unwrap_or(text) -} - -pub fn read_value(value: Option, from_stdin: bool) -> Result> { - if from_stdin { - let mut text = String::new(); - std::io::stdin() - .read_to_string(&mut text) - .context("Failed to read the value from stdin")?; - let value = trim_trailing_newline(&text).to_string(); - if value.is_empty() { - bail!("Nothing was read from stdin."); - } - return Ok(Some(value)); - } - if value.as_deref() == Some("") { - bail!("--value must not be empty."); - } - Ok(value) -} - -pub fn name_of(secret: &Value) -> &str { - secret["name"].as_str().unwrap_or("-") -} - -pub fn id_of(secret: &Value) -> Result { - secret["id"] - .as_str() - .map(str::to_string) - .context("the API returned a secret without an id") -} - -async fn run_list(args: ProjectArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .list_secrets(&base_url, mode, &auth, args.project_id.as_deref()) - .await?; - if output::is_json() { - output::success_data(data); - } else { - print_secrets(&data); - } - Ok(()) -} - -async fn run_create(args: CreateArgs) -> Result<()> { - let Some(value) = read_value(args.value, args.value_stdin)? else { - bail!("Give the value with --value or --value-stdin."); - }; - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let request = CreateSecret { - name: args.name, - value, - project_id: args.project.project_id, - }; - let data = client - .create_secret(&base_url, mode, &auth, &request) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Created {} as {}.", name_of(&data), id_of(&data)?); - } - Ok(()) -} - -async fn run_get(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .get_secret( - &base_url, - mode, - &auth, - &args.secret_id, - args.project.project_id.as_deref(), - ) - .await?; - output::success_data(data); - Ok(()) -} - -async fn run_update(args: UpdateArgs) -> Result<()> { - let request = UpdateSecret { - name: args.name, - value: read_value(args.value, args.value_stdin)?, - }; - if request.is_empty() { - bail!("Give something to change: --name, --value or --value-stdin."); - } - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .update_secret( - &base_url, - mode, - &auth, - &args.secret_id, - args.project.project_id.as_deref(), - &request, - ) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Updated {}.", args.secret_id); - } - Ok(()) -} - -async fn run_delete(args: IdArgs) -> Result<()> { - let (mode, base_url, auth) = api::resolve_with_auth(); - let client = SteelClient::new()?; - let data = client - .delete_secret( - &base_url, - mode, - &auth, - &args.secret_id, - args.project.project_id.as_deref(), - ) - .await?; - if output::is_json() { - output::success_data(data); - } else { - println!("Deleted {}.", args.secret_id); - } - Ok(()) -} - -fn print_secrets(data: &Value) { - let secrets = data["secrets"].as_array().cloned().unwrap_or_default(); - if secrets.is_empty() { - println!("No secrets."); - return; - } - let rows: Vec> = secrets - .iter() - .map(|secret| { - vec![ - secret["id"].as_str().unwrap_or("").to_string(), - name_of(secret).to_string(), - secret["version"] - .as_u64() - .map_or_else(|| "-".to_string(), |version| version.to_string()), - secret["updatedAt"].as_str().unwrap_or("").to_string(), - ] - }) - .collect(); - output::print_table(&["ID", "NAME", "VERSION", "UPDATED"], &rows); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn trailing_newlines_are_dropped_once() { - assert_eq!(trim_trailing_newline("abc\n"), "abc"); - assert_eq!(trim_trailing_newline("abc\r\n"), "abc"); - assert_eq!(trim_trailing_newline("abc\n\n"), "abc\n"); - assert_eq!(trim_trailing_newline("abc"), "abc"); - } - - #[test] - fn values_from_flags_pass_through_and_empty_ones_are_refused() { - assert_eq!(read_value(None, false).unwrap(), None); - assert_eq!( - read_value(Some("x".into()), false).unwrap().as_deref(), - Some("x") - ); - assert!(read_value(Some(String::new()), false).is_err()); - } -} diff --git a/src/config/settings.rs b/src/config/settings.rs index 5656879..ac4aab6 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -113,8 +113,6 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub telemetry: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub computer: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub onboarding: Option, } @@ -125,13 +123,6 @@ pub struct OnboardingConfig { pub source: Option, } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ComputerConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub default_id: Option, -} - #[derive(Debug, Serialize, Deserialize, Default, Clone)] #[serde(rename_all = "camelCase")] pub struct BrowserConfig { @@ -162,13 +153,6 @@ impl Config { .unwrap_or(false) } - pub fn default_computer_id(&self) -> Option<&str> { - self.computer - .as_ref() - .and_then(|c| c.default_id.as_deref()) - .filter(|s| !s.trim().is_empty()) - } - pub fn onboarding_source(&self) -> Option<&str> { self.onboarding .as_ref() @@ -233,7 +217,6 @@ mod tests { telemetry: Some(TelemetryConfig { disabled: Some(true), }), - computer: None, onboarding: Some(OnboardingConfig { source: Some("claude-code".into()), }), diff --git a/src/util/mod.rs b/src/util/mod.rs index 16676b4..b2a2124 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,3 @@ pub mod api; pub mod output; -pub mod pairs; pub mod url; diff --git a/src/util/output.rs b/src/util/output.rs index 342f55c..d937b2e 100644 --- a/src/util/output.rs +++ b/src/util/output.rs @@ -256,7 +256,7 @@ fn classify_error(err: &anyhow::Error) -> (i32, &'static str, Option<&'static st crate::api::client::ApiError::RetryLater { .. } => ( exit_code::API_SERVER, "not_ready", - Some("The computer is not ready. Try again shortly."), + Some("The API asked to retry later. Try again shortly."), ), crate::api::client::ApiError::RequestFailed { status, .. } => match *status { 401 => ( diff --git a/src/util/pairs.rs b/src/util/pairs.rs deleted file mode 100644 index adfa951..0000000 --- a/src/util/pairs.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Result, bail}; - -pub fn parse_pairs(flag: &str, shape: &str, pairs: &[String]) -> Result> { - let mut parsed = BTreeMap::new(); - for pair in pairs { - let Some((name, value)) = pair.split_once('=') else { - bail!("{flag} wants {shape}, got {pair:?}."); - }; - if name.is_empty() { - bail!("{flag} wants a name before the '=', got {pair:?}."); - } - if parsed.insert(name.to_string(), value.to_string()).is_some() { - bail!("{flag} {name} was given twice."); - } - } - Ok(parsed) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reads_pairs_and_refuses_bad_ones() { - assert!(parse_pairs("--env", "KEY=VALUE", &[]).unwrap().is_empty()); - let parsed = parse_pairs( - "--env", - "KEY=VALUE", - &["A=1".into(), "B=x=y".into(), "C=".into()], - ) - .unwrap(); - assert_eq!(parsed["A"], "1"); - assert_eq!(parsed["B"], "x=y"); - assert_eq!(parsed["C"], ""); - let err = parse_pairs("--env", "KEY=VALUE", &["NOPE".into()]).unwrap_err(); - assert!(err.to_string().contains("--env wants KEY=VALUE")); - assert!(parse_pairs("--env", "KEY=VALUE", &["=1".into()]).is_err()); - let err = - parse_pairs("--secret", "NAME=SECRET_ID", &["A=1".into(), "A=2".into()]).unwrap_err(); - assert!(err.to_string().contains("--secret A was given twice")); - } -} diff --git a/tests/cli-spec.json b/tests/cli-spec.json index 00c5150..561c769 100644 --- a/tests/cli-spec.json +++ b/tests/cli-spec.json @@ -18,31 +18,6 @@ ] }, { "name": "cache" }, - { - "name": "checkpoint", - "subcommands": [ - { "name": "delete" }, - { "name": "get" }, - { "name": "list" }, - { "name": "restore" } - ] - }, - { - "name": "computer", - "subcommands": [ - { "name": "checkpoint" }, - { "name": "create" }, - { "name": "delete" }, - { "name": "exec" }, - { "name": "get" }, - { "name": "list" }, - { "name": "pause" }, - { "name": "quota" }, - { "name": "resume" }, - { "name": "ssh" }, - { "name": "use" } - ] - }, { "name": "config" }, { "name": "credentials", @@ -61,16 +36,6 @@ { "name": "stop" } ] }, - { - "name": "environment", - "subcommands": [ - { "name": "create" }, - { "name": "delete" }, - { "name": "get" }, - { "name": "list" }, - { "name": "update" } - ] - }, { "name": "forge" }, { "name": "login" }, { "name": "logout" }, @@ -86,16 +51,6 @@ }, { "name": "scrape" }, { "name": "screenshot" }, - { - "name": "secret", - "subcommands": [ - { "name": "create" }, - { "name": "delete" }, - { "name": "get" }, - { "name": "list" }, - { "name": "update" } - ] - }, { "name": "update" } ] } diff --git a/tests/cli_blackbox.rs b/tests/cli_blackbox.rs index 8abfbcf..ed4537e 100644 --- a/tests/cli_blackbox.rs +++ b/tests/cli_blackbox.rs @@ -755,66 +755,3 @@ fn help_flag_exits_with_code_0() { let code = output.status.code().expect("should have exit code"); assert_eq!(code, 0, "--help should exit with code 0, got: {code}"); } - -// ─── Computers ────────────────────────────────────────────────────── - -fn run_without_computer_context(args: &[&str]) -> Output { - let (mut cmd, _tmp) = steel_cmd(); - cmd.env_remove("STEEL_COMPUTER_ID"); - cmd.args(args); - cmd.output().expect("failed to execute steel binary") -} - -#[test] -fn computer_help_lists_subcommands() { - let output = run(&["computer", "--help"]); - assert!( - output.status.success(), - "steel computer --help should exit 0" - ); - let out = stdout(&output); - for sub in &[ - "create", "list", "get", "delete", "pause", "resume", "use", "exec", "ssh", - ] { - assert!( - out.contains(sub), - "computer help should list '{sub}', got: {out}" - ); - } -} - -#[test] -fn computer_exec_help_shows_expected_flags() { - let output = run(&["computer", "exec", "--help"]); - assert!( - output.status.success(), - "steel computer exec --help should exit 0" - ); - let out = stdout(&output); - for flag in &["--command", "--cwd", "--env", "--timeout", "ARGV"] { - assert!( - out.contains(flag), - "computer exec help should mention '{flag}', got: {out}" - ); - } -} - -#[test] -fn computer_exec_without_a_computer_explains_how_to_pick_one() { - let output = run_without_computer_context(&["computer", "exec", "--", "ls"]); - assert!( - !output.status.success(), - "exec without a computer should fail" - ); - let text = format!("{}{}", stdout(&output), stderr(&output)); - assert!( - text.contains("steel computer use"), - "the error should point at `steel computer use`, got: {text}" - ); -} - -#[test] -fn computer_use_without_an_id_fails() { - let output = run_without_computer_context(&["computer", "use"]); - assert!(!output.status.success(), "use without an id should fail"); -} diff --git a/tests/computer_checkpoint.rs b/tests/computer_checkpoint.rs deleted file mode 100644 index 9b0f25a..0000000 --- a/tests/computer_checkpoint.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! End-to-end tests for the checkpoint commands against a fake API host. -//! -//! The real `steel` binary runs against a wiremock server, so the request -//! shapes, the waiting loops and the printed tables are exercised as a user -//! sees them. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{body_partial_json, header, method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -const COMPUTER: &str = "cmp_0000123456789abcdefghjkmnpqrs"; -const CHECKPOINT: &str = "ckpt_0000123456789abcdefghjkmn"; - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.env_remove("STEEL_COMPUTER_ID"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn stdout(output: &Output) -> String { - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn checkpoint(status: &str) -> serde_json::Value { - json!({ - "id": CHECKPOINT, - "computerId": COMPUTER, - "name": "ready", - "status": status, - "sizeBytes": 2_147_483_648u64, - "statusChangedAt": "2026-09-11T00:00:00Z" - }) -} - -#[tokio::test(flavor = "multi_thread")] -async fn checkpoint_create_waits_until_the_snapshot_is_ready() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(format!("/v1/computers/{COMPUTER}/checkpoints"))) - .and(header("steel-api-key", "ste-test-key")) - .and(body_partial_json(json!({ "name": "ready" }))) - .respond_with(ResponseTemplate::new(201).set_body_json(checkpoint("creating"))) - .expect(1) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/v1/checkpoints/{CHECKPOINT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(checkpoint("creating"))) - .up_to_n_times(1) - .expect(1) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/v1/checkpoints/{CHECKPOINT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(checkpoint("ready"))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "computer", - "checkpoint", - COMPUTER, - "--name", - "ready", - "--wait", - ], - ) - .await; - - assert!(output.status.success()); - assert!(stdout(&output).contains(&format!("{CHECKPOINT} is ready."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn checkpoint_create_stops_when_the_snapshot_fails() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(format!("/v1/computers/{COMPUTER}/checkpoints"))) - .respond_with(ResponseTemplate::new(201).set_body_json(checkpoint("creating"))) - .expect(1) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/v1/checkpoints/{CHECKPOINT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(checkpoint("failed"))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "checkpoint", COMPUTER, "--wait"]).await; - - assert!(!output.status.success()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn checkpoint_list_prints_a_table() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/checkpoints")) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ "checkpoints": [checkpoint("ready")] })), - ) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["checkpoint", "list"]).await; - - assert!(output.status.success()); - let text = stdout(&output); - assert!(text.contains("ID")); - assert!(text.contains(CHECKPOINT)); - assert!(text.contains("ready")); - assert!(text.contains("2.0G")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn checkpoint_restore_starts_a_computer_and_can_wait_for_it() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(format!("/v1/checkpoints/{CHECKPOINT}/computers"))) - .and(body_partial_json(json!({ "timeoutSeconds": 600 }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "id": COMPUTER, - "status": "creating" - }))) - .expect(1) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/v1/computers/{COMPUTER}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "id": COMPUTER, - "status": "running" - }))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "checkpoint", - "restore", - CHECKPOINT, - "--timeout", - "600", - "--wait", - "--use", - ], - ) - .await; - - assert!(output.status.success()); - let text = stdout(&output); - assert!(text.contains(&format!("{COMPUTER} is running."))); - assert!(text.contains("default computer")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn checkpoint_delete_reports_the_id() { - let server = MockServer::start().await; - Mock::given(method("DELETE")) - .and(path(format!("/v1/checkpoints/{CHECKPOINT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(checkpoint("deleting"))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["checkpoint", "delete", CHECKPOINT]).await; - - assert!(output.status.success()); - assert!(stdout(&output).contains(&format!("Deleted {CHECKPOINT}."))); -} diff --git a/tests/computer_create.rs b/tests/computer_create.rs deleted file mode 100644 index dffc545..0000000 --- a/tests/computer_create.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! End-to-end tests for `steel computer create` with secrets, an environment and a project. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{body_json, method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -const COMPUTER: &str = "cmp_0000123456789abcdefghjkmnpqrs"; -const ENVIRONMENT: &str = "9b2d1c5e-1b2a-4c3d-8e4f-5a6b7c8d9e0f"; -const SECRET: &str = "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31"; -const PROJECT: &str = "0c1d2e3f-4a5b-4c6d-8e7f-8091a2b3c4d5"; - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.env_remove("STEEL_COMPUTER_ID"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn stdout(output: &Output) -> String { - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn stderr(output: &Output) -> String { - String::from_utf8_lossy(&output.stderr).to_string() -} - -#[tokio::test(flavor = "multi_thread")] -async fn create_sends_secrets_environment_and_project() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/computers")) - .and(body_json(json!({ - "environmentId": ENVIRONMENT, - "secrets": { "TOKEN": SECRET }, - "projectId": PROJECT, - "env": { "A": "1" } - }))) - .respond_with( - ResponseTemplate::new(201) - .set_body_json(json!({ "id": COMPUTER, "status": "starting" })), - ) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "computer", - "create", - "--environment", - ENVIRONMENT, - "--secret", - &format!("TOKEN={SECRET}"), - "--project", - PROJECT, - "--env", - "A=1", - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("{COMPUTER} is starting."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn create_refuses_plain_values_for_secret() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/computers")) - .respond_with(ResponseTemplate::new(500)) - .expect(0) - .mount(&server) - .await; - - let output = run_steel( - &server, - &["computer", "create", "--secret", "TOKEN=sk-live-abc"], - ) - .await; - - assert!(!output.status.success()); - assert!(stderr(&output).contains("steel secret create")); -} diff --git a/tests/computer_exec.rs b/tests/computer_exec.rs deleted file mode 100644 index cec731f..0000000 --- a/tests/computer_exec.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! End-to-end tests for `steel computer exec` against a fake API host. -//! -//! The real `steel` binary runs against a wiremock server that plays the -//! box gateway, so the request shape, the ndjson stream, the collect mode, -//! the wake retry and the exit codes are all exercised as a user sees them. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{body_partial_json, header, method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -const COMPUTER: &str = "cmp_0000123456789abcdefghjkmnpqrs"; - -fn exec_path() -> String { - format!("/v1/computers/{COMPUTER}/exec") -} - -async fn run_steel(server: &MockServer, args: &[&str], env: &[(&str, &str)]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.env_remove("STEEL_COMPUTER_ID"); - for (key, value) in env { - cmd.env(key, value); - } - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn stdout(output: &Output) -> String { - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn stderr(output: &Output) -> String { - String::from_utf8_lossy(&output.stderr).to_string() -} - -fn ndjson(events: &[serde_json::Value]) -> ResponseTemplate { - let body = events - .iter() - .map(|event| event.to_string() + "\n") - .collect::(); - ResponseTemplate::new(200).set_body_raw(body, "application/x-ndjson") -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_streams_output_and_returns_the_remote_exit_code() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .and(header("steel-api-key", "ste-test-key")) - .and(body_partial_json(json!({ - "argv": ["sh", "-c", "echo hi; exit 3"], - "cwd": "/tmp", - "env": { "A": "1" }, - "timeoutSeconds": 30, - "stream": true - }))) - .respond_with(ndjson(&[ - json!({"event": "start"}), - json!({"event": "output", "data": "hi\n"}), - json!({"event": "keepalive"}), - json!({"event": "output", "data": "and more"}), - json!({"event": "exit", "exitCode": 3, "timedOut": false}), - ])) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "computer", - "exec", - COMPUTER, - "--cwd", - "/tmp", - "--env", - "A=1", - "--timeout", - "30", - "--", - "sh", - "-c", - "echo hi; exit 3", - ], - &[], - ) - .await; - - assert_eq!(stdout(&output), "hi\nand more"); - assert_eq!(output.status.code(), Some(3)); - assert_eq!(stderr(&output), ""); -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_uses_the_default_computer_and_maps_timeouts_to_124() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .and(body_partial_json( - json!({ "command": "sleep 999", "stream": true }), - )) - .respond_with(ndjson(&[ - json!({"event": "start"}), - json!({"event": "exit", "exitCode": -1, "timedOut": true}), - ])) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &["computer", "exec", "-c", "sleep 999"], - &[("STEEL_COMPUTER_ID", COMPUTER)], - ) - .await; - - assert_eq!(stdout(&output), ""); - assert_eq!(output.status.code(), Some(124)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_in_json_mode_collects_one_result() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .and(body_partial_json( - json!({ "argv": ["id"], "stream": false }), - )) - .respond_with(ResponseTemplate::new(200).set_body_raw( - r#" {"output":"uid=0(root)\n","exitCode":0,"timedOut":false,"truncated":false}"#, - "application/json", - )) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &["--json", "computer", "exec", COMPUTER, "--", "id"], - &[], - ) - .await; - - assert!(output.status.success(), "stderr: {}", stderr(&output)); - let parsed: serde_json::Value = serde_json::from_str(stdout(&output).trim()).unwrap(); - assert_eq!(parsed["success"], json!(true)); - assert_eq!(parsed["data"]["output"], json!("uid=0(root)\n")); - assert_eq!(parsed["data"]["exitCode"], json!(0)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_retries_while_the_computer_wakes() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .respond_with( - ResponseTemplate::new(503) - .insert_header("Retry-After", "1") - .set_body_json(json!({"error": "computer is not available"})), - ) - .up_to_n_times(1) - .expect(1) - .mount(&server) - .await; - Mock::given(method("POST")) - .and(path(exec_path())) - .respond_with(ndjson(&[ - json!({"event": "start"}), - json!({"event": "output", "data": "awake\n"}), - json!({"event": "exit", "exitCode": 0, "timedOut": false}), - ])) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "exec", COMPUTER, "--", "true"], &[]).await; - - assert_eq!(stdout(&output), "awake\n"); - assert!(output.status.success()); - assert!( - stderr(&output).contains("not ready yet"), - "stderr: {}", - stderr(&output) - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_reports_an_unknown_computer() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .respond_with( - ResponseTemplate::new(404).set_body_json(json!({"error": "computer not found"})), - ) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "exec", COMPUTER, "--", "true"], &[]).await; - - assert_eq!(output.status.code(), Some(5)); - assert!( - stderr(&output).contains("computer not found"), - "stderr: {}", - stderr(&output) - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn exec_fails_when_the_stream_ends_early() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path(exec_path())) - .respond_with(ndjson(&[ - json!({"event": "start"}), - json!({"event": "output", "data": "partial"}), - ])) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "exec", COMPUTER, "--", "true"], &[]).await; - - assert_eq!(stdout(&output), "partial"); - assert!(!output.status.success()); - assert!( - stderr(&output).contains("before the command finished"), - "stderr: {}", - stderr(&output) - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn list_prints_a_table_and_create_remembers_the_default() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/computers")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "computers": [{ - "id": COMPUTER, "status": "running", "region": "us-east", "template": "steel", - "vcpu": 2, "memoryMib": 2048, "diskMib": 25600, "timeoutSeconds": 3600, - "autoPause": false, "checkpointId": null, "statusChangedAt": "2026-09-10T00:00:00Z" - }] - }))) - .mount(&server) - .await; - Mock::given(method("POST")) - .and(path("/v1/computers")) - .and(body_partial_json(json!({ "template": "steel", "vcpu": 4 }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "id": COMPUTER, "status": "creating", "region": null, "template": "steel", - "vcpu": 4, "memoryMib": 2048, "diskMib": 25600, "timeoutSeconds": 3600, - "autoPause": false, "checkpointId": null, "statusChangedAt": "2026-09-10T00:00:00Z" - }))) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "list"], &[]).await; - let text = stdout(&output); - assert!(output.status.success(), "stderr: {}", stderr(&output)); - assert!(text.starts_with("ID"), "table header missing: {text}"); - assert!( - text.contains(COMPUTER) && text.contains("running"), - "{text}" - ); - - let output = run_steel( - &server, - &[ - "computer", - "create", - "--template", - "steel", - "--vcpu", - "4", - "--use", - ], - &[], - ) - .await; - assert!(output.status.success(), "stderr: {}", stderr(&output)); - assert!( - stdout(&output).contains("is creating"), - "{}", - stdout(&output) - ); - assert!( - stdout(&output).contains("default computer"), - "{}", - stdout(&output) - ); -} diff --git a/tests/computer_lifecycle.rs b/tests/computer_lifecycle.rs deleted file mode 100644 index 57dbf88..0000000 --- a/tests/computer_lifecycle.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! End-to-end tests for `steel computer stop|start|restart` against a fake API host. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -const ID: &str = "cmp_00x1492gqevd1nvs7vya5py3d0m33"; - -async fn mount(server: &MockServer, verb: &str, status: &str) { - Mock::given(method("POST")) - .and(path(format!("/v1/computers/{ID}/{verb}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "id": ID, - "status": status - }))) - .expect(1) - .mount(server) - .await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn stop_reports_the_settled_status() { - let server = MockServer::start().await; - mount(&server, "stop", "stopped").await; - - let output = run_steel(&server, &["computer", "stop", ID]).await; - - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(text.contains(&format!("{ID} is stopped."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn start_reports_the_settled_status() { - let server = MockServer::start().await; - mount(&server, "start", "running").await; - - let output = run_steel(&server, &["computer", "start", ID]).await; - - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(text.contains(&format!("{ID} is running."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn restart_calls_restart_and_not_start() { - let server = MockServer::start().await; - mount(&server, "restart", "running").await; - Mock::given(method("POST")) - .and(path(format!("/v1/computers/{ID}/start"))) - .respond_with(ResponseTemplate::new(500)) - .expect(0) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "restart", ID]).await; - - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(text.contains(&format!("{ID} is running."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn create_sends_the_idle_timeout_and_env() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/computers")) - .and(wiremock::matchers::body_partial_json(json!({ - "idleTimeoutSeconds": 120, - "env": { "A": "1", "TOKEN": "x=y" } - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "id": ID, - "status": "running" - }))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "computer", - "create", - "--idle-timeout", - "120", - "--env", - "A=1", - "--env", - "TOKEN=x=y", - ], - ) - .await; - - assert!(output.status.success()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn create_refuses_an_env_without_an_equals() { - let server = MockServer::start().await; - - let output = run_steel(&server, &["computer", "create", "--env", "NOPE"]).await; - - assert!(!output.status.success()); - let text = String::from_utf8_lossy(&output.stderr).to_string(); - assert!(text.contains("KEY=VALUE")); -} diff --git a/tests/computer_quota.rs b/tests/computer_quota.rs deleted file mode 100644 index 21fd971..0000000 --- a/tests/computer_quota.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! End-to-end test for `steel computer quota` against a fake API host. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -#[tokio::test(flavor = "multi_thread")] -async fn quota_prints_usage_against_each_limit() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/computers/quota")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "computerCount": 2, - "computerLimit": 5, - "runningCount": 1, - "runningLimit": 3, - "checkpointCount": 0, - "checkpointLimit": 2 - }))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["computer", "quota"]).await; - - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(text.contains("Computers 2/5")); - assert!(text.contains("Running 1/3")); - assert!(text.contains("Checkpoints 0/2")); -} diff --git a/tests/computer_ssh.rs b/tests/computer_ssh.rs deleted file mode 100644 index 942c553..0000000 --- a/tests/computer_ssh.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! End-to-end tests for `steel computer ssh` against an in-process bridge. -//! -//! A WebSocket server plays the box gateway and an SSH server sits behind it, -//! so the real `steel` binary exercises the handshake headers, the WebSocket -//! transport, the SSH login, exec, shell and the exit status. - -use std::process::{Command, Output}; -use std::sync::{Arc, Mutex}; - -use russh::keys::PrivateKey; -use russh::keys::ssh_key::private::{Ed25519Keypair, KeypairData}; -use russh::server::{self, Auth, ChannelOpenHandle, Msg, Session}; -use russh::{Channel, ChannelId}; -use steel_cli::commands::computer::wsio::WsIo; -use tokio::net::TcpListener; -use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response}; -use tokio_tungstenite::tungstenite::http::HeaderValue; - -const COMPUTER: &str = "cmp_0000123456789abcdefghjkmnpqrs"; -const SUBPROTOCOL: &str = "steel-ssh-v1"; - -#[derive(Default, Debug, Clone)] -struct Observed { - path: String, - subprotocol: String, - api_key: String, - user: String, - password: String, - exec: Option, - shell: bool, -} - -type Shared = Arc>; - -struct Bridge { - observed: Shared, - exit_status: u32, -} - -impl server::Handler for Bridge { - type Error = russh::Error; - - async fn auth_password(&mut self, user: &str, password: &str) -> Result { - { - let mut observed = self.observed.lock().unwrap(); - observed.user = user.to_string(); - observed.password = password.to_string(); - } - Ok(Auth::Accept) - } - - async fn channel_open_session( - &mut self, - _channel: Channel, - reply: ChannelOpenHandle, - _session: &mut Session, - ) -> Result<(), Self::Error> { - reply.accept().await; - Ok(()) - } - - async fn exec_request( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> Result<(), Self::Error> { - let command = String::from_utf8_lossy(data).to_string(); - self.observed.lock().unwrap().exec = Some(command.clone()); - session.channel_success(channel)?; - session.data(channel, format!("ran {command}\n").into_bytes())?; - finish(session, channel, self.exit_status) - } - - async fn shell_request( - &mut self, - channel: ChannelId, - session: &mut Session, - ) -> Result<(), Self::Error> { - self.observed.lock().unwrap().shell = true; - session.channel_success(channel)?; - session.data(channel, b"shell ready\n".to_vec())?; - finish(session, channel, self.exit_status) - } -} - -fn finish(session: &mut Session, channel: ChannelId, exit_status: u32) -> Result<(), russh::Error> { - session.exit_status_request(channel, exit_status)?; - session.eof(channel)?; - session.close(channel)?; - Ok(()) -} - -async fn start_bridge(exit_status: u32) -> (u16, Shared) { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = listener.local_addr().unwrap().port(); - let observed: Shared = Arc::default(); - let shared = observed.clone(); - tokio::spawn(async move { - let (tcp, _) = listener.accept().await.unwrap(); - let handshake = shared.clone(); - let callback = - move |request: &Request, mut response: Response| -> Result { - let header = |name: &str| { - request - .headers() - .get(name) - .and_then(|value| value.to_str().ok()) - .unwrap_or("") - .to_string() - }; - { - let mut observed = handshake.lock().unwrap(); - observed.path = request.uri().path().to_string(); - observed.subprotocol = header("Sec-WebSocket-Protocol"); - observed.api_key = header("Steel-Api-Key"); - } - response.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_static(SUBPROTOCOL), - ); - Ok(response) - }; - let ws = tokio_tungstenite::accept_hdr_async(tcp, callback) - .await - .unwrap(); - let host_key = KeypairData::Ed25519(Ed25519Keypair::from_seed(&[7u8; 32])); - let config = Arc::new(server::Config { - keys: vec![PrivateKey::try_from(host_key).unwrap()], - ..Default::default() - }); - let handler = Bridge { - observed: shared, - exit_status, - }; - let session = server::run_stream(config, WsIo::new(ws), handler) - .await - .unwrap(); - let _ = session.await; - }); - (port, observed) -} - -async fn run_steel(port: u16, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("http://127.0.0.1:{port}/v1")); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env_remove("STEEL_COMPUTER_ID"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn text(bytes: &[u8]) -> String { - String::from_utf8_lossy(bytes).to_string() -} - -#[tokio::test(flavor = "multi_thread")] -async fn ssh_runs_a_command_through_the_bridge_and_returns_its_exit_status() { - let (port, observed) = start_bridge(7).await; - - let output = run_steel( - port, - &["computer", "ssh", COMPUTER, "--", "echo", "hi there"], - ) - .await; - - assert_eq!( - text(&output.stdout), - "ran 'echo' 'hi there'\n", - "stderr: {}", - text(&output.stderr) - ); - assert_eq!(output.status.code(), Some(7)); - let observed = observed.lock().unwrap().clone(); - assert_eq!(observed.path, format!("/v1/computers/{COMPUTER}/ssh")); - assert_eq!(observed.subprotocol, SUBPROTOCOL); - assert_eq!(observed.api_key, "ste-test-key"); - assert_eq!(observed.user, "root"); - assert_eq!(observed.password, ""); - assert_eq!(observed.exec.as_deref(), Some("'echo' 'hi there'")); - assert!(!observed.shell); -} - -#[tokio::test(flavor = "multi_thread")] -async fn ssh_opens_a_shell_when_no_command_is_given() { - let (port, observed) = start_bridge(0).await; - - let output = run_steel(port, &["computer", "ssh", COMPUTER]).await; - - assert_eq!( - text(&output.stdout), - "shell ready\n", - "stderr: {}", - text(&output.stderr) - ); - assert!(output.status.success()); - let observed = observed.lock().unwrap().clone(); - assert!(observed.shell); - assert!(observed.exec.is_none()); -} diff --git a/tests/environment_commands.rs b/tests/environment_commands.rs deleted file mode 100644 index 8333ff7..0000000 --- a/tests/environment_commands.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! End-to-end tests for `steel environment` against a fake API host. - -use std::process::{Command, Output}; - -use serde_json::json; -use wiremock::matchers::{body_json, method, path, query_param}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -const ENVIRONMENT: &str = "9b2d1c5e-1b2a-4c3d-8e4f-5a6b7c8d9e0f"; -const SECRET: &str = "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31"; -const PROJECT: &str = "0c1d2e3f-4a5b-4c6d-8e7f-8091a2b3c4d5"; - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", tmp.path()); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.arg("--no-update-check"); - cmd.args(args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn stdout(output: &Output) -> String { - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn stderr(output: &Output) -> String { - String::from_utf8_lossy(&output.stderr).to_string() -} - -fn environment() -> serde_json::Value { - json!({ - "id": ENVIRONMENT, - "name": "dev", - "version": 1, - "spec": { "template": "steel", "vcpu": 2, "env": { "A": "1" } }, - "secrets": { "TOKEN": SECRET, "OLD": SECRET }, - "networkSecrets": [], - "createdAt": "2026-09-12T00:00:00Z", - "updatedAt": "2026-09-12T01:00:00Z" - }) -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_list_prints_a_table() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/environments")) - .and(query_param("projectId", PROJECT)) - .respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "environments": [environment()] })), - ) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["environment", "list", "--project", PROJECT]).await; - - assert!(output.status.success(), "{}", stderr(&output)); - let text = stdout(&output); - assert!(text.contains("TEMPLATE")); - assert!(text.contains(ENVIRONMENT)); - assert!(text.contains("dev")); - assert!(text.contains("steel")); - assert!(text.contains('2')); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_create_sends_the_spec_and_secrets() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/environments")) - .and(body_json(json!({ - "name": "dev", - "projectId": PROJECT, - "spec": { "template": "steel", "vcpu": 2, "autoPause": true, "env": { "A": "1" } }, - "secrets": { "TOKEN": { "secretId": SECRET }, "KEY": { "value": "abc" } } - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(environment())) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "environment", - "create", - "dev", - "--template", - "steel", - "--vcpu", - "2", - "--auto-pause", - "--env", - "A=1", - "--secret", - &format!("TOKEN={SECRET}"), - "--secret-value", - "KEY=abc", - "--project", - PROJECT, - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Created dev as {ENVIRONMENT}."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_create_refuses_plain_values_for_secret() { - let server = MockServer::start().await; - - let output = run_steel( - &server, - &[ - "environment", - "create", - "dev", - "--secret", - "TOKEN=sk-live-abc", - ], - ) - .await; - - assert!(!output.status.success()); - assert!(stderr(&output).contains("steel secret create")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_update_merges_the_spec_and_unsets_secrets() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(environment())) - .expect(1) - .mount(&server) - .await; - Mock::given(method("PATCH")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .and(body_json(json!({ - "name": "staging", - "spec": { "template": "steel", "vcpu": 2, "memoryMib": 4096, "env": { "A": "1", "B": "2" } }, - "secrets": { "OLD": null } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(environment())) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "environment", - "update", - ENVIRONMENT, - "--name", - "staging", - "--memory", - "4096", - "--env", - "B=2", - "--unset-secret", - "OLD", - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Updated {ENVIRONMENT}."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_update_skips_the_fetch_without_spec_changes() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .respond_with(ResponseTemplate::new(500)) - .expect(0) - .mount(&server) - .await; - Mock::given(method("PATCH")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .and(body_json( - json!({ "secrets": { "TOKEN": { "secretId": SECRET } } }), - )) - .respond_with(ResponseTemplate::new(200).set_body_json(environment())) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "environment", - "update", - ENVIRONMENT, - "--secret", - &format!("TOKEN={SECRET}"), - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_update_needs_a_change() { - let server = MockServer::start().await; - - let output = run_steel(&server, &["environment", "update", ENVIRONMENT]).await; - - assert!(!output.status.success()); - assert!(stderr(&output).contains("--unset-secret")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn environment_get_and_delete_use_the_id() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(environment())) - .expect(1) - .mount(&server) - .await; - Mock::given(method("DELETE")) - .and(path(format!("/v1/environments/{ENVIRONMENT}"))) - .and(query_param("projectId", PROJECT)) - .respond_with(ResponseTemplate::new(204)) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["--json", "environment", "get", ENVIRONMENT]).await; - assert!(output.status.success(), "{}", stderr(&output)); - let parsed: serde_json::Value = serde_json::from_str(&stdout(&output)).expect("json"); - assert_eq!(parsed["data"]["name"], "dev"); - - let output = run_steel( - &server, - &["environment", "delete", ENVIRONMENT, "--project", PROJECT], - ) - .await; - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Deleted {ENVIRONMENT}."))); -} diff --git a/tests/secret_commands.rs b/tests/secret_commands.rs deleted file mode 100644 index 7021e1e..0000000 --- a/tests/secret_commands.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! End-to-end tests for `steel secret` against a fake API host. - -use std::io::Write; -use std::process::{Command, Output, Stdio}; - -use serde_json::json; -use wiremock::matchers::{body_json, header, method, path, query_param}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -const SECRET: &str = "2f1b6f2e-2c6f-4d9a-9c2a-8f4c1e0d7b31"; -const PROJECT: &str = "0c1d2e3f-4a5b-4c6d-8e7f-8091a2b3c4d5"; - -fn command(server: &MockServer, dir: &std::path::Path, args: &[&str]) -> Command { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_steel")); - cmd.env("STEEL_CONFIG_DIR", dir); - cmd.env("STEEL_API_URL", format!("{}/v1", server.uri())); - cmd.env("STEEL_API_KEY", "ste-test-key"); - cmd.env("STEEL_TELEMETRY_DISABLED", "1"); - cmd.env("STEEL_FORCE_TTY", "1"); - cmd.arg("--no-update-check"); - cmd.args(args); - cmd -} - -async fn run_steel(server: &MockServer, args: &[&str]) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = command(server, tmp.path(), args); - tokio::task::spawn_blocking(move || { - let output = cmd.output().expect("failed to execute steel binary"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -async fn run_steel_with_stdin(server: &MockServer, args: &[&str], stdin: &str) -> Output { - let tmp = tempfile::tempdir().expect("temp dir"); - let mut cmd = command(server, tmp.path(), args); - cmd.stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let input = stdin.to_string(); - tokio::task::spawn_blocking(move || { - let mut child = cmd.spawn().expect("failed to execute steel binary"); - child - .stdin - .take() - .expect("stdin") - .write_all(input.as_bytes()) - .expect("write stdin"); - let output = child.wait_with_output().expect("steel process"); - drop(tmp); - output - }) - .await - .expect("steel process") -} - -fn stdout(output: &Output) -> String { - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn stderr(output: &Output) -> String { - String::from_utf8_lossy(&output.stderr).to_string() -} - -fn secret(name: &str, version: u64) -> serde_json::Value { - json!({ - "id": SECRET, - "name": name, - "source": "steel", - "version": version, - "createdAt": "2026-09-12T00:00:00Z", - "updatedAt": "2026-09-12T01:00:00Z" - }) -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_list_prints_a_table() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/secrets")) - .and(header("steel-api-key", "ste-test-key")) - .respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "secrets": [secret("TOKEN", 3)] })), - ) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["secret", "list"]).await; - - assert!(output.status.success()); - let text = stdout(&output); - assert!(text.contains("ID")); - assert!(text.contains("VERSION")); - assert!(text.contains(SECRET)); - assert!(text.contains("TOKEN")); - assert!(text.contains('3')); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_list_scopes_to_a_project() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/v1/secrets")) - .and(query_param("projectId", PROJECT)) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "secrets": [] }))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["secret", "list", "--project", PROJECT]).await; - - assert!(output.status.success()); - assert!(stdout(&output).contains("No secrets.")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_create_sends_the_value_and_project() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/secrets")) - .and(body_json(json!({ - "name": "TOKEN", - "value": "sk-live-abc", - "projectId": PROJECT - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(secret("TOKEN", 1))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "secret", - "create", - "TOKEN", - "--value", - "sk-live-abc", - "--project", - PROJECT, - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Created TOKEN as {SECRET}."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_create_reads_the_value_from_stdin() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/secrets")) - .and(body_json(json!({ "name": "TOKEN", "value": "from stdin" }))) - .respond_with(ResponseTemplate::new(201).set_body_json(secret("TOKEN", 1))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel_with_stdin( - &server, - &["secret", "create", "TOKEN", "--value-stdin"], - "from stdin\n", - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_create_needs_a_value() { - let server = MockServer::start().await; - - let output = run_steel(&server, &["secret", "create", "TOKEN"]).await; - - assert!(!output.status.success()); - assert!(stderr(&output).contains("--value")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_get_prints_the_metadata() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path(format!("/v1/secrets/{SECRET}"))) - .respond_with(ResponseTemplate::new(200).set_body_json(secret("TOKEN", 2))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["--json", "secret", "get", SECRET]).await; - - assert!(output.status.success()); - let parsed: serde_json::Value = serde_json::from_str(&stdout(&output)).expect("json"); - assert_eq!(parsed["data"]["id"], SECRET); - assert_eq!(parsed["data"]["version"], 2); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_update_sends_only_the_given_fields() { - let server = MockServer::start().await; - Mock::given(method("PATCH")) - .and(path(format!("/v1/secrets/{SECRET}"))) - .and(query_param("projectId", PROJECT)) - .and(body_json(json!({ "value": "rotated" }))) - .respond_with(ResponseTemplate::new(200).set_body_json(secret("TOKEN", 2))) - .expect(1) - .mount(&server) - .await; - - let output = run_steel( - &server, - &[ - "secret", - "update", - SECRET, - "--value", - "rotated", - "--project", - PROJECT, - ], - ) - .await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Updated {SECRET}."))); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_update_needs_a_change() { - let server = MockServer::start().await; - - let output = run_steel(&server, &["secret", "update", SECRET]).await; - - assert!(!output.status.success()); - assert!(stderr(&output).contains("--name")); -} - -#[tokio::test(flavor = "multi_thread")] -async fn secret_delete_reports_the_id() { - let server = MockServer::start().await; - Mock::given(method("DELETE")) - .and(path(format!("/v1/secrets/{SECRET}"))) - .respond_with(ResponseTemplate::new(204)) - .expect(1) - .mount(&server) - .await; - - let output = run_steel(&server, &["secret", "delete", SECRET]).await; - - assert!(output.status.success(), "{}", stderr(&output)); - assert!(stdout(&output).contains(&format!("Deleted {SECRET}."))); -}