@@ -2,6 +2,41 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
[[package]]
name = "ahash"
version = "0.8.12"
@@ -69,7 +104,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -80,7 +115,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -131,6 +166,206 @@ dependencies = [
"wait-timeout",
]
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener 5.4.1",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener 5.4.1",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-std"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
+dependencies = [
+ "async-attributes",
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-tungstenite"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
+dependencies = [
+ "async-std",
+ "atomic-waker",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "futures-util",
+ "log",
+ "pin-project-lite",
+ "tungstenite",
+]
+
+[[package]]
+name = "async_io_stream"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"
+dependencies = [
+ "futures",
+ "pharos",
+ "rustc_version",
+]
+
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
@@ -140,12 +375,24 @@ dependencies = [
"critical-section",
]
+[[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.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -161,6 +408,15 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -170,6 +426,19 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
[[package]]
name = "bstr"
version = "1.12.1"
@@ -187,6 +456,16 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+[[package]]
+name = "bytecodec"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325"
+dependencies = [
+ "byteorder",
+ "trackable 0.2.24",
+]
+
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -215,6 +494,30 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
[[package]]
name = "chrono"
version = "0.4.44"
@@ -226,6 +529,17 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
+
[[package]]
name = "clap"
version = "4.5.60"
@@ -292,6 +606,15 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -307,12 +630,33 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
[[package]]
name = "crossterm"
version = "0.29.0"
@@ -343,9 +687,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
+ "rand_core 0.6.4",
"typenum",
]
+[[package]]
+name = "crypto_secretbox"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1"
+dependencies = [
+ "aead",
+ "cipher",
+ "generic-array",
+ "poly1305",
+ "salsa20",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "fiat-crypto",
+ "rand_core 0.6.4",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "darling"
version = "0.20.11"
@@ -381,6 +777,21 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "data-encoding"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@@ -392,6 +803,27 @@ dependencies = [
"syn 2.0.117",
]
+[[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.117",
+ "unicode-xid",
+]
+
[[package]]
name = "diff"
version = "0.1.13"
@@ -412,6 +844,18 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
@@ -496,7 +940,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener 5.4.1",
+ "pin-project-lite",
]
[[package]]
@@ -505,6 +976,23 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "filetime"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -532,6 +1020,116 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[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.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
[[package]]
name = "generator"
version = "0.8.8"
@@ -555,6 +1153,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
+ "zeroize",
]
[[package]]
@@ -609,6 +1208,28 @@ dependencies = [
"wasip3",
]
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "hash32"
version = "0.2.1"
@@ -688,6 +1309,55 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -712,6 +1382,87 @@ dependencies = [
"cc",
]
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -724,6 +1475,37 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "if-addrs"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "im"
version = "15.1.0"
@@ -751,6 +1533,15 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -791,6 +1582,15 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -0,0 +1,230 @@
+//! Peer-to-peer project sync via magic-wormhole.
+//!
+//! Both peers open the same project, exchange Loro version vectors,
+//! compute deltas containing only the ops the other side lacks, then
+//! exchange and import those deltas. The result is that both docs
+//! converge to the same state without sending duplicate operations.
+
+use std::borrow::Cow;
+use std::path::Path;
+
+use anyhow::{bail, Context, Result};
+use loro::{ExportMode, VersionVector};
+use magic_wormhole::{AppConfig, AppID, Code, MailboxConnection, Wormhole};
+use serde::{Deserialize, Serialize};
+
+use crate::db;
+
+/// Custom AppID scoping our wormhole traffic away from other protocols.
+const APP_ID: &str = "td.sync.v1";
+
+/// Number of random words in the generated wormhole code.
+const CODE_WORD_COUNT: usize = 2;
+
+/// Handshake message exchanged before the delta payload.
+#[derive(Debug, Serialize, Deserialize)]
+struct SyncHandshake {
+ /// Human-readable project name.
+ project_name: String,
+ /// Stable identity (ULID stored in the doc's root meta map).
+ project_id: String,
+ /// Serialised version vector so the peer can compute a minimal delta.
+ #[serde(with = "vv_serde")]
+ version_vector: VersionVector,
+}
+
+/// Serde adapter for `VersionVector` using its postcard `encode()`/`decode()`.
+mod vv_serde {
+ use loro::VersionVector;
+ use serde::{self, Deserializer, Serializer};
+
+ pub fn serialize<S: Serializer>(vv: &VersionVector, ser: S) -> Result<S::Ok, S::Error> {
+ ser.serialize_bytes(&vv.encode())
+ }
+
+ pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<VersionVector, D::Error> {
+ let bytes: Vec<u8> = serde::Deserialize::deserialize(de)?;
+ VersionVector::decode(&bytes).map_err(serde::de::Error::custom)
+ }
+}
+
+/// Outcome of a sync exchange, returned by [`exchange`].
+pub struct SyncReport {
+ pub sent_bytes: usize,
+ pub received_bytes: usize,
+ pub imported: bool,
+}
+
+pub fn wormhole_config() -> AppConfig<serde_json::Value> {
+ AppConfig {
+ id: AppID::new(APP_ID),
+ rendezvous_url: Cow::Borrowed(magic_wormhole::rendezvous::DEFAULT_RENDEZVOUS_SERVER),
+ app_version: serde_json::json!({"v": 1}),
+ }
+}
+
+/// Run the sync protocol over an already-established wormhole.
+///
+/// Both sides call this concurrently. The protocol is symmetric: each
+/// peer sends its version vector, receives the other's, computes a
+/// minimal delta, sends it, receives the peer's delta, and imports it.
+pub async fn exchange(store: &db::Store, mut wormhole: Wormhole) -> Result<SyncReport> {
+ let my_vv = store.doc().oplog_vv();
+ let my_handshake = SyncHandshake {
+ project_name: store.project_name().to_string(),
+ project_id: read_project_id(store)?,
+ version_vector: my_vv,
+ };
+
+ // --- Phase 1: exchange handshakes ---
+ wormhole
+ .send_json(&my_handshake)
+ .await
+ .context("failed to send handshake")?;
+
+ let their_handshake: SyncHandshake = wormhole
+ .receive_json::<SyncHandshake>()
+ .await
+ .context("failed to receive handshake")?
+ .context("peer sent invalid handshake JSON")?;
+
+ if my_handshake.project_id != their_handshake.project_id {
+ let _ = wormhole.close().await;
+ bail!(
+ "project identity mismatch: local '{}' ({}) vs peer '{}' ({})",
+ my_handshake.project_name,
+ my_handshake.project_id,
+ their_handshake.project_name,
+ their_handshake.project_id,
+ );
+ }
+
+ // --- Phase 2: compute and exchange deltas ---
+ let my_delta = store
+ .doc()
+ .export(ExportMode::updates(&their_handshake.version_vector))
+ .context("failed to export delta for peer")?;
+
+ wormhole
+ .send(my_delta.clone())
+ .await
+ .context("failed to send delta")?;
+
+ let their_delta = wormhole
+ .receive()
+ .await
+ .context("failed to receive delta from peer")?;
+
+ wormhole.close().await.context("failed to close wormhole")?;
+
+ // --- Phase 3: import the peer's delta locally ---
+ let imported = if !their_delta.is_empty() {
+ store
+ .doc()
+ .import(&their_delta)
+ .context("failed to import peer delta")?;
+ store.doc().commit();
+ store.save_raw_delta(&their_delta)?;
+ true
+ } else {
+ false
+ };
+
+ Ok(SyncReport {
+ sent_bytes: my_delta.len(),
+ received_bytes: their_delta.len(),
+ imported,
+ })
+}
+
+pub fn run(root: &Path, code: Option<&str>, json: bool) -> Result<()> {
+ let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
+ rt.block_on(run_async(root, code, json))
+}
+
+async fn run_async(root: &Path, code: Option<&str>, json: bool) -> Result<()> {
+ let store = db::open(root)?;
+ let c = crate::color::stderr_theme();
+
+ let wormhole = match code {
+ None => {
+ let mailbox = MailboxConnection::create(wormhole_config(), CODE_WORD_COUNT)
+ .await
+ .context("failed to create wormhole mailbox")?;
+
+ let code = mailbox.code().clone();
+ if json {
+ println!(
+ "{}",
+ serde_json::to_string(&serde_json::json!({"code": code.to_string()}))?
+ );
+ } else {
+ eprintln!("{}wormhole:{} run on the other machine:\n", c.blue, c.reset);
+ eprintln!(" td sync {}{}{}\n", c.bold, code, c.reset);
+ eprintln!("waiting for peer...");
+ }
+
+ Wormhole::connect(mailbox)
+ .await
+ .context("wormhole key exchange failed")?
+ }
+ Some(raw) => {
+ let code: Code = raw.parse().context("invalid wormhole code")?;
+ let mailbox = MailboxConnection::connect(wormhole_config(), code, false)
+ .await
+ .context("failed to connect to wormhole mailbox")?;
+
+ if !json {
+ eprintln!("{}wormhole:{} connecting...", c.blue, c.reset);
+ }
+
+ Wormhole::connect(mailbox)
+ .await
+ .context("wormhole key exchange failed")?
+ }
+ };
+
+ if !json {
+ eprintln!("{}wormhole:{} connected, syncing...", c.blue, c.reset);
+ }
+
+ let report = exchange(&store, wormhole).await?;
+
+ if json {
+ println!(
+ "{}",
+ serde_json::to_string(&serde_json::json!({
+ "synced": true,
+ "project": store.project_name(),
+ "sent_bytes": report.sent_bytes,
+ "received_bytes": report.received_bytes,
+ }))?
+ );
+ } else {
+ eprintln!(
+ "{}synced:{} {} (sent {} bytes, received {} bytes)",
+ c.green,
+ c.reset,
+ store.project_name(),
+ report.sent_bytes,
+ report.received_bytes,
+ );
+ if report.imported {
+ eprintln!("{}info:{} imported peer changes", c.blue, c.reset);
+ } else {
+ eprintln!("{}info:{} peer had no new changes", c.blue, c.reset);
+ }
+ }
+
+ Ok(())
+}
+
+/// Read the stable project identity from the doc's root meta map.
+fn read_project_id(store: &db::Store) -> Result<String> {
+ let root = serde_json::to_value(store.doc().get_deep_value())?;
+ root.get("meta")
+ .and_then(|m| m.get("project_id"))
+ .and_then(|v| v.as_str())
+ .map(str::to_owned)
+ .ok_or_else(|| anyhow::anyhow!("missing meta.project_id in project doc"))
+}
@@ -0,0 +1,178 @@
+use assert_cmd::Command;
+use predicates::prelude::*;
+
+#[test]
+fn sync_help_shows_usage() {
+ let mut cmd = Command::cargo_bin("td").unwrap();
+ cmd.args(["sync", "--help"]);
+ cmd.assert()
+ .success()
+ .stdout(predicate::str::contains("Wormhole code"));
+}
+
+#[test]
+fn sync_invalid_code_format_fails() {
+ let home = tempfile::tempdir().unwrap();
+ let cwd = tempfile::tempdir().unwrap();
+
+ Command::cargo_bin("td")
+ .unwrap()
+ .args(["init", "synctest"])
+ .current_dir(cwd.path())
+ .env("HOME", home.path())
+ .assert()
+ .success();
+
+ let mut cmd = Command::cargo_bin("td").unwrap();
+ cmd.args(["sync", "not-a-valid-code"])
+ .current_dir(cwd.path())
+ .env("HOME", home.path());
+ cmd.assert().failure();
+}
+
+/// Two peers sync over a real wormhole connection.
+///
+/// Setup: both stores share the same project_id (simulating a project
+/// that was cloned to a second machine). Each side creates a task the
+/// other doesn't have. After sync, both should see both tasks.
+#[test]
+fn sync_exchanges_tasks_between_peers() {
+ use std::fs;
+ use yatd::db;
+
+ let home_a = tempfile::tempdir().unwrap();
+ let cwd_a = tempfile::tempdir().unwrap();
+ let home_b = tempfile::tempdir().unwrap();
+ let cwd_b = tempfile::tempdir().unwrap();
+
+ // --- Set up peer A: init a project and create a task ---
+ std::env::set_var("HOME", home_a.path());
+ let store_a = db::init(cwd_a.path(), "shared").unwrap();
+ let id_a = db::gen_id();
+ store_a
+ .apply_and_persist(|doc| {
+ let tasks = doc.get_map("tasks");
+ let task = db::insert_task_map(&tasks, &id_a)?;
+ task.insert("title", "task from A")?;
+ task.insert("description", "")?;
+ task.insert("type", "task")?;
+ task.insert("priority", "medium")?;
+ task.insert("status", "open")?;
+ task.insert("effort", "medium")?;
+ task.insert("parent", "")?;
+ task.insert("created_at", db::now_utc())?;
+ task.insert("updated_at", db::now_utc())?;
+ task.insert("deleted_at", "")?;
+ task.insert_container("labels", loro::LoroMap::new())?;
+ task.insert_container("blockers", loro::LoroMap::new())?;
+ task.insert_container("logs", loro::LoroMap::new())?;
+ Ok(())
+ })
+ .unwrap();
+
+ // --- Set up peer B: clone from A's snapshot, then add its own task ---
+ //
+ // Copy A's project directory so B has the same project_id and
+ // initial state, then create a separate device_id for B.
+ let data_a = home_a.path().join(".local/share/td/projects/shared");
+ let data_b = home_b.path().join(".local/share/td/projects/shared");
+ fs::create_dir_all(data_b.join("changes")).unwrap();
+ // Copy only the base snapshot — A's change deltas stay with A.
+ fs::copy(data_a.join("base.loro"), data_b.join("base.loro")).unwrap();
+
+ // Write a binding so db::open from cwd_b resolves to "shared".
+ let binding_dir = home_b.path().join(".local/share/td");
+ fs::create_dir_all(&binding_dir).unwrap();
+ let canonical_b = fs::canonicalize(cwd_b.path()).unwrap();
+ let bindings = serde_json::json!({
+ "bindings": {
+ canonical_b.to_string_lossy().to_string(): "shared"
+ }
+ });
+ fs::write(
+ binding_dir.join("bindings.json"),
+ serde_json::to_string_pretty(&bindings).unwrap(),
+ )
+ .unwrap();
+
+ std::env::set_var("HOME", home_b.path());
+ let store_b = db::open(cwd_b.path()).unwrap();
+ let id_b = db::gen_id();
+ store_b
+ .apply_and_persist(|doc| {
+ let tasks = doc.get_map("tasks");
+ let task = db::insert_task_map(&tasks, &id_b)?;
+ task.insert("title", "task from B")?;
+ task.insert("description", "")?;
+ task.insert("type", "task")?;
+ task.insert("priority", "high")?;
+ task.insert("status", "open")?;
+ task.insert("effort", "low")?;
+ task.insert("parent", "")?;
+ task.insert("created_at", db::now_utc())?;
+ task.insert("updated_at", db::now_utc())?;
+ task.insert("deleted_at", "")?;
+ task.insert_container("labels", loro::LoroMap::new())?;
+ task.insert_container("blockers", loro::LoroMap::new())?;
+ task.insert_container("logs", loro::LoroMap::new())?;
+ Ok(())
+ })
+ .unwrap();
+
+ // Verify pre-sync: A has 1 task, B has 2 (init snapshot + its own).
+ // A's delta hasn't been applied to B's snapshot yet, so B only sees
+ // tasks from the base snapshot plus its own delta. Meanwhile A has
+ // the base snapshot plus its own delta.
+ let a_tasks_before = store_a.list_tasks().unwrap();
+ let b_tasks_before = store_b.list_tasks().unwrap();
+ assert_eq!(a_tasks_before.len(), 1, "A should have 1 task before sync");
+ assert_eq!(b_tasks_before.len(), 1, "B should have 1 task before sync");
+
+ // --- Sync via real wormhole ---
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ rt.block_on(async {
+ use magic_wormhole::{MailboxConnection, Wormhole};
+ use yatd::cmd::sync::{exchange, wormhole_config};
+
+ // Peer A creates the mailbox.
+ let mailbox_a = MailboxConnection::create(wormhole_config(), 2)
+ .await
+ .unwrap();
+ let code = mailbox_a.code().clone();
+
+ // Peer B connects with the code.
+ let mailbox_b = MailboxConnection::connect(wormhole_config(), code, false)
+ .await
+ .unwrap();
+
+ // Both complete SPAKE2 key exchange concurrently.
+ let (wormhole_a, wormhole_b) =
+ tokio::try_join!(Wormhole::connect(mailbox_a), Wormhole::connect(mailbox_b),).unwrap();
+
+ // Run the sync protocol on both sides concurrently.
+ let (report_a, report_b) = tokio::try_join!(
+ exchange(&store_a, wormhole_a),
+ exchange(&store_b, wormhole_b),
+ )
+ .unwrap();
+
+ assert!(report_a.imported, "A should have imported B's changes");
+ assert!(report_b.imported, "B should have imported A's changes");
+ assert!(report_a.sent_bytes > 0);
+ assert!(report_b.sent_bytes > 0);
+ });
+
+ // --- Verify convergence ---
+ let a_tasks = store_a.list_tasks().unwrap();
+ let b_tasks = store_b.list_tasks().unwrap();
+
+ assert_eq!(a_tasks.len(), 2, "A should have 2 tasks after sync");
+ assert_eq!(b_tasks.len(), 2, "B should have 2 tasks after sync");
+
+ let a_titles: Vec<&str> = a_tasks.iter().map(|t| t.title.as_str()).collect();
+ let b_titles: Vec<&str> = b_tasks.iter().map(|t| t.title.as_str()).collect();
+ assert!(a_titles.contains(&"task from A"));
+ assert!(a_titles.contains(&"task from B"));
+ assert!(b_titles.contains(&"task from A"));
+ assert!(b_titles.contains(&"task from B"));
+}