From 6e469d1b3b0a32ac29a6e6b55ac1a2f51752175d Mon Sep 17 00:00:00 2001 From: Duy Do Date: Tue, 22 Oct 2024 10:43:03 +0700 Subject: [PATCH] CLI (#145) --- .gitignore | 2 +- Cargo.lock | 40 +- Cargo.toml | 3 +- config.toml => admin.toml | 0 crates/cmds-std/Cargo.toml | 2 +- crates/command-rpc/src/server.rs | 3 +- .../pdg-common/src/nft_metadata/generate.rs | 5 +- lib/client/src/types.rs | 2 - lib/flow-lib/src/command/builder.rs | 2 +- lib/flow-lib/src/command/mod.rs | 5 +- lib/flow-lib/src/config/node.rs | 2 + lib/space-operator-cli/Cargo.lock | 2965 +++++++++++++++++ lib/space-operator-cli/Cargo.toml | 47 + lib/space-operator-cli/README.md | 436 +++ lib/space-operator-cli/src/main.rs | 1834 ++++++++++ lib/space-operator-cli/src/schema.rs | 151 + 16 files changed, 5466 insertions(+), 33 deletions(-) rename config.toml => admin.toml (100%) create mode 100644 lib/space-operator-cli/Cargo.lock create mode 100644 lib/space-operator-cli/Cargo.toml create mode 100644 lib/space-operator-cli/README.md create mode 100644 lib/space-operator-cli/src/main.rs create mode 100644 lib/space-operator-cli/src/schema.rs diff --git a/.gitignore b/.gitignore index 8ae12cf2..7cc6d658 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ target .vscode _data guest_local_storage -crates/flow-server/local_storage/* +local_storage config.toml .env diff --git a/Cargo.lock b/Cargo.lock index 7b35c404..44a8c203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,7 +1231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" dependencies = [ "borsh-derive 0.10.4", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -1665,18 +1665,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstyle", "clap_lex 0.7.2", @@ -1827,11 +1827,11 @@ dependencies = [ "hyper 0.14.30", "mime_guess", "once_cell", - "postgrest", "reqwest 0.12.8", "rust_decimal", "serde", "serde_json", + "spo-postgrest", "thiserror", "tokio", "tracing", @@ -2167,7 +2167,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.19", + "clap 4.5.18", "criterion-plot", "is-terminal", "itertools", @@ -4278,7 +4278,7 @@ dependencies = [ "solana-program 1.16.27", "spl-associated-token-account 2.2.0", "spl-token 4.0.0", - "spl-token-2022 0.6.1", + "spl-token-2022 0.9.0", ] [[package]] @@ -4355,7 +4355,7 @@ dependencies = [ "solana-program 1.16.27", "spl-associated-token-account 2.2.0", "spl-token 4.0.0", - "spl-token-2022 0.6.1", + "spl-token-2022 0.9.0", ] [[package]] @@ -4680,7 +4680,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2 1.0.86", "quote 1.0.37", "syn 2.0.79", @@ -5113,16 +5113,6 @@ dependencies = [ "uuid 1.10.0", ] -[[package]] -name = "postgrest" -version = "1.6.0" -source = "git+https://github.com/space-operator/postgrest-rs?branch=dev#e2a2e607be3b5e4802fdac4a7ef1920f8e571805" -dependencies = [ - "bytes", - "reqwest 0.12.8", - "serde", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -7873,6 +7863,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "spo-postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2f6cfdcd676f2b74c661287ff37df9e947f14259f2eed9519fc43fd4faa8c" +dependencies = [ + "reqwest 0.12.8", + "serde", +] + [[package]] name = "srpc" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 9773cfd4..7d074859 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["lib/*", "crates/*"] -exclude = ["crates/space-wasm/tests"] +exclude = ["crates/space-wasm/tests", "lib/space-operator-cli"] resolver = "2" [patch.crates-io] @@ -44,3 +44,4 @@ serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } anyhow = "1" bs58 = "0.4" +postgrest = { package = "spo-postgrest", version = "1.6.0" } diff --git a/config.toml b/admin.toml similarity index 100% rename from config.toml rename to admin.toml diff --git a/crates/cmds-std/Cargo.toml b/crates/cmds-std/Cargo.toml index 83c704d8..fab8a660 100644 --- a/crates/cmds-std/Cargo.toml +++ b/crates/cmds-std/Cargo.toml @@ -23,7 +23,7 @@ rust_decimal = { version = "1.32.0", features = ["serde-with-float"] } tracing = "0.1.40" bytes = "1.5.0" mime_guess = "2.0.4" -postgrest = { git = "https://github.com/space-operator/postgrest-rs", branch = "dev" } +postgrest = { workspace = true } tokio = "1.33.0" once_cell = "1.17" url = { version = "2.5.0", features = ["serde"] } diff --git a/crates/command-rpc/src/server.rs b/crates/command-rpc/src/server.rs index beb63560..fadde374 100644 --- a/crates/command-rpc/src/server.rs +++ b/crates/command-rpc/src/server.rs @@ -27,13 +27,12 @@ pub enum Error { pub type WsStream = tokio_tungstenite::WebSocketStream>; +#[allow(dead_code)] pub struct CommandHost { natives: BTreeMap, CommandDescription>, stream: WsStream, } -async fn send() {} - impl CommandHost { pub async fn connect(url: &str) -> Result { let (stream, _) = tokio_tungstenite::connect_async(url).await?; diff --git a/crates/pdg-common/src/nft_metadata/generate.rs b/crates/pdg-common/src/nft_metadata/generate.rs index b0a02be9..4412721c 100644 --- a/crates/pdg-common/src/nft_metadata/generate.rs +++ b/crates/pdg-common/src/nft_metadata/generate.rs @@ -480,10 +480,7 @@ impl RenderParams { #[cfg(test)] mod tests { - use std::{ - collections::{HashMap, HashSet}, - vec, - }; + use std::collections::{HashMap, HashSet}; use serde_json::json; use strum::IntoEnumIterator; diff --git a/lib/client/src/types.rs b/lib/client/src/types.rs index bab76b80..8c7e96a2 100644 --- a/lib/client/src/types.rs +++ b/lib/client/src/types.rs @@ -11,14 +11,12 @@ use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] - pub enum RestResult { Error(ErrorBody), Success(T), } #[derive(Debug)] - pub struct True; impl Serialize for True { diff --git a/lib/flow-lib/src/command/builder.rs b/lib/flow-lib/src/command/builder.rs index bedad412..faddd89c 100644 --- a/lib/flow-lib/src/command/builder.rs +++ b/lib/flow-lib/src/command/builder.rs @@ -226,7 +226,7 @@ impl CmdBuilder { optional: x.optional, }) .collect(), - instruction_info: None, + instruction_info: self.def.data.instruction_info, permissions: self.def.permissions, }; diff --git a/lib/flow-lib/src/command/mod.rs b/lib/flow-lib/src/command/mod.rs index aeb3d9a5..64735267 100644 --- a/lib/flow-lib/src/command/mod.rs +++ b/lib/flow-lib/src/command/mod.rs @@ -29,14 +29,17 @@ pub mod prelude { }, config::{client::NodeData, node::Permissions}, context::Context, + solana::Instructions, CmdInputDescription as Input, CmdOutputDescription as Output, FlowId, Name, ValueSet, ValueType, }; pub use async_trait::async_trait; + pub use bytes::Bytes; pub use serde::{Deserialize, Serialize}; pub use serde_json::Value as JsonValue; + pub use solana_sdk::{pubkey::Pubkey, signature::Signature, signer::keypair::Keypair}; pub use thiserror::Error as ThisError; - pub use value::{self, Value}; + pub use value::{self, Decimal, Value}; } /// Error type of commmands. diff --git a/lib/flow-lib/src/config/node.rs b/lib/flow-lib/src/config/node.rs index e005738e..6553e8e3 100644 --- a/lib/flow-lib/src/config/node.rs +++ b/lib/flow-lib/src/config/node.rs @@ -1,4 +1,5 @@ //! Note: only add fields that are needed in backend. +use crate::command::InstructionInfo; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Clone)] @@ -20,6 +21,7 @@ pub struct Permissions { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Data { pub node_id: String, + pub instruction_info: Option, } #[derive(Deserialize, Serialize, Debug, Clone)] diff --git a/lib/space-operator-cli/Cargo.lock b/lib/space-operator-cli/Cargo.lock new file mode 100644 index 00000000..fab2d5e1 --- /dev/null +++ b/lib/space-operator-cli/Cargo.lock @@ -0,0 +1,2965 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap-markdown" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ebc67e6266e14f8b31541c2f204724fa2ac7ad5c17d6f5908fbb92a60f42cff" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_builder" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version", +] + +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gix" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048b8d1ae2104f045cb37e5c450fc49d5d8af22609386bfc739c11ba88995eb" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-actor" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665" +dependencies = [ + "bstr", + "gix-date", + "gix-utils", + "itoa", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-attributes" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebccbf25aa4a973dd352564a9000af69edca90623e8a16dad9cbc03713131311" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-chunk" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-command" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff2e692b36bbcf09286c70803006ca3fd56551a311de450be317a0ab8ea92e7" +dependencies = [ + "bstr", + "gix-path", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78" +dependencies = [ + "bstr", + "gix-chunk", + "gix-features", + "gix-hash", + "memmap2", + "thiserror", +] + +[[package]] +name = "gix-config" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "once_cell", + "smallvec", + "thiserror", + "unicode-bom", + "winnow", +] + +[[package]] +name = "gix-config-value" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c84b7af01e68daf7a6bb8bb909c1ff5edb3ce4326f1f43063a5a96d3c3c8a5" +dependencies = [ + "bstr", + "itoa", + "jiff", + "thiserror", +] + +[[package]] +name = "gix-diff" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c9afd80fff00f8b38b1c1928442feb4cd6d2232a6ed806b6b193151a3d336c" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "imara-diff", + "thiserror", +] + +[[package]] +name = "gix-dir" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed3a9076661359a1c5a27c12ad6c3ebe2dd96b8b3c0af6488ab7c128b7bdd98" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0577366b9567376bc26e815fd74451ebd0e6218814e242f8e5b7072c58d956d2" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-hash", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-features" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" +dependencies = [ + "crc32fast", + "flate2", + "gix-hash", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "prodash", + "sha1_smol", + "thiserror", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4121790ae140066e5b953becc72e7496278138d19239be2e63b5067b0843119e" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" +dependencies = [ + "fastrand", + "gix-features", + "gix-utils", +] + +[[package]] +name = "gix-glob" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" +dependencies = [ + "faster-hex", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" +dependencies = [ + "gix-hash", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e447cd96598460f5906a0f6c75e950a39f98c2705fc755ad2f2020c9e937fab7" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.14.5", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-object" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-odb" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3158068701c17df54f0ab2adda527f5a6aca38fd5fd80ceb7e3c0a2717ec747" +dependencies = [ + "arc-swap", + "gix-date", + "gix-features", + "gix-fs", + "gix-hash", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3223aa342eee21e1e0e403cad8ae9caf9edca55ef84c347738d10681676fd954" +dependencies = [ + "clru", + "gix-chunk", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9802304baa798dd6f5ff8008a2b6516d54b74a69ca2d3a2b9e2d6c3b5556b40" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfc4febd088abdcbc9f1246896e57e37b7a34f6909840045a1767c6dafac7af" +dependencies = [ + "bstr", + "gix-trace", + "home", + "once_cell", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d23bf239532b4414d0e63b8ab3a65481881f7237ed9647bb10c1e3cc54c5ceb" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + +[[package]] +name = "gix-quote" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff" +dependencies = [ + "bstr", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-ref" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-refspec" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb005f82341ba67615ffdd9f7742c87787544441c88090878393d0682869ca6" +dependencies = [ + "bstr", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4621b219ac0cdb9256883030c3d56a6c64a6deaa829a92da73b9a576825e1e" +dependencies = [ + "bstr", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", + "thiserror", +] + +[[package]] +name = "gix-revwalk" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41e72544b93084ee682ef3d5b31b1ba4d8fa27a017482900e5e044d5b1b3984" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-status" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70d35ba639f0c16a6e4cca81aa374a05f07b23fa36ee8beb72c100d98b4ffea" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror", +] + +[[package]] +name = "gix-submodule" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529d0af78cc2f372b3218f15eb1e3d1635a21c8937c12e2dd0b6fc80c2ca874b" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "once_cell", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b" + +[[package]] +name = "gix-traverse" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "home", + "thiserror", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" +dependencies = [ + "bstr", + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2badbb64e57b404593ee26b752c26991910fd0d81fe6f9a71c1a8309b6c86" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "gix-worktree" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c312ad76a3f2ba8e865b360d5cb3aa04660971d16dec6dd0ce717938d903149a" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "imara-diff" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" +dependencies = [ + "ahash", + "hashbrown 0.14.5", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jiff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prodash" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[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 = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "space-operator-cli" +version = "0.3.0" +dependencies = [ + "bon", + "cargo_metadata", + "chrono", + "clap", + "clap-markdown", + "console", + "directories", + "error-stack", + "futures", + "gix", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "reqwest", + "semver", + "serde", + "serde_json", + "similar", + "spo-postgrest", + "strum", + "syn", + "thiserror", + "tokio", + "toml", + "url", + "uuid", + "xshell", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spo-postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2f6cfdcd676f2b74c661287ff37df9e947f14259f2eed9519fc43fd4faa8c" +dependencies = [ + "reqwest", + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "xshell" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db0ab86eae739efd1b054a8d3d16041914030ac4e01cd1dca0cf252fd8b6437" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/lib/space-operator-cli/Cargo.toml b/lib/space-operator-cli/Cargo.toml new file mode 100644 index 00000000..a35af1e9 --- /dev/null +++ b/lib/space-operator-cli/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "space-operator-cli" +version = "0.3.0" +edition = "2021" +description = "CLI for Space Operator" +license = "AGPL-3.0-only" +repository = "https://github.com/space-operator/flow-backend" +homepage = "https://spaceoperator.com" +readme = "README.md" + +[workspace] + +[[bin]] +path = "src/main.rs" +name = "spo" + +[dependencies] +bon = "2.3.0" +cargo_metadata = "0.18.1" +chrono = { version = "0.4.38", features = ["serde"] } +clap = { version = "=4.5.18", features = ["derive"] } +console = "0.15.8" +directories = "5.0.1" +error-stack = "0.5.0" +futures = "0.3.30" +gix = { version = "0.66.0", default-features = false, features = ["status"] } +postgrest = { package = "spo-postgrest", version = "1.6.0" } +prettyplease = "0.2.22" +proc-macro2 = "1.0.87" +quote = { version = "1.0.37" } +regex = "1.11.0" +reqwest = { version = "0.12.8", features = ["json"] } +semver = "1.0.23" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = { version = "1.0.128", features = ["preserve_order"] } +similar = { version = "2.6.0", features = ["inline"] } +strum = { version = "0.26.3", features = ["derive"] } +syn = "2.0.79" +thiserror = "1.0.64" +tokio = { version = "1.40.0", features = ["macros", "fs"] } +toml = { version = "0.8.19", features = ["preserve_order"] } +url = { version = "2.5.2", features = ["serde"] } +uuid = { version = "1.10.0", features = ["serde"] } +xshell = "0.2.6" + +[dev-dependencies] +clap-markdown = "0.1.4" diff --git a/lib/space-operator-cli/README.md b/lib/space-operator-cli/README.md new file mode 100644 index 00000000..26abbf21 --- /dev/null +++ b/lib/space-operator-cli/README.md @@ -0,0 +1,436 @@ +# Space Operator CLI + +[![Crates.io][crates-badge]][crates-url] +[![AGPLv3 licensed][AGPLv3-badge]][AGPLv3-url] + +[crates-badge]: https://img.shields.io/crates/v/space-operator-cli.svg +[crates-url]: https://crates.io/crates/space-operator-cli +[AGPLv3-badge]: https://img.shields.io/badge/license-AGPLv3-blue.svg +[AGPLv3-url]: ../../LICENSE + +CLI for [Space Operator](https://spaceoperator.com). + +Table of contents: +- [Install](#install) +- [Login](#login) +- [Run flow-server](#run-flow-server) +- [Generate a new native node](#generate-a-new-native-node) +- [Generate input and output struct](#generate-input-and-output-struct) +- [Upload node](#upload-node) +- [Command-Line Help for spo](#command-line-help-for-spo) + +## Install + +Install using `cargo install`: + +```shell +cargo install space-operator-cli --force +``` + +Binary name: `spo` + +``` +$ spo --help +Usage: spo [OPTIONS] [COMMAND] + +Commands: + login Login to Space Operator using API key + start Start flow-server [aliases: s] + node Manage your nodes [aliases: n] + generate Generate various things [aliases: g] + help Print this message or the help of the given subcommand(s) + +Options: + --url URL of flow-server to use (default: https://dev-api.spaceoperator.com) + -h, --help Print help +``` + +## Login + +Run `spo login`: + +```bash +$ spo login +Go to https://spaceoperator.com/dashboard/profile/apikey go generate a key +Please paste your API key below +``` + +Enter your API key to login. + +## Run flow-server + +Clone [flow-backend](https://github.com/space-operator/flow-backend) repository: + +```bash +git clone https://github.com/space-operator/flow-backend +``` + +`cd` into `flow-backend` and run `spo start`: + +```bash +cd flow-backend +spo start +``` + +This will create a configuration file for flow-server then compile and run it +(first compilation will take several minutes). + +Example output: +``` +generated config.toml +$ cargo build --bin flow-server + Finished `dev` profile [optimized] target(s) in 0.60s +$ target/debug/flow-server config.toml +2024-10-19T14:19:36.514531Z INFO flow_server: native commands: ["add_config_lines", "add_config_lines_core", "add_required_signatory", "add_signatory", "arweave_file_upload", "arweave_nft_upload", "associated_token_account", "attest_from_eth", "attest_token", "burn_cNFT", "burn_v1", "cancel_proposal", "cast_vote", "collect", "complete_native", "complete_proposal", "complete_transfer_wrapped", "const", "create_core_collection_v2", "create_core_v2", "create_governance", "create_master_edition", "create_metadata_account", "create_mint_account", "create_native_treasury", "create_proposal", "create_realm", "create_streamflow_timelock", "create_token_account", "create_token_owner_record", "create_tree", "create_v1", "create_wrapped", "create_wrapped_on_eth", "das_api", "delegate_v1", "deposit_governing_tokens", "execute_transaction", "fetch_assets", "fileexplorer", "finalize_vote", "find_pda", "flow_input", "flow_output", "flow_run_info", "foreach", "gen_metaplex_attrs", "gen_pdg_attrs", "generate_base", "generate_keypair", "get_balance", "get_effect_list", "get_foreign_asset_eth", "get_vaa", "governance_post_message", "http_request", "initialize_candy_guard", "initialize_candy_machine", "initialize_candy_machine_core", "initialize_core_candy_guards", "initialize_record_with_seed", "initialize_token_bridge", "insert_transaction", "interflow", "interflow_instructions", "json_extract", "json_get_field", "json_insert", "kv_create_store", "kv_delete_store", "kv_read_item", "kv_write_item", "kvexplorer", "memo", "mint", "mint_cNFT_to_collection", "mint_candy_machine_core", "mint_compressed_NFT", "mint_token", "mint_v1", "mpl_core_update_plugin", "nft_complete_native", "nft_complete_wrapped", "nft_complete_wrapped_meta", "nft_transfer_native", "nft_transfer_wrapped", "note", "parse_pdg_attrs", "parse_vaa", "pdg_render", "post_message", "post_vaa", "postgrest_builder_eq", "postgrest_builder_insert", "postgrest_builder_is", "postgrest_builder_limit", "postgrest_builder_match", "postgrest_builder_neq", "postgrest_builder_not", "postgrest_builder_order", "postgrest_builder_select", "postgrest_builder_update", "postgrest_builder_upsert", "postgrest_execute_query", "postgrest_new_query", "postgrest_new_rpc", "print", "push_effect_list", "pyth_price", "range", "read_record", "redeem_nft_on_eth", "redeem_on_eth", "refund_proposal_deposit", "relinquish_token_owner_record_locks", "relinquish_vote", "remove_required_signatory", "remove_transaction", "request_airdrop", "revoke_governing_tokens", "set_authority", "set_authority_2022", "set_governance_config", "set_governance_delegate", "set_realm_authority", "set_realm_config", "set_token_owner_record_locks", "sign_off_proposal", "storage_create_signed_url", "storage_delete", "storage_download", "storage_get_file_metadata", "storage_get_public_url", "storage_list", "storage_upload", "supabase", "to_bytes", "to_string", "to_vec", "transfer_cNFT", "transfer_from_eth", "transfer_native", "transfer_nft_from_eth", "transfer_sol", "transfer_token", "transfer_wrapped", "update_cNFT", "update_core_v1", "update_render_params", "update_v1", "verify_collection_v1", "verify_creator_v1", "verify_signatures", "wait", "wallet", "withdraw_governing_tokens", "withdraw_streamflow_timelock", "wrap", "wrap_core", "write_to_record"] +2024-10-19T14:19:36.514586Z INFO flow_server: allow CORS origins: ["*"] +2024-10-19T14:19:36.528967Z INFO db::local_storage: openning sled storage: _data/guest_local_storage +2024-10-19T14:19:37.728815Z WARN flow_server: missing credentials, some routes are not available: need database credentials +2024-10-19T14:19:37.728846Z INFO flow_server: listening on "0.0.0.0" port 8080 +2024-10-19T14:19:37.729177Z INFO actix_server::builder: starting 8 workers +2024-10-19T14:19:37.729198Z INFO actix_server::server: Actix runtime found; starting in Actix runtime +2024-10-19T14:19:37.729207Z INFO actix_server::server: starting service: "actix-web-service-0.0.0.0:8080", workers: 8, listening on: 0.0.0.0:8080 +2024-10-19T14:19:37.759200Z DEBUG flow_server::db_worker::token_worker: started TokenWorker c334e245-75b4-49fd-93c0-c4b25ab74f70 +2024-10-19T14:19:37.759314Z INFO flow_server::db_worker: started DBWorker +``` + +## Generate a new native node + +Make sure you are inside [flow-backend](https://github.com/space-operator/flow-backend) repository. + +Generate with `spo node new`: +``` +$ spo node new +could not determine which package to update +use `-p` option to specify a package +available packages: + client + flow-lib + spo-helius + flow-value + space-lib + cmds-deno + command-rpc + srpc + cmds-pdg + pdg-common + cmds-solana + cmds-std + db + flow + rhai-script + space-wasm + utils + flow-server +``` + +Because our workspace have several packages, you must specify one of them to use +(our CLI can automatically choose one if you are inside one of them). + +``` +$ spo node new -p cmds-solana +``` + +Fill the prompts for node definition, for example: + +``` +$ spo node new -p cmds-solana +using package: cmds-solana +enter ? for help +? module path: ? +enter valid Rust module path to save the node (empty to save at root) +? module path: +? node id: transfer +? display name: Transfer +description: + +adding node inputs (enter empty name to finish) +? name: fee_payer +? input type: keypair +? optional (true/false): false +? passthrough (true/false): true + +adding node inputs (enter empty name to finish) +? name: amount +? input type: decimal +? optional (true/false): false +? passthrough (true/false): false + +adding node inputs (enter empty name to finish) +? name: + +adding node outputs (enter empty name to finish) +? name: balance +? output type: decimal +? optional (true/false): true + +adding node outputs (enter empty name to finish) +? name: +will this node emit Solana instructions? (y/n): y +adding `signature` output +adding `submit` input +adding instruction info: { + "before": [ + "balance", + "fee_payer" + ], + "signature": "signature", + "after": [] +} +writing node definition to crates/cmds-solana/node-definitions/transfer.json +writing code to crates/cmds-solana/src/transfer.rs +updating module crates/cmds-solana/src/lib.rs +upload node (y/n): y +node: transfer +command is not in database +upload? (y/n): y +inserted new node, id=1256 +view your node: +https://spaceoperator.com/dashboard/nodes/c334e245-75b4-49fd-93c0-c4b25ab74f70.transfer.0.1 +``` + +Generated Rust code: +```rust +use flow_lib::command::prelude::*; +const NAME: &str = "transfer"; +flow_lib::submit!(CommandDescription::new(NAME, | _ | build())); +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/transfer.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} +#[derive(Deserialize, Serialize, Debug)] +struct Input { + #[serde(with = "value::keypair")] + fee_payer: Keypair, + #[serde(with = "value::decimal")] + amount: Decimal, + #[serde(default = "value::default::bool_true")] + submit: bool, +} +#[derive(Deserialize, Serialize, Debug)] +struct Output { + #[serde(default, with = "value::decimal::opt")] + balance: Option, + #[serde(default, with = "value::signature::opt")] + signature: Option, +} +async fn run(mut ctx: Context, input: Input) -> Result { + tracing::info!("input: {:?}", input); + let signature = ctx + .execute(Instructions::default(), value::map! {}) + .await? + .signature; + Err(CommandError::msg("unimplemented")) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_build() { + build().unwrap(); + } + #[tokio::test] + async fn test_run() { + let ctx = Context::default(); + build().unwrap().run(ctx, ValueSet::new()).await.unwrap_err(); + } +} +``` + +Then, you can use `spo start` to run a local flow-server and test your node in flow editor. + +## Generate input and output struct + +If you updated the node definition manually, you can use `spo generate input` and +`spo generate output` to generate new type definitions. + +For example + +```shell +spo generate input crates/cmds-solana/node-definitions/nft/v1/mint_v1.json +``` + +```rust +#[derive(Deserialize, Serialize, Debug)] +struct Input { + #[serde(with = "value::keypair")] + fee_payer: Keypair, + #[serde(default, with = "value::keypair::opt")] + authority: Option, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + #[serde(with = "value::pubkey")] + token_owner: Pubkey, + amount: Option, + #[serde(default, with = "value::pubkey::opt")] + delegate_record: Option, + #[serde(default, with = "value::pubkey::opt")] + authorization_rules_program: Option, + #[serde(default, with = "value::pubkey::opt")] + authorization_rules: Option, + authorization_data: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} +``` + +## Upload node + +Use `spo node upload` to upload new node definition or update existing one. +We only support `native` node at the moment. + +# Command-Line Help for `spo` + +This document contains the help content for the `spo` command-line program. + +**Command Overview:** + +* [`spo`↴](#spo) +* [`spo login`↴](#spo-login) +* [`spo start`↴](#spo-start) +* [`spo node`↴](#spo-node) +* [`spo node new`↴](#spo-node-new) +* [`spo node upload`↴](#spo-node-upload) +* [`spo generate`↴](#spo-generate) +* [`spo generate input`↴](#spo-generate-input) +* [`spo generate output`↴](#spo-generate-output) +* [`spo generate config`↴](#spo-generate-config) + +## `spo` + +**Usage:** `spo [OPTIONS] [COMMAND]` + +###### **Subcommands:** + +* `login` — Login to Space Operator using API key +* `start` — Start flow-server +* `node` — Manage your nodes +* `generate` — Generate various things + +###### **Options:** + +* `--url ` — URL of flow-server to use (default: https://dev-api.spaceoperator.com) + + + +## `spo login` + +Login to Space Operator using API key + +**Usage:** `spo login` + + + +## `spo start` + +Start flow-server + +**Usage:** `spo start [CONFIG]` + +###### **Arguments:** + +* `` — Path to configuration file + + + +## `spo node` + +Manage your nodes + +**Usage:** `spo node ` + +###### **Subcommands:** + +* `new` — Generate a new node +* `upload` — Upload nodes + + + +## `spo node new` + +Generate a new node + +**Usage:** `spo node new [OPTIONS]` + +###### **Options:** + +* `--allow-dirty` — Allow dirty git repository +* `-p`, `--package ` — Specify which Rust package to add the new node to + + + +## `spo node upload` + +Upload nodes + +**Usage:** `spo node upload [OPTIONS] ` + +###### **Arguments:** + +* `` — Path to JSON node definition file + +###### **Options:** + +* `--dry-run` — Only print diff, don't do anything +* `--no-confirm` — Don't ask for confirmation + + + +## `spo generate` + +Generate various things + +**Usage:** `spo generate ` + +###### **Subcommands:** + +* `input` — Generate input struct +* `output` — Generate output struct +* `config` — Generate configuration file for flow-server + + + +## `spo generate input` + +Generate input struct + +**Usage:** `spo generate input ` + +###### **Arguments:** + +* `` — Path to node definition file + + + +## `spo generate output` + +Generate output struct + +**Usage:** `spo generate output ` + +###### **Arguments:** + +* `` — Path to node definition file + + + +## `spo generate config` + +Generate configuration file for flow-server + +**Usage:** `spo generate config [PATH]` + +###### **Arguments:** + +* `` — Path to save configuration file (default: config.toml) + + + +
+ + + This document was generated automatically by + clap-markdown. + diff --git a/lib/space-operator-cli/src/main.rs b/lib/space-operator-cli/src/main.rs new file mode 100644 index 00000000..d337757c --- /dev/null +++ b/lib/space-operator-cli/src/main.rs @@ -0,0 +1,1834 @@ +#![allow(clippy::print_stdout, clippy::print_stderr)] + +use cargo_metadata::{Metadata, Package, Target}; +use chrono::Utc; +use clap::{ColorChoice, CommandFactory, Parser, Subcommand, ValueEnum}; +use console::style; +use directories::ProjectDirs; +use error_stack::{Report, ResultExt}; +use futures::{io::AllowStdIo, AsyncBufReadExt, AsyncReadExt}; +use postgrest::Postgrest; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use regex::Regex; +use reqwest::{ + header::{HeaderName, HeaderValue, AUTHORIZATION}, + StatusCode, +}; +use schema::{CommandDefinition, CommandId, ValueType}; +use semver::Version; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + borrow::{Borrow, Cow}, + cmp::Ordering, + fmt::Display, + io::{BufReader, Stdin, Write}, + path::{Path, PathBuf}, + sync::LazyLock, +}; +use strum::IntoEnumIterator; +use thiserror::Error as ThisError; +use tokio::task::spawn_blocking; +use url::Url; +use uuid::Uuid; +use xshell::{cmd, Shell}; + +static CLIENT: LazyLock = LazyLock::new(|| reqwest::Client::new()); + +pub mod schema; + +pub mod claim_token { + use chrono::{DateTime, Utc}; + use uuid::Uuid; + + use super::*; + + #[derive(Deserialize, Serialize, Debug)] + pub struct Output { + pub user_id: Uuid, + pub access_token: String, + pub refresh_token: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub expires_at: DateTime, + } + + pub async fn claim_token( + http: &reqwest::Client, + flow_server: &Url, + apikey: &str, + ) -> Result> { + let apikey = HeaderValue::from_str(apikey).change_context(Error::InvalidApiKey)?; + let resp = http + .post( + flow_server + .join("/auth/claim_token") + .change_context(Error::Url)?, + ) + .header(HeaderName::from_static("x-api-key"), apikey) + .send() + .await + .change_context(Error::Http)?; + read_json_response::<_, FlowServerErrorBody>(resp).await + } +} + +pub mod get_info { + use url::Url; + + use super::*; + + #[derive(Deserialize, Serialize, Debug)] + pub struct Output { + pub supabase_url: Url, + pub anon_key: String, + } + + pub async fn get_info( + http: &reqwest::Client, + flow_server: &Url, + access_token: &str, + ) -> Result> { + let resp = http + .get(flow_server.join("/info").change_context(Error::Url)?) + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await + .change_context(Error::Http)?; + read_json_response::<_, FlowServerErrorBody>(resp).await + } +} + +async fn refresh( + http: &reqwest::Client, + info: &get_info::Output, + refresh_token: &str, + user_id: &Uuid, +) -> Result> { + #[derive(Serialize)] + struct Body<'a> { + refresh_token: &'a str, + } + + #[derive(Deserialize)] + struct Resp { + access_token: String, + expires_in: u32, + refresh_token: String, + } + + let resp = http + .post( + info.supabase_url + .join("/auth/v1/token?grant_type=refresh_token") + .change_context(Error::Url)?, + ) + .header(HeaderName::from_static("apikey"), &info.anon_key) + .json(&Body { refresh_token }) + .send() + .await + .change_context(Error::Http)?; + + let resp = read_json_response::(resp).await?; + + Ok(claim_token::Output { + user_id: *user_id, + access_token: resp.access_token, + refresh_token: resp.refresh_token, + expires_at: Utc::now() + chrono::Duration::seconds(resp.expires_in as i64), + }) +} + +fn get_color() -> ColorChoice { + std::env::var("COLOR") + .ok() + .and_then(|var| ColorChoice::from_str(&var, true).ok()) + .unwrap_or_default() +} + +#[derive(Parser, Debug)] +#[command(name = "spo")] +#[command(color = get_color())] +struct Args { + /// URL of flow-server to use (default: https://dev-api.spaceoperator.com) + #[arg(long)] + url: Option, + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Login to Space Operator using API key + Login {}, + /// Start flow-server + #[command(visible_alias = "s")] + Start { + /// Path to configuration file + config: Option, + }, + /// Manage your nodes + #[command(visible_alias = "n")] + Node { + #[command(subcommand)] + command: NodeCommands, + }, + /// Generate various things + #[command(visible_alias = "g")] + Generate { + #[command(subcommand)] + command: GenerateCommands, + }, +} + +#[derive(Subcommand, Debug)] +enum GenerateCommands { + /// Generate input struct + #[command(visible_alias = "i")] + Input { + /// Path to node definition file + path: PathBuf, + }, + /// Generate output struct + #[command(visible_alias = "o")] + Output { + /// Path to node definition file + path: PathBuf, + }, + /// Generate configuration file for flow-server + #[command(visible_alias = "c")] + Config { + /// Path to save configuration file (default: config.toml) + path: Option, + }, +} + +#[derive(Subcommand, Debug)] +enum NodeCommands { + /// Generate a new node + #[command(visible_alias = "n")] + New { + /// Allow dirty git repository + #[arg(long)] + allow_dirty: bool, + /// Specify which Rust package to add the new node to + #[arg(long, short)] + package: Option, + }, + /// Upload nodes + #[command(visible_alias = "u")] + Upload { + /// Path to JSON node definition file + path: PathBuf, + /// Only print diff, don't do anything + #[arg(long)] + dry_run: bool, + /// Don't ask for confirmation + #[arg(long)] + no_confirm: bool, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub flow_server: Url, + pub info: get_info::Output, + pub apikey: String, + pub jwt: claim_token::Output, +} + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("response from server: {}", .0)] + ErrorResponse(String), + #[error("{}: {}", .0, .1)] + UnknownResponse(StatusCode, String), + #[error("invalid API key")] + InvalidApiKey, + #[error("HTTP error")] + Http, + #[error("invalid semver string")] + Version, + #[error("no shell available")] + Shell, + #[error("an error occurred")] + Subprocess, + #[error("thread join error")] + Thread, + #[error("URL error")] + Url, + #[error("DB error")] + Postgrest, + #[error("could not find location to save application data")] + Dir, + #[error("failed to serialize application data")] + SerializeData, + #[error("failed to parse application data")] + ParseData, + #[error("failed to write application data {}", .0.display())] + WriteData(PathBuf), + #[error("failed to read application data {}", .0.display())] + ReadData(PathBuf), + #[error("failed to read file {}", .0.display())] + ReadFile(PathBuf), + #[error("failed to write file {}", .0.display())] + WriteFile(PathBuf), + #[error("failed to parse node definition")] + ParseNodeDefinition, + #[error("{}", .0)] + Unimplemented(&'static str), + #[error("you are not logged in")] + NotLogin, + #[error("JSON error")] + Json, + #[error("token refresh error")] + TokenRefresh, + #[error("git error: {}", .0)] + Gix(&'static str), + #[error("IO error: {}", .0)] + Io(&'static str), + #[error("failed to get cargo metadata")] + Metadata, + #[error("package not found: {}", .0)] + PackageNotFound(String), + #[error("package is not a library: {}", .0)] + NotLib(String), + #[error("invalid value")] + InvalidValue, +} + +#[derive(Deserialize, ThisError, Debug)] +#[error("{error}")] +pub struct FlowServerErrorBody { + pub error: String, +} + +#[derive(Deserialize, ThisError, Debug)] +#[error("{msg}")] +pub struct GoTrueErrorBody { + pub msg: String, +} + +#[derive(Deserialize, ThisError, Debug)] +#[serde(untagged)] +pub enum PostgrestErrorBody { + #[error("{error}")] + Error { error: String }, + #[error("{message}")] + Postgrest { + message: String, + details: Option, + hint: Option, + }, +} + +async fn read_json_response( + resp: reqwest::Response, +) -> Result> { + let code = resp.status(); + let bytes = resp.bytes().await.change_context(Error::Http)?; + if code.is_success() { + match serde_json::from_slice::(&bytes) { + Ok(body) => Ok(body), + Err(error) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + Err(Report::new(error).change_context(Error::UnknownResponse(code, text))) + } + } + } else { + match serde_json::from_slice::(&bytes) { + Ok(body) => Err(Error::ErrorResponse(body.to_string()).into()), + Err(_) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + Err(Error::UnknownResponse(code, text).into()) + } + } + } +} + +async fn read_file(path: impl AsRef) -> Result> { + let path = path.as_ref(); + if path == Path::new("-") { + let mut stdin = stdin(); + let mut result = String::new(); + stdin + .read_to_string(&mut result) + .await + .change_context(Error::Io("read stdin"))?; + Ok(result) + } else { + tokio::fs::read_to_string(path) + .await + .change_context_lazy(|| Error::ReadFile(path.to_owned())) + } +} + +async fn write_file(path: impl AsRef, data: impl AsRef<[u8]>) -> Result> { + let path = path.as_ref(); + if path == Path::new("-") { + let mut stdout = std::io::stdout(); + stdout + .write_all(data.as_ref()) + .change_context(Error::Io("write stdout"))?; + stdout.flush().change_context(Error::Io("write stdout"))?; + Ok(false) + } else { + tokio::fs::write(path, data) + .await + .change_context_lazy(|| Error::WriteFile(path.to_owned()))?; + Ok(true) + } +} + +pub struct ApiClient { + pg: postgrest::Postgrest, + config: Config, +} + +impl ApiClient { + pub fn from_config(config: Config) -> Result> { + let pg = Postgrest::new_with_client( + config + .info + .supabase_url + .join("/rest/v1") + .change_context(Error::Url)?, + CLIENT.clone(), + ) + .insert_header(HeaderName::from_static("apikey"), &config.info.anon_key); + Ok(Self { pg, config }) + } + + pub async fn load() -> Result> { + let path = Self::data_file_full_path()?; + let text = tokio::fs::read_to_string(&path) + .await + .change_context_lazy(|| Error::ReadData(path.clone()))?; + let config: Config = toml::from_str(&text).change_context(Error::ParseData)?; + Self::from_config(config) + } + + pub async fn new(flow_server: Url, apikey: String) -> Result> { + let token = claim_token::claim_token(&CLIENT, &flow_server, &apikey).await?; + let info = get_info::get_info(&CLIENT, &flow_server, &token.access_token).await?; + let config = Config { + flow_server, + info, + apikey, + jwt: token, + }; + Self::from_config(config) + } + + async fn get_access_token(&mut self) -> Result> { + let now = chrono::Utc::now(); + if now >= self.config.jwt.expires_at + chrono::Duration::minutes(1) { + self.config.jwt = refresh( + &CLIENT, + &self.config.info, + &self.config.jwt.refresh_token, + &self.config.jwt.user_id, + ) + .await + .change_context(Error::TokenRefresh)?; + } + Ok(self.config.jwt.access_token.clone()) + } + + pub async fn update_node( + &mut self, + id: CommandId, + def: &CommandDefinition, + ) -> Result<(CommandId, String), Report> { + #[derive(Serialize)] + struct UpdateNode<'a> { + #[serde(flatten)] + def: &'a CommandDefinition, + unique_node_id: &'a str, + name: &'a str, + } + + let unique_node_id = format!( + "{}.{}.{}", + self.config.jwt.user_id, def.data.node_id, def.data.version + ); + let body = serde_json::to_string_pretty(&UpdateNode { + def, + unique_node_id: &unique_node_id, + name: &def.data.node_id, + }) + .change_context(Error::Json)?; + + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .eq("id", id.to_string()) + .update(body) + .select("id") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Resp { + id: CommandId, + } + + let resp = read_json_response::(resp).await?; + + Ok((resp.id, unique_node_id)) + } + + pub async fn insert_node( + &mut self, + def: &CommandDefinition, + ) -> Result<(CommandId, String), Report> { + #[derive(Serialize)] + struct InsertNode<'a> { + #[serde(flatten)] + def: &'a CommandDefinition, + #[serde(rename = "isPublic")] + is_public: bool, + unique_node_id: &'a str, + name: &'a str, + } + + let unique_node_id = format!( + "{}.{}.{}", + self.config.jwt.user_id, def.data.node_id, def.data.version + ); + let body = serde_json::to_string(&InsertNode { + def, + is_public: false, + unique_node_id: &unique_node_id, + name: &def.data.node_id, + }) + .change_context(Error::Json)?; + + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .insert(body) + .select("id") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Resp { + id: CommandId, + } + + let resp = read_json_response::(resp).await?; + + Ok((resp.id, unique_node_id)) + } + + pub async fn get_my_native_node( + &mut self, + node_id: &str, + ) -> Result, Report> { + #[derive(Serialize)] + struct Query<'a> { + node_id: &'a str, + } + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .eq("user_id", self.config.jwt.user_id.to_string()) + .eq("type", "native") + .cs( + "data", + serde_json::to_string(&Query { node_id }).change_context(Error::Json)?, + ) + .select("*") + .execute() + .await + .change_context(Error::Postgrest)?; + let mut nodes = + read_json_response::, PostgrestErrorBody>(resp).await?; + error_stack::ensure!( + nodes.len() <= 1, + Error::ErrorResponse("more than 1 native nodes".to_owned()) + ); + + match nodes.pop() { + Some(json) => { + #[derive(Deserialize)] + struct Row { + id: CommandId, + #[serde(flatten)] + def: CommandDefinition, + } + + let row = serde_json::from_value::(json).change_context(Error::Json)?; + Ok(Some((row.id, row.def))) + } + None => Ok(None), + } + } + + pub fn data_dir() -> Result> { + Ok(project_dirs()?.data_dir().to_owned()) + } + + pub const fn data_file_name() -> &'static str { + "data.toml" + } + + pub fn data_file_full_path() -> Result> { + Ok(Self::data_dir()?.join(Self::data_file_name()).to_owned()) + } + + pub async fn save_application_data(&self) -> Result<(), Report> { + let base = Self::data_dir()?; + + tokio::fs::create_dir_all(&base) + .await + .change_context_lazy(|| Error::WriteData(base.clone()))?; + + let path = base.join(Self::data_file_name()); + + let data = toml::to_string_pretty(&self.config).change_context(Error::SerializeData)?; + tokio::fs::write(&path, data) + .await + .change_context_lazy(|| Error::WriteData(path.clone()))?; + Ok(()) + } + + pub async fn get_username(&mut self) -> Result, Report> { + let resp = self + .pg + .from("users_public") + .auth(self.get_access_token().await?) + .eq("user_id", self.config.jwt.user_id.to_string()) + .select("username") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Body { + username: Option, + } + + read_json_response::(resp) + .await + .map(|body| body.username) + } +} + +fn project_dirs() -> Result> { + Ok(ProjectDirs::from("com", "spaceoperator", "spo").ok_or(Error::Dir)?) +} + +async fn ask(q: &str) -> bool { + print!("{} (y/n): ", style(q).bold()); + std::io::stdout().flush().ok(); + + let mut stdin = AllowStdIo::new(BufReader::new(stdin())); + let mut answer = String::new(); + stdin.read_line(&mut answer).await.ok(); + + answer.trim().to_lowercase() == "y" +} + +struct Line(Option); + +impl std::fmt::Display for Line { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self.0 { + None => write!(f, " "), + Some(idx) => write!(f, "{:<4}", idx + 1), + } + } +} + +fn print_diff(local: &T, db: &T) -> bool { + use console::{style, Style}; + use similar::{ChangeTag, TextDiff}; + + let local_json = serde_json::to_string_pretty(local).unwrap(); + let db_json = serde_json::to_string_pretty(db).unwrap(); + + let diff = TextDiff::from_lines(db_json.as_str(), local_json.as_str()); + + if diff + .iter_all_changes() + .filter(|c| c.tag() != ChangeTag::Equal) + .count() + == 0 + { + println!("No differences"); + return false; + } + + for (idx, group) in diff.grouped_ops(3).iter().enumerate() { + if idx > 0 { + println!("{:-^1$}", "-", 80); + } + for op in group { + for change in diff.iter_inline_changes(op) { + let (sign, s) = match change.tag() { + ChangeTag::Delete => ("-", Style::new().red()), + ChangeTag::Insert => ("+", Style::new().green()), + ChangeTag::Equal => (" ", Style::new().dim()), + }; + print!( + "{}{} |{}", + style(Line(change.old_index())).dim(), + style(Line(change.new_index())).dim(), + s.apply_to(sign).bold(), + ); + for (emphasized, value) in change.iter_strings_lossy() { + if emphasized { + print!("{}", s.apply_to(value).underlined().on_black()); + } else { + print!("{}", s.apply_to(value)); + } + } + if change.missing_newline() { + println!(); + } + } + } + } + + true +} + +fn is_dirty() -> Result> { + let repo = + gix::ThreadSafeRepository::discover(".").change_context(Error::Gix("open repository"))?; + Ok(repo + .to_thread_local() + .is_dirty() + .change_context(Error::Gix("get status"))?) +} + +fn cargo_metadata() -> Result> { + cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .change_context(Error::Metadata) +} + +fn find_target_crate_by_name<'a>( + meta: &'a Metadata, + name: &str, +) -> Result<&'a Package, Report> { + let members = meta.workspace_packages(); + Ok(members + .into_iter() + .find(|p| p.name == name) + .ok_or_else(|| Error::PackageNotFound(name.to_owned()))?) +} + +fn find_target_crate<'a>(meta: &'a Metadata) -> Result, Report> { + let members = meta.workspace_packages(); + let pwd = std::env::current_dir().change_context(Error::Io("get current dir"))?; + let member = members.into_iter().find(|p| { + p.targets.iter().any(|t| t.is_lib()) + && p.manifest_path + .parent() + .map(|root| pwd.starts_with(root)) + .unwrap_or(false) + }); + Ok(member) +} + +async fn prompt_node_definition() -> Result> { + let mut stdin = stdin(); + + let name_regex = Regex::new(r#"^[[:alpha:]][[:word:]]*$"#).unwrap(); + let name_hint = "value can only contains characters [a-zA-Z0-9_] and must start with [a-zA-Z]"; + + let node_id = Prompt::builder() + .question("node id: ") + .check_regex(&name_regex) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + + let display_name = Prompt::builder() + .question("display name: ") + .check_regex(&Regex::new(r#"\S+"#).unwrap()) + .regex_hint("value cannot be empty") + .build() + .prompt(&mut stdin) + .await?; + + let description = Prompt::builder() + .question("description: ") + .build() + .prompt(&mut stdin) + .await?; + + let mut inputs = Vec::::new(); + loop { + println!("\nadding node inputs (enter empty name to finish)"); + + let name = Prompt::builder() + .question("name: ") + .check_regex(&name_regex) + .allow_empty(true) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + if name.is_empty() { + break; + } + + let types = schema::ValueType::iter() + .map(|t| t.into()) + .collect::>(); + let type_bound_str = Prompt::builder() + .question("input type: ") + .check_list(&types) + .build() + .prompt(&mut stdin) + .await?; + let type_bound: schema::ValueType = type_bound_str.parse().unwrap(); + + let optional = Prompt::builder() + .question("optional (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let optional: bool = optional.parse().unwrap(); + + let default_value = if optional && type_bound == ValueType::Bool { + let default = Prompt::builder() + .question("default value (empty/true/false): ") + .check_list(&["true", "false"]) + .allow_empty(true) + .build() + .prompt(&mut stdin) + .await?; + default + .parse::() + .ok() + .map(serde_json::Value::Bool) + .unwrap_or(serde_json::Value::Null) + } else { + serde_json::Value::Null + }; + + let passthrough = Prompt::builder() + .question("passthrough (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let passthrough: bool = passthrough.parse().unwrap(); + + inputs.push(schema::Target { + name, + type_bounds: [type_bound_str].into(), + required: !optional, + default_value, + passthrough, + tooltip: String::new(), + }); + } + + let mut outputs = Vec::::new(); + loop { + println!("\nadding node outputs (enter empty name to finish)"); + + let name = Prompt::builder() + .question("name: ") + .check_regex(&name_regex) + .allow_empty(true) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + if name.is_empty() { + break; + } + + let types = schema::ValueType::iter() + .map(|t| t.into()) + .collect::>(); + let type_bound_str = Prompt::builder() + .question("output type: ") + .check_list(&types) + .build() + .prompt(&mut stdin) + .await?; + let _: schema::ValueType = type_bound_str.parse().unwrap(); + + let optional = Prompt::builder() + .question("optional (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let optional: bool = optional.parse().unwrap(); + + outputs.push(schema::Source { + name, + r#type: type_bound_str, + tooltip: String::new(), + optional, + default_value: serde_json::Value::Null, + }); + } + + let ins = ask("will this node emit Solana instructions?").await; + let info = if ins { + if !outputs.iter().any(|o| o.name == "signature") { + println!("adding `signature` output"); + outputs.push(schema::Source { + name: "signature".to_owned(), + r#type: "signature".to_owned(), + default_value: serde_json::Value::Null, + tooltip: String::new(), + optional: true, + }); + } + if !inputs.iter().any(|o| o.name == "submit") { + println!("adding `submit` input"); + inputs.push(schema::Target { + name: "submit".to_owned(), + type_bounds: ["bool".to_owned()].into(), + default_value: serde_json::Value::Bool(true), + tooltip: String::new(), + required: false, + passthrough: false, + }); + } + + let info = schema::InstructionInfo { + before: outputs + .iter() + .map(|o| o.name.clone()) + .filter(|name| name != "signature") + .chain( + inputs + .iter() + .filter(|i| i.passthrough) + .map(|i| i.name.clone()), + ) + .collect(), + signature: "signature".to_owned(), + after: Vec::new(), + }; + println!( + "adding instruction info: {}", + serde_json::to_string_pretty(&info).unwrap() + ); + Some(info) + } else { + None + }; + + let def = schema::CommandDefinition { + r#type: "native".to_owned(), + data: schema::Data { + node_definition_version: Some("0.1".to_owned()), + unique_id: Some(String::new()), + node_id, + version: "0.1".to_owned(), + display_name, + description, + tags: Some(Vec::new()), + related_to: Some( + [schema::RelatedTo { + id: String::new(), + r#type: String::new(), + relationship: String::new(), + }] + .into(), + ), + resources: Some(schema::Resources { + source_code_url: String::new(), + documentation_url: String::new(), + }), + usage: Some(schema::Usage { + license: "Apache-2.0".to_owned(), + license_url: String::new(), + pricing: schema::Pricing { + currency: "USDC".to_owned(), + purchase_price: 0, + price_per_run: 0, + custom: Some(schema::CustomPricing { + unit: "monthly".to_owned(), + value: "0".to_owned(), + }), + }, + }), + authors: Some( + [schema::Author { + name: "Space Operator".to_owned(), + contact: String::new(), + }] + .into(), + ), + instruction_info: info, + options: None, + design: None, + }, + sources: outputs, + targets: inputs, + ui_schema: serde_json::Value::Object(<_>::default()), + json_schema: serde_json::Value::Object(<_>::default()), + }; + + Ok(def) +} + +fn relative_to_pwd>(path: P) -> PathBuf { + let path = path.as_ref(); + if path.is_relative() { + return path.to_owned(); + } + let mut pwd = std::env::current_dir().unwrap_or_default(); + let mut result = PathBuf::new(); + loop { + match path.strip_prefix(&pwd) { + Ok(suffix) => { + result.push(suffix); + break; + } + Err(_) => { + pwd.pop(); + result.push(".."); + } + } + } + result +} + +async fn write_node_definition( + def: &CommandDefinition, + package: &Package, + modules: &[&str], +) -> Result, Report> { + let root = package + .manifest_path + .parent() + .ok_or_else(|| Report::new(Error::Io("find package path")))? + .as_std_path(); + + let mut path = root.join("node-definitions"); + path.extend(modules); + tokio::fs::create_dir_all(&path) + .await + .change_context(Error::Io("create dir"))?; + path.push(&format!("{}.json", def.data.node_id)); + let path = relative_to_pwd(path); + + println!("writing node definition to {}", path.display()); + if path.is_file() { + if !ask("file already exists, overwrite?").await { + return Ok(None); + } + } + let content = serde_json::to_string_pretty(def).change_context(Error::Json)?; + write_file(&path, content).await?; + Ok(Some(path)) +} + +fn value_type_to_rust_type(ty: schema::ValueType) -> TokenStream { + match ty { + schema::ValueType::Bool => quote! { bool }, + schema::ValueType::U8 => quote! { u8 }, + schema::ValueType::U16 => quote! { u16 }, + schema::ValueType::U32 => quote! { u32 }, + schema::ValueType::U64 => quote! { u64}, + schema::ValueType::U128 => quote! { u128 }, + schema::ValueType::I8 => quote! { i8 }, + schema::ValueType::I16 => quote! { i16}, + schema::ValueType::I32 => quote! { i32 }, + schema::ValueType::I64 => quote! { i64 }, + schema::ValueType::I128 => quote! { i128}, + schema::ValueType::F32 => quote! { f32 }, + schema::ValueType::F64 => quote! { f64 }, + schema::ValueType::Decimal => quote! { Decimal }, + schema::ValueType::Pubkey => quote! { Pubkey }, + schema::ValueType::Address => quote! { String }, + schema::ValueType::Keypair => quote! { Keypair }, + schema::ValueType::Signature => quote! { Signature }, + schema::ValueType::String => quote! { String }, + schema::ValueType::Bytes => quote! { Bytes }, + schema::ValueType::Array => quote! { Vec }, + schema::ValueType::Map => quote! { ValueSet }, + schema::ValueType::Json => quote! { JsonValue }, + schema::ValueType::Free => quote! { Value }, + } +} + +fn rust_type( + bounds: &[String], + optional: bool, + default_value: &serde_json::Value, +) -> proc_macro2::TokenStream { + let ty = bounds + .get(0) + .and_then(|ty| ty.parse::().ok()) + .unwrap_or(schema::ValueType::Free); + let use_option = + optional && !(ty == ValueType::Bool && matches!(default_value, serde_json::Value::Bool(_))); + let ty = value_type_to_rust_type(ty); + let ty = if use_option { + quote! { Option<#ty> } + } else { + ty + }; + ty +} + +fn rust_type_serde_decor( + bounds: &[String], + optional: bool, + default_value: &serde_json::Value, +) -> proc_macro2::TokenStream { + let ty = bounds + .get(0) + .and_then(|ty| ty.parse::().ok()) + .unwrap_or(schema::ValueType::Free); + match ty { + ValueType::Bool => { + if optional && default_value.as_bool().is_some() { + let default = default_value.as_bool().unwrap(); + let path = if default { + "value::default::bool_true" + } else { + "value::default::bool_false" + }; + return quote! { #[serde(default = #path)]}; + } + } + ValueType::U8 => {} + ValueType::U16 => {} + ValueType::U32 => {} + ValueType::U64 => {} + ValueType::U128 => {} + ValueType::I8 => {} + ValueType::I16 => {} + ValueType::I32 => {} + ValueType::I64 => {} + ValueType::I128 => {} + ValueType::F32 => {} + ValueType::F64 => {} + ValueType::Decimal => { + return if optional { + quote! { + #[serde(default, with = "value::decimal::opt")] + } + } else { + quote! { + #[serde(with = "value::decimal")] + } + }; + } + ValueType::Pubkey => { + return if optional { + quote! { + #[serde(default, with = "value::pubkey::opt")] + } + } else { + quote! { + #[serde(with = "value::pubkey")] + } + }; + } + ValueType::Address => {} + ValueType::Keypair => { + return if optional { + quote! { + #[serde(default, with = "value::keypair::opt")] + } + } else { + quote! { + #[serde(with = "value::keypair")] + } + }; + } + ValueType::Signature => { + return if optional { + quote! { + #[serde(default, with = "value::signature::opt")] + } + } else { + quote! { + #[serde(with = "value::signature")] + } + }; + } + ValueType::String => {} + ValueType::Bytes => {} + ValueType::Array => {} + ValueType::Map => {} + ValueType::Json => {} + ValueType::Free => {} + } + + quote! {} +} + +fn make_input_struct( + targets: impl IntoIterator>, +) -> TokenStream { + let inputs = targets.into_iter().map(|t| { + let t = t.borrow(); + let name = format_ident!("{}", t.name); + let ty = rust_type(&t.type_bounds, !t.required, &t.default_value); + let serde_decor = rust_type_serde_decor(&t.type_bounds, !t.required, &t.default_value); + quote! { + #serde_decor + #name: #ty + } + }); + quote! { + #[derive(Deserialize, Serialize, Debug)] + struct Input { + #(#inputs),* + } + } +} + +fn make_output_struct( + sources: impl IntoIterator>, +) -> TokenStream { + let outputs = sources.into_iter().map(|t| { + let t = t.borrow(); + let name = format_ident!("{}", t.name); + let ty = rust_type(&[t.r#type.clone()], t.optional, &t.default_value); + let serde_decor = rust_type_serde_decor(&[t.r#type.clone()], t.optional, &t.default_value); + quote! { + #serde_decor + #name: #ty + } + }); + quote! { + #[derive(Deserialize, Serialize, Debug)] + struct Output { + #(#outputs),* + } + } +} + +fn fmt_code(code: TokenStream) -> String { + syn::parse2::(code.clone()) + .map(|file| prettyplease::unparse(&file)) + .unwrap_or_else(|error| { + eprintln!("invalid code: {}", error); + code.to_string() + }) +} + +fn code_template(def: &CommandDefinition, modules: &[&str]) -> String { + let node_id = &def.data.node_id; + let node_definition_path = modules.join("/") + "/" + node_id + ".json"; + let input_struct = make_input_struct(&def.targets); + let output_struct = make_output_struct(&def.sources); + let execute = if def.data.instruction_info.is_some() { + quote! { + // call ctx.execute to emit Solana instructions + let signature = ctx.execute(Instructions::default(), value::map! {}).await?.signature; + } + } else { + quote! {} + }; + let code = quote! { + use flow_lib::command::prelude::*; + + const NAME: &str = #node_id; + + flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + + fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!(#node_definition_path); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) + } + + #input_struct + + #output_struct + + async fn run(mut ctx: Context, input: Input) -> Result { + tracing::info!("input: {:?}", input); + + #execute + + Err(CommandError::msg("unimplemented")) + // Ok(Output { }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + async fn test_run() { + let ctx = Context::default(); + + build().unwrap().run(ctx, ValueSet::new()).await.unwrap_err(); + } + } + }; + + fmt_code(code) +} + +fn find_parent_module>(path: P) -> Result> { + let path = path.as_ref(); + let parent = path.parent().ok_or_else(|| Error::Io("get parent path"))?; + + let mod_rs = parent.join("mod.rs"); + if mod_rs.is_file() { + return Ok(mod_rs); + } + + let mut mod_rs = parent.to_owned(); + mod_rs.set_extension("rs"); + if mod_rs.is_file() { + return Ok(mod_rs); + } + + let lib_rs = parent.join("lib.rs"); + if lib_rs.is_file() { + return Ok(lib_rs); + } + + Err(Report::new(Error::Io("find parent module"))) +} + +async fn update_parent_module( + def: &CommandDefinition, + module_path: &Path, +) -> Result<(), Report> { + let parent_module_path = find_parent_module(module_path)?; + + let mut parent_module = read_file(&parent_module_path).await?; + + let parsed_parent_module = + syn::parse_file(&parent_module).change_context(Error::Io("invalid rust code"))?; + let has_module = parsed_parent_module.items.iter().any(|item| { + if let syn::Item::Mod(m) = item { + return m.ident.to_string() == def.data.node_id; + } else { + false + } + }); + + if !has_module { + std::fmt::write( + &mut parent_module, + format_args!("\npub mod {};\n", def.data.node_id), + ) + .unwrap(); + println!("updating module {}", parent_module_path.display()); + write_file(&parent_module_path, parent_module).await?; + } + Ok(()) +} + +async fn write_code( + def: &CommandDefinition, + target: &Target, + modules: &[&str], +) -> Result<(), Report> { + let root = target + .src_path + .parent() + .ok_or_else(|| Report::new(Error::Io("find package source path")))? + .as_std_path(); + + let mut path = root.to_path_buf(); + path.extend(modules); + tokio::fs::create_dir_all(&path) + .await + .change_context(Error::Io("create dir"))?; + path.push(format!("{}.rs", def.data.node_id)); + let path = relative_to_pwd(path); + + println!("writing code to {}", path.display()); + if path.is_file() { + if !ask("file already exists, overwrite?").await { + return Ok(()); + } + } + + let code = code_template(def, modules); + tokio::fs::write(&path, code) + .await + .change_context(Error::Io("write file"))?; + + if let Err(error) = update_parent_module(def, &path).await { + eprintln!("failed to update parent module: {:?}", error); + } + + Ok(()) +} + +async fn new_node(allow_dirty: bool, package: &Option) -> Result<(), Report> { + if is_dirty() + .inspect_err(|error| { + eprintln!("{:?}", error); + }) + .unwrap_or(false) + { + if !allow_dirty { + eprintln!("dirty git repository"); + eprintln!("use --allow-dirty to continue"); + return Ok(()); + } + } + let meta = cargo_metadata()?; + let member = if let Some(name) = package.as_deref() { + find_target_crate_by_name(&meta, name)? + } else if let Some(member) = find_target_crate(&meta)? { + member + } else { + eprintln!("could not determine which package to update"); + eprintln!("use `-p` option to specify a package"); + let list = meta + .workspace_packages() + .iter() + .filter(|p| p.targets.iter().any(|t| t.is_lib())) + .map(|p| format!("\n {}", p.name)) + .collect::(); + eprintln!("available packages: {}", list); + return Ok(()); + }; + let lib_target = member + .targets + .iter() + .find(|p| p.is_lib()) + .ok_or_else(|| Error::NotLib(member.name.clone()))?; + println!("using package: {}", member.name); + + println!("enter ? for help"); + + let rust_module_regex = Regex::new( + r#"^(\p{XID_Start}|_)\p{XID_Continue}*(::(\p{XID_Start}|_)\p{XID_Continue}*)*$"#, + ) + .unwrap(); + let rust_module_hint = "enter valid Rust module path to save the node (empty to save at root)"; + let module_path = Prompt::builder() + .question("module path: ") + .check_regex(&rust_module_regex) + .regex_hint(rust_module_hint) + .allow_empty(true) + .build() + .prompt(&mut stdin()) + .await?; + let modules = module_path.split("::").collect::>(); + + let def = prompt_node_definition().await?; + + if let Some(nd) = write_node_definition(&def, member, &modules).await? { + write_code(&def, lib_target, &modules).await?; + + let upload = ask("upload node").await; + if upload { + upload_node(&nd, false, false).await?; + } + } + Ok(()) +} + +#[derive(bon::Builder)] +struct Prompt<'a> { + #[builder(into)] + question: Cow<'a, str>, + #[builder(default)] + allow_empty: bool, + check_regex: Option<&'a Regex>, + #[builder(into)] + regex_hint: Option>, + check_list: Option<&'a [&'a str]>, +} + +impl<'a> Prompt<'a> { + pub async fn prompt( + &self, + stdin: &mut S, + ) -> Result> { + let mut tries = 5; + loop { + match self.prompt_inner(stdin).await { + Ok(result) => break Ok(result), + Err(error) => { + tries -= 1; + if tries == 0 { + break Err(error); + } else { + eprintln!("{:?}", error); + } + } + } + } + } + + async fn prompt_inner( + &self, + stdin: &mut S, + ) -> Result> { + let result = { + loop { + if self.check_list.is_some() || self.regex_hint.is_some() { + print!("{} ", style("?").dim()); + } + print!("{}", style(&self.question).bold()); + std::io::stdout().flush().ok(); + let mut result = String::new(); + stdin.read_line(&mut result).await.ok(); + let result = result.trim(); + if let Some(hint) = &self.regex_hint { + if result == "?" { + println!("{}", hint); + continue; + } + } + if let Some(list) = self.check_list { + if result == "?" { + let availables = format!("possible values: {}", list.join(", ")); + println!("{}", availables); + continue; + } + } + break result.to_owned(); + } + }; + + if self.allow_empty && result.is_empty() { + return Ok(result.to_owned()); + } + if let Some(re) = &self.check_regex { + if !re.is_match(&result) { + let mut report = Report::new(Error::InvalidValue); + if let Some(hint) = &self.regex_hint { + report = report.attach_printable(hint.clone().into_owned()); + } + return Err(report); + } + } + if let Some(list) = self.check_list { + if !list.contains(&result.as_str()) { + let availables = format!("possible values: {}", list.join(", ")); + let report = Report::new(Error::InvalidValue).attach_printable(availables); + return Err(report); + } + } + Ok(result.to_owned()) + } +} + +async fn upload_node(path: &Path, dry_run: bool, no_confirm: bool) -> Result<(), Report> { + let mut client = ApiClient::load().await.change_context(Error::NotLogin)?; + let text = read_file(path).await?; + let def = serde_json::from_str::(&text) + .change_context(Error::ParseNodeDefinition)?; + if def.r#type != "native" { + return Err( + Error::Unimplemented("we only support uploading native nodes at the moment").into(), + ); + } + println!("node: {}", def.data.node_id); + match client.get_my_native_node(&def.data.node_id).await? { + Some((id, db)) => { + if print_diff(&def, &db) { + if dry_run { + return Ok(()); + } + if !no_confirm { + let yes = ask("update node?").await; + if !yes { + return Ok(()); + } + } + let (_, url_path) = client.update_node(id, &def).await?; + println!("updated node, id={}", id); + let url = format!("https://spaceoperator.com/dashboard/nodes/{}", url_path); + println!("view your node:\n{}", url); + } + } + None => { + println!("command is not in database"); + if dry_run { + return Ok(()); + } + if !no_confirm { + let yes = ask("upload?").await; + if !yes { + return Ok(()); + } + } + let (id, url_path) = client.insert_node(&def).await?; + println!("inserted new node, id={}", id); + let url = format!("https://spaceoperator.com/dashboard/nodes/{}", url_path); + println!("view your node:\n{}", url); + } + } + + Ok(()) +} + +type AsyncStdin = AllowStdIo>; + +fn stdin() -> AsyncStdin { + AllowStdIo::new(BufReader::new(std::io::stdin())) +} + +async fn generate_input_struct(path: impl AsRef) -> Result<(), Report> { + let nd = read_file(path).await?; + let nd = serde_json::from_str::(&nd) + .change_context(Error::Json) + .attach_printable("not a valid node definition file")?; + let input_struct = make_input_struct(&nd.targets); + let code = fmt_code(input_struct); + println!("{}", code); + Ok(()) +} + +async fn generate_output_struct(path: impl AsRef) -> Result<(), Report> { + let nd = read_file(path).await?; + let nd = serde_json::from_str::(&nd) + .change_context(Error::Json) + .attach_printable("not a valid node definition file")?; + let output_struct = make_output_struct(&nd.sources); + let code = fmt_code(output_struct); + println!("{}", code); + Ok(()) +} + +fn strip_slash(mut s: String) -> String { + if s.ends_with("/") { + s.pop(); + } + s +} + +async fn generate_flow_server_config(path: impl AsRef) -> Result<(), Report> { + let client = ApiClient::load().await.change_context(Error::NotLogin)?; + + let apikey = client.config.apikey; + let anon_key = client.config.info.anon_key; + let endpoint = strip_slash(client.config.info.supabase_url.to_string()); + let upstream_url = strip_slash(client.config.flow_server.to_string()); + + #[rustfmt::skip] + let config = toml::toml! { +host = "0.0.0.0" +port = 8080 + +local_storage = "_data/guest_local_storage" + +cors_origins = [ "*" ] + +[supabase] +anon_key = anon_key +endpoint = endpoint + +[db] +upstream_url = upstream_url +api_keys = [ apikey ] + }; + + let text = toml::to_string_pretty(&config).change_context(Error::SerializeData)?; + + let path = path.as_ref(); + if write_file(path, text).await? { + println!("generated {}", relative_to_pwd(path).display()); + } + + Ok(()) +} + +fn make_absolute(path: impl AsRef) -> Result> { + let path = path.as_ref(); + if path.is_relative() { + let pwd = std::env::current_dir().change_context(Error::Io("pwd"))?; + Ok(pwd.join(path)) + } else { + Ok(path.to_owned()) + } +} + +async fn start_flow_server(config: &Option) -> Result<(), Report> { + let meta = + cargo_metadata().attach_printable("make sure you are inside flow-backend repository")?; + find_target_crate_by_name(&meta, "flow-server") + .attach_printable("make sure you are inside flow-backend repository")?; + + let config_path = match config { + Some(config) => make_absolute(config)?, + None => { + let path = meta.workspace_root.join("config.toml"); + if !path.is_file() { + generate_flow_server_config(&path).await?; + } + path.into_std_path_buf() + } + }; + + if matches!(std::env::current_dir(), Ok(dir) if dir != meta.workspace_root) { + println!("$ cd {}", relative_to_pwd(&meta.workspace_root).display()); + std::env::set_current_dir(&meta.workspace_root).change_context(Error::Io("chdir"))?; + } + let config_path = relative_to_pwd(config_path); + + spawn_blocking(move || -> Result<(), Report> { + let sh = Shell::new().change_context(Error::Shell)?; + cmd!(sh, "cargo build --bin flow-server") + .run() + .change_context(Error::Subprocess)?; + let flow_server = relative_to_pwd(meta.target_directory.join("debug/flow-server")); + let rust_log = std::env::var("RUST_LOG") + .unwrap_or_else(|_| "info,actix_web=debug,flow_server=debug".to_owned()); + cmd!(sh, "{flow_server} {config_path}") + .env("RUST_LOG", rust_log) + .run() + .change_context(Error::Subprocess)?; + Ok(()) + }) + .await + .change_context(Error::Thread)??; + + Ok(()) +} + +async fn get_latest_version() -> Result> { + let versions = CLIENT + .get("https://index.crates.io/sp/ac/space-operator-cli") + .send() + .await + .change_context(Error::Http)? + .text() + .await + .change_context(Error::Http)?; + let latest = versions.lines().last().unwrap_or_default(); + #[derive(Deserialize)] + struct Meta { + vers: String, + } + let latest = serde_json::from_str::(latest).change_context(Error::Json)?; + Version::parse(&latest.vers).change_context(Error::Version) +} + +async fn check_latest_version() -> Result<(), Report> { + let name = env!("CARGO_PKG_NAME"); + assert!(name == "space-operator-cli"); + let version = Version::parse(env!("CARGO_PKG_VERSION")).change_context(Error::Version)?; + match get_latest_version().await { + Ok(latest) => { + if version.cmp_precedence(&latest) == Ordering::Less { + let string = format!("new version available: {}, please update!", latest); + eprintln!("{}", style(&string).yellow()); + } + } + Err(error) => { + eprintln!("{:?}", error); + } + } + + Ok(()) +} + +async fn run() -> Result<(), Report> { + let args = Args::parse(); + let flow_server = args + .url + .unwrap_or_else(|| Url::parse("https://dev-api.spaceoperator.com").unwrap()); + match &args.command { + Some(Commands::Login {}) => { + check_latest_version().await?; + println!("Go to https://spaceoperator.com/dashboard/profile/apikey go generate a key"); + println!("Please paste your API key below"); + let mut key = String::new(); + let mut stdin = stdin(); + stdin.read_line(&mut key).await.ok(); + let key = key.trim().to_owned(); + + let mut client = ApiClient::new(flow_server, key).await?; + let username = client.get_username().await?.unwrap_or_default(); + println!("Logged in as {:?}", username); + client.save_application_data().await?; + } + Some(Commands::Start { config }) => { + check_latest_version().await?; + start_flow_server(config).await?; + } + Some(Commands::Node { command }) => match command { + NodeCommands::New { + allow_dirty, + package, + } => { + check_latest_version().await?; + new_node(*allow_dirty, package).await?; + } + NodeCommands::Upload { + path, + dry_run, + no_confirm, + } => { + check_latest_version().await?; + upload_node(path, *dry_run, *no_confirm).await?; + } + }, + Some(Commands::Generate { command }) => match command { + GenerateCommands::Input { path } => generate_input_struct(path).await?, + GenerateCommands::Output { path } => generate_output_struct(path).await?, + GenerateCommands::Config { path } => match path { + Some(path) => generate_flow_server_config(path).await?, + None => generate_flow_server_config("config.toml").await?, + }, + }, + None => { + Args::command().print_long_help().ok(); + } + } + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let color_mode = match get_color() { + ColorChoice::Auto => { + if console::colors_enabled_stderr() { + error_stack::fmt::ColorMode::Color + } else { + error_stack::fmt::ColorMode::None + } + } + ColorChoice::Always => error_stack::fmt::ColorMode::Color, + ColorChoice::Never => error_stack::fmt::ColorMode::None, + }; + Report::set_color_mode(color_mode); + Report::install_debug_hook::(|_, _| {}); + if let Err(error) = run().await { + eprintln!("{:#?}", error); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap_markdown::help_markdown; + + #[test] + fn print_markdown_help() { + println!("{}", help_markdown::()); + } +} diff --git a/lib/space-operator-cli/src/schema.rs b/lib/space-operator-cli/src/schema.rs new file mode 100644 index 00000000..467ad0d4 --- /dev/null +++ b/lib/space-operator-cli/src/schema.rs @@ -0,0 +1,151 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstructionInfo { + pub before: Vec, + pub signature: String, + pub after: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, strum::EnumString, strum::EnumIter, strum::IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum ValueType { + Bool, + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + F32, + F64, + #[strum(serialize = "number")] + #[strum(serialize = "decimal")] + Decimal, + Pubkey, + // Wormhole address + Address, + Keypair, + Signature, + String, + Bytes, + #[strum(serialize = "array")] + #[strum(serialize = "list")] + Array, + #[strum(serialize = "object")] + #[strum(serialize = "map")] + Map, + Json, + Free, +} + +// ID in database +pub type CommandId = i64; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Data { + pub node_definition_version: Option, + pub unique_id: Option, + pub node_id: String, + pub version: String, + pub display_name: String, + pub description: String, + pub tags: Option>, + pub related_to: Option>, + pub resources: Option, + pub usage: Option, + pub authors: Option>, + pub design: Option, + pub options: Option, + pub instruction_info: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct RelatedTo { + pub id: String, + pub r#type: String, + pub relationship: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Resources { + pub source_code_url: String, + pub documentation_url: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Usage { + pub license: String, + pub license_url: String, + pub pricing: Pricing, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Pricing { + pub currency: String, + pub purchase_price: u64, + pub price_per_run: u64, + pub custom: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CustomPricing { + pub unit: String, + pub value: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Author { + pub name: String, + pub contact: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Design { + pub width: u64, + pub height: u64, + pub icon_url: String, + #[serde(rename = "backgroundColor")] + pub background_color: String, + #[serde(rename = "backgroundColorDark")] + pub background_color_dark: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Source { + pub name: String, + pub r#type: String, + #[serde(rename = "defaultValue")] + pub default_value: JsonValue, + pub tooltip: String, + #[serde(default)] + pub optional: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Target { + pub name: String, + pub type_bounds: Vec, + pub required: bool, + #[serde(rename = "defaultValue")] + pub default_value: JsonValue, + pub tooltip: String, + pub passthrough: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CommandDefinition { + pub r#type: String, + pub data: Data, + pub sources: Vec, + pub targets: Vec, + #[serde(rename = "targets_form.ui_schema")] + pub ui_schema: JsonValue, + #[serde(rename = "targets_form.json_schema")] + pub json_schema: JsonValue, +}