Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

add pull and serve to cli

+555 -5
cli/Cargo.lock
···
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
+
name = "axum"
+
version = "0.7.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+
dependencies = [
+
"async-trait",
+
"axum-core",
+
"bytes",
+
"futures-util",
+
"http",
+
"http-body",
+
"http-body-util",
+
"hyper",
+
"hyper-util",
+
"itoa",
+
"matchit",
+
"memchr",
+
"mime",
+
"percent-encoding",
+
"pin-project-lite",
+
"rustversion",
+
"serde",
+
"serde_json",
+
"serde_path_to_error",
+
"serde_urlencoded",
+
"sync_wrapper",
+
"tokio",
+
"tower 0.5.2",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "axum-core"
+
version = "0.4.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+
dependencies = [
+
"async-trait",
+
"bytes",
+
"futures-util",
+
"http",
+
"http-body",
+
"http-body-util",
+
"mime",
+
"pin-project-lite",
+
"rustversion",
+
"sync_wrapper",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+
[[package]]
+
name = "byteorder"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
···
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
[[package]]
+
name = "cordyceps"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
+
dependencies = [
+
"loom",
+
"tracing",
+
]
+
+
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
]
[[package]]
+
name = "derive_more"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
+
dependencies = [
+
"derive_more-impl",
+
]
+
+
[[package]]
+
name = "derive_more-impl"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn 2.0.108",
+
"unicode-xid",
+
]
+
+
[[package]]
+
name = "diatomic-waker"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
+
+
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
]
[[package]]
+
name = "futures-buffered"
+
version = "0.2.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd"
+
dependencies = [
+
"cordyceps",
+
"diatomic-waker",
+
"futures-core",
+
"pin-project-lite",
+
"spin 0.10.0",
+
]
+
+
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
+
name = "futures-lite"
+
version = "2.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+
dependencies = [
+
"fastrand",
+
"futures-core",
+
"futures-io",
+
"parking",
+
"pin-project-lite",
+
]
+
+
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"pin-project-lite",
"pin-utils",
"slab",
+
]
+
+
[[package]]
+
name = "generator"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
+
dependencies = [
+
"cc",
+
"cfg-if",
+
"libc",
+
"log",
+
"rustversion",
+
"windows",
[[package]]
···
[[package]]
+
name = "http-range-header"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"http",
"http-body",
"httparse",
+
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
···
"js-sys",
"log",
"wasm-bindgen",
-
"windows-core",
+
"windows-core 0.62.2",
[[package]]
···
"bon",
"bytes",
"chrono",
+
"ciborium",
"cid",
+
"futures",
"getrandom 0.2.16",
"getrandom 0.3.4",
"http",
···
"miette",
"multibase",
"multihash",
+
"n0-future",
"ouroboros",
"p256",
"rand 0.9.2",
···
"smol_str",
"thiserror 2.0.17",
"tokio",
+
"tokio-tungstenite-wasm",
"tokio-util",
"trait-variant",
"url",
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
-
"spin",
+
"spin 0.9.8",
[[package]]
···
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
+
name = "loom"
+
version = "0.7.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
+
dependencies = [
+
"cfg-if",
+
"generator",
+
"scoped-tls",
+
"tracing",
+
"tracing-subscriber",
+
]
+
+
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "matchers"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+
dependencies = [
+
"regex-automata",
+
]
+
+
[[package]]
+
name = "matchit"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "n0-future"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794"
+
dependencies = [
+
"cfg_aliases",
+
"derive_more",
+
"futures-buffered",
+
"futures-lite",
+
"futures-util",
+
"js-sys",
+
"pin-project",
+
"send_wrapper",
+
"tokio",
+
"tokio-util",
+
"wasm-bindgen",
+
"wasm-bindgen-futures",
+
"web-time",
+
]
+
+
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "nu-ansi-term"
+
version = "0.50.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+
dependencies = [
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
name = "num-bigint-dig"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
+
name = "openssl-probe"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "parking"
+
version = "2.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
+
]
+
+
[[package]]
+
name = "pin-project"
+
version = "1.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+
dependencies = [
+
"pin-project-internal",
+
]
+
+
[[package]]
+
name = "pin-project-internal"
+
version = "1.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn 2.0.108",
[[package]]
···
"tokio",
"tokio-rustls",
"tokio-util",
-
"tower",
-
"tower-http",
+
"tower 0.5.2",
+
"tower-http 0.6.6",
"tower-service",
"url",
"wasm-bindgen",
···
[[package]]
+
name = "rustls-native-certs"
+
version = "0.8.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
+
dependencies = [
+
"openssl-probe",
+
"rustls-pki-types",
+
"schannel",
+
"security-framework",
+
]
+
+
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "schannel"
+
version = "0.1.28"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+
dependencies = [
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "scoped-tls"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "security-framework"
+
version = "3.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
+
dependencies = [
+
"bitflags",
+
"core-foundation 0.10.1",
+
"core-foundation-sys",
+
"libc",
+
"security-framework-sys",
+
]
+
+
[[package]]
+
name = "security-framework-sys"
+
version = "2.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
+
[[package]]
+
name = "send_wrapper"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+
+
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "serde_path_to_error"
+
version = "0.1.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+
dependencies = [
+
"itoa",
+
"serde",
+
"serde_core",
+
]
+
+
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "sha1"
+
version = "0.10.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+
dependencies = [
+
"cfg-if",
+
"cpufeatures",
+
"digest",
+
]
+
+
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "sharded-slab"
+
version = "0.1.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+
dependencies = [
+
"lazy_static",
+
]
+
+
[[package]]
name = "shellexpand"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+
[[package]]
+
name = "spin"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]]
name = "spki"
···
[[package]]
+
name = "thread_local"
+
version = "1.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+
dependencies = [
+
"cfg-if",
+
]
+
+
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "tokio-tungstenite"
+
version = "0.24.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
+
dependencies = [
+
"futures-util",
+
"log",
+
"rustls",
+
"rustls-native-certs",
+
"rustls-pki-types",
+
"tokio",
+
"tokio-rustls",
+
"tungstenite",
+
]
+
+
[[package]]
+
name = "tokio-tungstenite-wasm"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e21a5c399399c3db9f08d8297ac12b500e86bca82e930253fdc62eaf9c0de6ae"
+
dependencies = [
+
"futures-channel",
+
"futures-util",
+
"http",
+
"httparse",
+
"js-sys",
+
"rustls",
+
"thiserror 1.0.69",
+
"tokio",
+
"tokio-tungstenite",
+
"wasm-bindgen",
+
"web-sys",
+
]
+
+
[[package]]
name = "tokio-util"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"bytes",
"futures-core",
"futures-sink",
+
"futures-util",
"pin-project-lite",
"tokio",
[[package]]
name = "tower"
+
version = "0.4.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+
dependencies = [
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
···
"tokio",
"tower-layer",
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "tower-http"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+
dependencies = [
+
"async-compression",
+
"bitflags",
+
"bytes",
+
"futures-core",
+
"futures-util",
+
"http",
+
"http-body",
+
"http-body-util",
+
"http-range-header",
+
"httpdate",
+
"mime",
+
"mime_guess",
+
"percent-encoding",
+
"pin-project-lite",
+
"tokio",
+
"tokio-util",
+
"tower-layer",
+
"tower-service",
+
"tracing",
[[package]]
···
"http-body",
"iri-string",
"pin-project-lite",
-
"tower",
+
"tower 0.5.2",
"tower-layer",
"tower-service",
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
+
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
···
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
+
"valuable",
+
]
+
+
[[package]]
+
name = "tracing-log"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+
dependencies = [
+
"log",
+
"once_cell",
+
"tracing-core",
+
]
+
+
[[package]]
+
name = "tracing-subscriber"
+
version = "0.3.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+
dependencies = [
+
"matchers",
+
"nu-ansi-term",
+
"once_cell",
+
"regex-automata",
+
"sharded-slab",
+
"smallvec",
+
"thread_local",
+
"tracing",
+
"tracing-core",
+
"tracing-log",
[[package]]
···
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
+
name = "tungstenite"
+
version = "0.24.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
+
dependencies = [
+
"byteorder",
+
"bytes",
+
"data-encoding",
+
"http",
+
"httparse",
+
"log",
+
"rand 0.8.5",
+
"rustls",
+
"rustls-pki-types",
+
"sha1",
+
"thiserror 1.0.69",
+
"utf-8",
+
]
+
+
[[package]]
name = "twoway"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
+
name = "unicode-xid"
+
version = "0.2.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+
[[package]]
name = "unsigned-varint"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+
[[package]]
+
name = "valuable"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
···
[[package]]
+
name = "windows"
+
version = "0.61.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+
dependencies = [
+
"windows-collections",
+
"windows-core 0.61.2",
+
"windows-future",
+
"windows-link 0.1.3",
+
"windows-numerics",
+
]
+
+
[[package]]
+
name = "windows-collections"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+
dependencies = [
+
"windows-core 0.61.2",
+
]
+
+
[[package]]
+
name = "windows-core"
+
version = "0.61.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+
dependencies = [
+
"windows-implement",
+
"windows-interface",
+
"windows-link 0.1.3",
+
"windows-result 0.3.4",
+
"windows-strings 0.4.2",
+
]
+
+
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[package]]
+
name = "windows-future"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+
dependencies = [
+
"windows-core 0.61.2",
+
"windows-link 0.1.3",
+
"windows-threading",
+
]
+
+
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
+
name = "windows-numerics"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+
dependencies = [
+
"windows-core 0.61.2",
+
"windows-link 0.1.3",
+
]
+
+
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
+
]
+
+
[[package]]
+
name = "windows-threading"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+
dependencies = [
+
"windows-link 0.1.3",
[[package]]
···
name = "wisp-cli"
version = "0.1.0"
dependencies = [
+
"axum",
"base64 0.22.1",
"bytes",
+
"chrono",
"clap",
"flate2",
"futures",
···
"mime_guess",
"multibase",
"multihash",
+
"n0-future",
"reqwest",
"rustversion",
"serde",
···
"sha2",
"shellexpand",
"tokio",
+
"tower 0.4.13",
+
"tower-http 0.5.2",
+
"url",
"walkdir",
+7 -1
cli/Cargo.toml
···
jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["loopback"] }
jacquard-oauth = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
-
jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
+
jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["websocket"] }
jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["dns"] }
jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
···
multihash = "0.19.3"
multibase = "0.9"
sha2 = "0.10"
+
axum = "0.7"
+
tower-http = { version = "0.5", features = ["fs", "compression-gzip"] }
+
tower = "0.4"
+
n0-future = "0.1"
+
chrono = "0.4"
+
url = "2.5"
+71
cli/src/download.rs
···
+
use base64::Engine;
+
use bytes::Bytes;
+
use flate2::read::GzDecoder;
+
use jacquard_common::types::blob::BlobRef;
+
use miette::IntoDiagnostic;
+
use std::io::Read;
+
use url::Url;
+
+
/// Download a blob from the PDS
+
pub async fn download_blob(pds_url: &Url, blob_ref: &BlobRef<'_>, did: &str) -> miette::Result<Bytes> {
+
// Extract CID from blob ref
+
let cid = blob_ref.blob().r#ref.to_string();
+
+
// Construct blob download URL
+
// The correct endpoint is: /xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
+
let blob_url = pds_url
+
.join(&format!("/xrpc/com.atproto.sync.getBlob?did={}&cid={}", did, cid))
+
.into_diagnostic()?;
+
+
let client = reqwest::Client::new();
+
let response = client
+
.get(blob_url)
+
.send()
+
.await
+
.into_diagnostic()?;
+
+
if !response.status().is_success() {
+
return Err(miette::miette!(
+
"Failed to download blob: {}",
+
response.status()
+
));
+
}
+
+
let bytes = response.bytes().await.into_diagnostic()?;
+
Ok(bytes)
+
}
+
+
/// Decompress and decode a blob (base64 + gzip)
+
pub fn decompress_blob(data: &[u8], is_base64: bool, is_gzipped: bool) -> miette::Result<Vec<u8>> {
+
let mut current_data = data.to_vec();
+
+
// First, decode base64 if needed
+
if is_base64 {
+
current_data = base64::prelude::BASE64_STANDARD
+
.decode(&current_data)
+
.into_diagnostic()?;
+
}
+
+
// Then, decompress gzip if needed
+
if is_gzipped {
+
let mut decoder = GzDecoder::new(&current_data[..]);
+
let mut decompressed = Vec::new();
+
decoder.read_to_end(&mut decompressed).into_diagnostic()?;
+
current_data = decompressed;
+
}
+
+
Ok(current_data)
+
}
+
+
/// Download and decompress a blob
+
pub async fn download_and_decompress_blob(
+
pds_url: &Url,
+
blob_ref: &BlobRef<'_>,
+
did: &str,
+
is_base64: bool,
+
is_gzipped: bool,
+
) -> miette::Result<Vec<u8>> {
+
let data = download_blob(pds_url, blob_ref, did).await?;
+
decompress_blob(&data, is_base64, is_gzipped)
+
}
+
+109 -16
cli/src/main.rs
···
mod place_wisp;
mod cid;
mod blob_map;
+
mod metadata;
+
mod download;
+
mod pull;
+
mod serve;
-
use clap::Parser;
+
use clap::{Parser, Subcommand};
use jacquard::CowStr;
use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession, AgentSession};
use jacquard::oauth::client::OAuthClient;
···
use place_wisp::fs::*;
#[derive(Parser, Debug)]
-
#[command(author, version, about = "Deploy a static site to wisp.place")]
+
#[command(author, version, about = "wisp.place CLI tool")]
struct Args {
+
#[command(subcommand)]
+
command: Option<Commands>,
+
+
// Deploy arguments (when no subcommand is specified)
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
-
input: CowStr<'static>,
+
#[arg(global = true, conflicts_with = "command")]
+
input: Option<CowStr<'static>>,
/// Path to the directory containing your static site
-
#[arg(short, long, default_value = ".")]
-
path: PathBuf,
+
#[arg(short, long, global = true, conflicts_with = "command")]
+
path: Option<PathBuf>,
/// Site name (defaults to directory name)
-
#[arg(short, long)]
+
#[arg(short, long, global = true, conflicts_with = "command")]
site: Option<String>,
-
/// Path to auth store file (will be created if missing, only used with OAuth)
-
#[arg(long, default_value = "/tmp/wisp-oauth-session.json")]
-
store: String,
+
/// Path to auth store file
+
#[arg(long, global = true, conflicts_with = "command")]
+
store: Option<String>,
-
/// App Password for authentication (alternative to OAuth)
-
#[arg(long)]
+
/// App Password for authentication
+
#[arg(long, global = true, conflicts_with = "command")]
password: Option<CowStr<'static>>,
}
+
#[derive(Subcommand, Debug)]
+
enum Commands {
+
/// Deploy a static site to wisp.place (default command)
+
Deploy {
+
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
+
input: CowStr<'static>,
+
+
/// Path to the directory containing your static site
+
#[arg(short, long, default_value = ".")]
+
path: PathBuf,
+
+
/// Site name (defaults to directory name)
+
#[arg(short, long)]
+
site: Option<String>,
+
+
/// Path to auth store file (will be created if missing, only used with OAuth)
+
#[arg(long, default_value = "/tmp/wisp-oauth-session.json")]
+
store: String,
+
+
/// App Password for authentication (alternative to OAuth)
+
#[arg(long)]
+
password: Option<CowStr<'static>>,
+
},
+
/// Pull a site from the PDS to a local directory
+
Pull {
+
/// Handle (e.g., alice.bsky.social) or DID
+
input: CowStr<'static>,
+
+
/// Site name (record key)
+
#[arg(short, long)]
+
site: String,
+
+
/// Output directory for the downloaded site
+
#[arg(short, long, default_value = ".")]
+
output: PathBuf,
+
},
+
/// Serve a site locally with real-time firehose updates
+
Serve {
+
/// Handle (e.g., alice.bsky.social) or DID
+
input: CowStr<'static>,
+
+
/// Site name (record key)
+
#[arg(short, long)]
+
site: String,
+
+
/// Output directory for the site files
+
#[arg(short, long, default_value = ".")]
+
output: PathBuf,
+
+
/// Port to serve on
+
#[arg(short, long, default_value = "8080")]
+
port: u16,
+
},
+
}
+
#[tokio::main]
async fn main() -> miette::Result<()> {
let args = Args::parse();
-
// Dispatch to appropriate authentication method
-
if let Some(password) = args.password {
-
run_with_app_password(args.input, password, args.path, args.site).await
-
} else {
-
run_with_oauth(args.input, args.store, args.path, args.site).await
+
match args.command {
+
Some(Commands::Deploy { input, path, site, store, password }) => {
+
// Dispatch to appropriate authentication method
+
if let Some(password) = password {
+
run_with_app_password(input, password, path, site).await
+
} else {
+
run_with_oauth(input, store, path, site).await
+
}
+
}
+
Some(Commands::Pull { input, site, output }) => {
+
pull::pull_site(input, CowStr::from(site), output).await
+
}
+
Some(Commands::Serve { input, site, output, port }) => {
+
serve::serve_site(input, CowStr::from(site), output, port).await
+
}
+
None => {
+
// Legacy mode: if input is provided, assume deploy command
+
if let Some(input) = args.input {
+
let path = args.path.unwrap_or_else(|| PathBuf::from("."));
+
let store = args.store.unwrap_or_else(|| "/tmp/wisp-oauth-session.json".to_string());
+
+
// Dispatch to appropriate authentication method
+
if let Some(password) = args.password {
+
run_with_app_password(input, password, path, args.site).await
+
} else {
+
run_with_oauth(input, store, path, args.site).await
+
}
+
} else {
+
// No command and no input, show help
+
use clap::CommandFactory;
+
Args::command().print_help().into_diagnostic()?;
+
Ok(())
+
}
+
}
}
}
+46
cli/src/metadata.rs
···
+
use serde::{Deserialize, Serialize};
+
use std::collections::HashMap;
+
use std::path::Path;
+
use miette::IntoDiagnostic;
+
+
/// Metadata tracking file CIDs for incremental updates
+
#[derive(Debug, Clone, Serialize, Deserialize)]
+
pub struct SiteMetadata {
+
/// Record CID from the PDS
+
pub record_cid: String,
+
/// Map of file paths to their blob CIDs
+
pub file_cids: HashMap<String, String>,
+
/// Timestamp when the site was last synced
+
pub last_sync: i64,
+
}
+
+
impl SiteMetadata {
+
pub fn new(record_cid: String, file_cids: HashMap<String, String>) -> Self {
+
Self {
+
record_cid,
+
file_cids,
+
last_sync: chrono::Utc::now().timestamp(),
+
}
+
}
+
+
/// Load metadata from a directory
+
pub fn load(dir: &Path) -> miette::Result<Option<Self>> {
+
let metadata_path = dir.join(".wisp-metadata.json");
+
if !metadata_path.exists() {
+
return Ok(None);
+
}
+
+
let contents = std::fs::read_to_string(&metadata_path).into_diagnostic()?;
+
let metadata: SiteMetadata = serde_json::from_str(&contents).into_diagnostic()?;
+
Ok(Some(metadata))
+
}
+
+
/// Save metadata to a directory
+
pub fn save(&self, dir: &Path) -> miette::Result<()> {
+
let metadata_path = dir.join(".wisp-metadata.json");
+
let contents = serde_json::to_string_pretty(self).into_diagnostic()?;
+
std::fs::write(&metadata_path, contents).into_diagnostic()?;
+
Ok(())
+
}
+
}
+
+305
cli/src/pull.rs
···
+
use crate::blob_map;
+
use crate::download;
+
use crate::metadata::SiteMetadata;
+
use crate::place_wisp::fs::*;
+
use jacquard::CowStr;
+
use jacquard::prelude::IdentityResolver;
+
use jacquard_common::types::string::Did;
+
use jacquard_common::xrpc::XrpcExt;
+
use jacquard_identity::PublicResolver;
+
use miette::IntoDiagnostic;
+
use std::collections::HashMap;
+
use std::path::{Path, PathBuf};
+
use url::Url;
+
+
/// Pull a site from the PDS to a local directory
+
pub async fn pull_site(
+
input: CowStr<'static>,
+
rkey: CowStr<'static>,
+
output_dir: PathBuf,
+
) -> miette::Result<()> {
+
println!("Pulling site {} from {}...", rkey, input);
+
+
// Resolve handle to DID if needed
+
let resolver = PublicResolver::default();
+
let did = if input.starts_with("did:") {
+
Did::new(&input).into_diagnostic()?
+
} else {
+
// It's a handle, resolve it
+
let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?;
+
resolver.resolve_handle(&handle).await.into_diagnostic()?
+
};
+
+
// Resolve PDS endpoint for the DID
+
let pds_url = resolver.pds_for_did(&did).await.into_diagnostic()?;
+
println!("Resolved PDS: {}", pds_url);
+
+
// Fetch the place.wisp.fs record
+
+
println!("Fetching record from PDS...");
+
let client = reqwest::Client::new();
+
+
// Use com.atproto.repo.getRecord
+
use jacquard::api::com_atproto::repo::get_record::GetRecord;
+
use jacquard_common::types::string::Rkey as RkeyType;
+
let rkey_parsed = RkeyType::new(&rkey).into_diagnostic()?;
+
+
use jacquard_common::types::ident::AtIdentifier;
+
use jacquard_common::types::string::RecordKey;
+
let request = GetRecord::new()
+
.repo(AtIdentifier::Did(did.clone()))
+
.collection(CowStr::from("place.wisp.fs"))
+
.rkey(RecordKey::from(rkey_parsed))
+
.build();
+
+
let response = client
+
.xrpc(pds_url.clone())
+
.send(&request)
+
.await
+
.into_diagnostic()?;
+
+
let record_output = response.into_output().into_diagnostic()?;
+
let record_cid = record_output.cid.as_ref().map(|c| c.to_string()).unwrap_or_default();
+
+
// Parse the record value as Fs
+
use jacquard_common::types::value::from_data;
+
let fs_record: Fs = from_data(&record_output.value).into_diagnostic()?;
+
+
let file_count = fs_record.file_count.map(|c| c.to_string()).unwrap_or_else(|| "?".to_string());
+
println!("Found site '{}' with {} files", fs_record.site, file_count);
+
+
// Load existing metadata for incremental updates
+
let existing_metadata = SiteMetadata::load(&output_dir)?;
+
let existing_file_cids = existing_metadata
+
.as_ref()
+
.map(|m| m.file_cids.clone())
+
.unwrap_or_default();
+
+
// Extract blob map from the new manifest
+
let new_blob_map = blob_map::extract_blob_map(&fs_record.root);
+
let new_file_cids: HashMap<String, String> = new_blob_map
+
.iter()
+
.map(|(path, (_blob_ref, cid))| (path.clone(), cid.clone()))
+
.collect();
+
+
// Clean up any leftover temp directories from previous failed attempts
+
let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new("."));
+
let output_name = output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy();
+
let temp_prefix = format!(".tmp-{}-", output_name);
+
+
if let Ok(entries) = parent.read_dir() {
+
for entry in entries.flatten() {
+
let name = entry.file_name();
+
if name.to_string_lossy().starts_with(&temp_prefix) {
+
let _ = std::fs::remove_dir_all(entry.path());
+
}
+
}
+
}
+
+
// Check if we need to update (but only if output directory actually exists with files)
+
if let Some(metadata) = &existing_metadata {
+
if metadata.record_cid == record_cid {
+
// Verify that the output directory actually exists and has content
+
let has_content = output_dir.exists() &&
+
output_dir.read_dir()
+
.map(|mut entries| entries.any(|e| {
+
if let Ok(entry) = e {
+
!entry.file_name().to_string_lossy().starts_with(".wisp-metadata")
+
} else {
+
false
+
}
+
}))
+
.unwrap_or(false);
+
+
if has_content {
+
println!("Site is already up to date!");
+
return Ok(());
+
}
+
}
+
}
+
+
// Create temporary directory for atomic update
+
// Place temp dir in parent directory to avoid issues with non-existent output_dir
+
let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new("."));
+
let temp_dir_name = format!(
+
".tmp-{}-{}",
+
output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy(),
+
chrono::Utc::now().timestamp()
+
);
+
let temp_dir = parent.join(temp_dir_name);
+
std::fs::create_dir_all(&temp_dir).into_diagnostic()?;
+
+
println!("Downloading files...");
+
let mut downloaded = 0;
+
let mut reused = 0;
+
+
// Download files recursively
+
let download_result = download_directory(
+
&fs_record.root,
+
&temp_dir,
+
&pds_url,
+
did.as_str(),
+
&new_blob_map,
+
&existing_file_cids,
+
&output_dir,
+
String::new(),
+
&mut downloaded,
+
&mut reused,
+
)
+
.await;
+
+
// If download failed, clean up temp directory
+
if let Err(e) = download_result {
+
let _ = std::fs::remove_dir_all(&temp_dir);
+
return Err(e);
+
}
+
+
println!(
+
"Downloaded {} files, reused {} files",
+
downloaded, reused
+
);
+
+
// Save metadata
+
let metadata = SiteMetadata::new(record_cid, new_file_cids);
+
metadata.save(&temp_dir)?;
+
+
// Move files from temp to output directory
+
let output_abs = std::fs::canonicalize(&output_dir).unwrap_or_else(|_| output_dir.clone());
+
let current_dir = std::env::current_dir().into_diagnostic()?;
+
+
// Special handling for pulling to current directory
+
if output_abs == current_dir {
+
// Move files from temp to current directory
+
for entry in std::fs::read_dir(&temp_dir).into_diagnostic()? {
+
let entry = entry.into_diagnostic()?;
+
let dest = current_dir.join(entry.file_name());
+
+
// Remove existing file/dir if it exists
+
if dest.exists() {
+
if dest.is_dir() {
+
std::fs::remove_dir_all(&dest).into_diagnostic()?;
+
} else {
+
std::fs::remove_file(&dest).into_diagnostic()?;
+
}
+
}
+
+
// Move from temp to current dir
+
std::fs::rename(entry.path(), dest).into_diagnostic()?;
+
}
+
+
// Clean up temp directory
+
std::fs::remove_dir_all(&temp_dir).into_diagnostic()?;
+
} else {
+
// If output directory exists and has content, remove it first
+
if output_dir.exists() {
+
std::fs::remove_dir_all(&output_dir).into_diagnostic()?;
+
}
+
+
// Ensure parent directory exists
+
if let Some(parent) = output_dir.parent() {
+
if !parent.as_os_str().is_empty() && !parent.exists() {
+
std::fs::create_dir_all(parent).into_diagnostic()?;
+
}
+
}
+
+
// Rename temp to final location
+
match std::fs::rename(&temp_dir, &output_dir) {
+
Ok(_) => {},
+
Err(e) => {
+
// Clean up temp directory on failure
+
let _ = std::fs::remove_dir_all(&temp_dir);
+
return Err(miette::miette!("Failed to move temp directory: {}", e));
+
}
+
}
+
}
+
+
println!("✓ Site pulled successfully to {}", output_dir.display());
+
+
Ok(())
+
}
+
+
/// Recursively download a directory
+
fn download_directory<'a>(
+
dir: &'a Directory<'_>,
+
output_dir: &'a Path,
+
pds_url: &'a Url,
+
did: &'a str,
+
new_blob_map: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
+
existing_file_cids: &'a HashMap<String, String>,
+
existing_output_dir: &'a Path,
+
path_prefix: String,
+
downloaded: &'a mut usize,
+
reused: &'a mut usize,
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send + 'a>> {
+
Box::pin(async move {
+
for entry in &dir.entries {
+
let entry_name = entry.name.as_str();
+
let current_path = if path_prefix.is_empty() {
+
entry_name.to_string()
+
} else {
+
format!("{}/{}", path_prefix, entry_name)
+
};
+
+
match &entry.node {
+
EntryNode::File(file) => {
+
let output_path = output_dir.join(entry_name);
+
+
// Check if file CID matches existing
+
if let Some((_blob_ref, new_cid)) = new_blob_map.get(&current_path) {
+
if let Some(existing_cid) = existing_file_cids.get(&current_path) {
+
if existing_cid == new_cid {
+
// File unchanged, copy from existing directory
+
let existing_path = existing_output_dir.join(&current_path);
+
if existing_path.exists() {
+
std::fs::copy(&existing_path, &output_path).into_diagnostic()?;
+
*reused += 1;
+
println!(" ✓ Reused {}", current_path);
+
continue;
+
}
+
}
+
}
+
}
+
+
// File is new or changed, download it
+
println!(" ↓ Downloading {}", current_path);
+
let data = download::download_and_decompress_blob(
+
pds_url,
+
&file.blob,
+
did,
+
file.base64.unwrap_or(false),
+
file.encoding.as_ref().map(|e| e.as_str() == "gzip").unwrap_or(false),
+
)
+
.await?;
+
+
std::fs::write(&output_path, data).into_diagnostic()?;
+
*downloaded += 1;
+
}
+
EntryNode::Directory(subdir) => {
+
let subdir_path = output_dir.join(entry_name);
+
std::fs::create_dir_all(&subdir_path).into_diagnostic()?;
+
+
download_directory(
+
subdir,
+
&subdir_path,
+
pds_url,
+
did,
+
new_blob_map,
+
existing_file_cids,
+
existing_output_dir,
+
current_path,
+
downloaded,
+
reused,
+
)
+
.await?;
+
}
+
EntryNode::Unknown(_) => {
+
// Skip unknown node types
+
println!(" ⚠ Skipping unknown node type for {}", current_path);
+
}
+
}
+
}
+
+
Ok(())
+
})
+
}
+
+202
cli/src/serve.rs
···
+
use crate::pull::pull_site;
+
use axum::Router;
+
use jacquard::CowStr;
+
use jacquard_common::jetstream::{CommitOperation, JetstreamMessage, JetstreamParams};
+
use jacquard_common::types::string::Did;
+
use jacquard_common::xrpc::{SubscriptionClient, TungsteniteSubscriptionClient};
+
use miette::IntoDiagnostic;
+
use n0_future::StreamExt;
+
use std::path::PathBuf;
+
use std::sync::Arc;
+
use tokio::sync::RwLock;
+
use tower_http::compression::CompressionLayer;
+
use tower_http::services::ServeDir;
+
use url::Url;
+
+
/// Shared state for the server
+
#[derive(Clone)]
+
struct ServerState {
+
did: CowStr<'static>,
+
rkey: CowStr<'static>,
+
output_dir: PathBuf,
+
last_cid: Arc<RwLock<Option<String>>>,
+
}
+
+
/// Serve a site locally with real-time firehose updates
+
pub async fn serve_site(
+
input: CowStr<'static>,
+
rkey: CowStr<'static>,
+
output_dir: PathBuf,
+
port: u16,
+
) -> miette::Result<()> {
+
println!("Serving site {} from {} on port {}...", rkey, input, port);
+
+
// Resolve handle to DID if needed
+
use jacquard_identity::PublicResolver;
+
use jacquard::prelude::IdentityResolver;
+
+
let resolver = PublicResolver::default();
+
let did = if input.starts_with("did:") {
+
Did::new(&input).into_diagnostic()?
+
} else {
+
// It's a handle, resolve it
+
let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?;
+
resolver.resolve_handle(&handle).await.into_diagnostic()?
+
};
+
+
println!("Resolved to DID: {}", did.as_str());
+
+
// Create output directory if it doesn't exist
+
std::fs::create_dir_all(&output_dir).into_diagnostic()?;
+
+
// Initial pull of the site
+
println!("Performing initial pull...");
+
let did_str = CowStr::from(did.as_str().to_string());
+
pull_site(did_str.clone(), rkey.clone(), output_dir.clone()).await?;
+
+
// Create shared state
+
let state = ServerState {
+
did: did_str.clone(),
+
rkey: rkey.clone(),
+
output_dir: output_dir.clone(),
+
last_cid: Arc::new(RwLock::new(None)),
+
};
+
+
// Start firehose listener in background
+
let firehose_state = state.clone();
+
tokio::spawn(async move {
+
if let Err(e) = watch_firehose(firehose_state).await {
+
eprintln!("Firehose error: {}", e);
+
}
+
});
+
+
// Create HTTP server with gzip compression
+
let app = Router::new()
+
.fallback_service(
+
ServeDir::new(&output_dir)
+
.precompressed_gzip()
+
)
+
.layer(CompressionLayer::new())
+
.with_state(state);
+
+
let addr = format!("0.0.0.0:{}", port);
+
let listener = tokio::net::TcpListener::bind(&addr)
+
.await
+
.into_diagnostic()?;
+
+
println!("\n✓ Server running at http://localhost:{}", port);
+
println!(" Watching for updates on the firehose...\n");
+
+
axum::serve(listener, app).await.into_diagnostic()?;
+
+
Ok(())
+
}
+
+
/// Watch the firehose for updates to the specific site
+
fn watch_firehose(state: ServerState) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send>> {
+
Box::pin(async move {
+
let jetstream_url = Url::parse("wss://jetstream1.us-east.fire.hose.cam")
+
.into_diagnostic()?;
+
+
println!("[Firehose] Connecting to Jetstream...");
+
+
// Create subscription client
+
let client = TungsteniteSubscriptionClient::from_base_uri(jetstream_url);
+
+
// Subscribe with no filters (we'll filter manually)
+
// Jetstream doesn't support filtering by collection in the params builder
+
let params = JetstreamParams::new().build();
+
+
let stream = client.subscribe(&params).await.into_diagnostic()?;
+
println!("[Firehose] Connected! Watching for updates...");
+
+
// Convert to typed message stream
+
let (_sink, mut messages) = stream.into_stream();
+
+
loop {
+
match messages.next().await {
+
Some(Ok(msg)) => {
+
if let Err(e) = handle_firehose_message(&state, msg).await {
+
eprintln!("[Firehose] Error handling message: {}", e);
+
}
+
}
+
Some(Err(e)) => {
+
eprintln!("[Firehose] Stream error: {}", e);
+
// Try to reconnect after a delay
+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
+
return Box::pin(watch_firehose(state)).await;
+
}
+
None => {
+
println!("[Firehose] Stream ended, reconnecting...");
+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
+
return Box::pin(watch_firehose(state)).await;
+
}
+
}
+
}
+
})
+
}
+
+
/// Handle a firehose message
+
async fn handle_firehose_message(
+
state: &ServerState,
+
msg: JetstreamMessage<'_>,
+
) -> miette::Result<()> {
+
match msg {
+
JetstreamMessage::Commit {
+
did,
+
commit,
+
..
+
} => {
+
// Check if this is our site
+
if did.as_str() == state.did.as_str()
+
&& commit.collection.as_str() == "place.wisp.fs"
+
&& commit.rkey.as_str() == state.rkey.as_str()
+
{
+
match commit.operation {
+
CommitOperation::Create | CommitOperation::Update => {
+
let new_cid = commit.cid.as_ref().map(|c| c.to_string());
+
+
// Check if CID changed
+
let should_update = {
+
let last_cid = state.last_cid.read().await;
+
new_cid != *last_cid
+
};
+
+
if should_update {
+
println!("\n[Update] Detected change to site {} (CID: {:?})", state.rkey, new_cid);
+
println!("[Update] Pulling latest version...");
+
+
// Pull the updated site
+
match pull_site(
+
state.did.clone(),
+
state.rkey.clone(),
+
state.output_dir.clone(),
+
)
+
.await
+
{
+
Ok(_) => {
+
// Update last CID
+
let mut last_cid = state.last_cid.write().await;
+
*last_cid = new_cid;
+
println!("[Update] ✓ Site updated successfully!\n");
+
}
+
Err(e) => {
+
eprintln!("[Update] Failed to pull site: {}", e);
+
}
+
}
+
}
+
}
+
CommitOperation::Delete => {
+
println!("\n[Update] Site {} was deleted", state.rkey);
+
}
+
}
+
}
+
}
+
_ => {
+
// Ignore identity and account messages
+
}
+
}
+
+
Ok(())
+
}
+