+555
-5
cli/Cargo.lock
+555
-5
cli/Cargo.lock
···
174
174
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
175
175
176
176
[[package]]
177
+
name = "axum"
178
+
version = "0.7.9"
179
+
source = "registry+https://github.com/rust-lang/crates.io-index"
180
+
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
181
+
dependencies = [
182
+
"async-trait",
183
+
"axum-core",
184
+
"bytes",
185
+
"futures-util",
186
+
"http",
187
+
"http-body",
188
+
"http-body-util",
189
+
"hyper",
190
+
"hyper-util",
191
+
"itoa",
192
+
"matchit",
193
+
"memchr",
194
+
"mime",
195
+
"percent-encoding",
196
+
"pin-project-lite",
197
+
"rustversion",
198
+
"serde",
199
+
"serde_json",
200
+
"serde_path_to_error",
201
+
"serde_urlencoded",
202
+
"sync_wrapper",
203
+
"tokio",
204
+
"tower 0.5.2",
205
+
"tower-layer",
206
+
"tower-service",
207
+
"tracing",
208
+
]
209
+
210
+
[[package]]
211
+
name = "axum-core"
212
+
version = "0.4.5"
213
+
source = "registry+https://github.com/rust-lang/crates.io-index"
214
+
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
215
+
dependencies = [
216
+
"async-trait",
217
+
"bytes",
218
+
"futures-util",
219
+
"http",
220
+
"http-body",
221
+
"http-body-util",
222
+
"mime",
223
+
"pin-project-lite",
224
+
"rustversion",
225
+
"sync_wrapper",
226
+
"tower-layer",
227
+
"tower-service",
228
+
"tracing",
229
+
]
230
+
231
+
[[package]]
177
232
name = "backtrace"
178
233
version = "0.3.76"
179
234
source = "registry+https://github.com/rust-lang/crates.io-index"
···
346
401
version = "3.19.0"
347
402
source = "registry+https://github.com/rust-lang/crates.io-index"
348
403
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
404
+
405
+
[[package]]
406
+
name = "byteorder"
407
+
version = "1.5.0"
408
+
source = "registry+https://github.com/rust-lang/crates.io-index"
409
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
349
410
350
411
[[package]]
351
412
name = "bytes"
···
549
610
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
550
611
551
612
[[package]]
613
+
name = "cordyceps"
614
+
version = "0.3.4"
615
+
source = "registry+https://github.com/rust-lang/crates.io-index"
616
+
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
617
+
dependencies = [
618
+
"loom",
619
+
"tracing",
620
+
]
621
+
622
+
[[package]]
552
623
name = "core-foundation"
553
624
version = "0.9.4"
554
625
source = "registry+https://github.com/rust-lang/crates.io-index"
···
751
822
]
752
823
753
824
[[package]]
825
+
name = "derive_more"
826
+
version = "1.0.0"
827
+
source = "registry+https://github.com/rust-lang/crates.io-index"
828
+
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
829
+
dependencies = [
830
+
"derive_more-impl",
831
+
]
832
+
833
+
[[package]]
834
+
name = "derive_more-impl"
835
+
version = "1.0.0"
836
+
source = "registry+https://github.com/rust-lang/crates.io-index"
837
+
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
838
+
dependencies = [
839
+
"proc-macro2",
840
+
"quote",
841
+
"syn 2.0.108",
842
+
"unicode-xid",
843
+
]
844
+
845
+
[[package]]
846
+
name = "diatomic-waker"
847
+
version = "0.2.3"
848
+
source = "registry+https://github.com/rust-lang/crates.io-index"
849
+
checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
850
+
851
+
[[package]]
754
852
name = "digest"
755
853
version = "0.10.7"
756
854
source = "registry+https://github.com/rust-lang/crates.io-index"
···
956
1054
]
957
1055
958
1056
[[package]]
1057
+
name = "futures-buffered"
1058
+
version = "0.2.12"
1059
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1060
+
checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd"
1061
+
dependencies = [
1062
+
"cordyceps",
1063
+
"diatomic-waker",
1064
+
"futures-core",
1065
+
"pin-project-lite",
1066
+
"spin 0.10.0",
1067
+
]
1068
+
1069
+
[[package]]
959
1070
name = "futures-channel"
960
1071
version = "0.3.31"
961
1072
source = "registry+https://github.com/rust-lang/crates.io-index"
···
989
1100
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
990
1101
991
1102
[[package]]
1103
+
name = "futures-lite"
1104
+
version = "2.6.1"
1105
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1106
+
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
1107
+
dependencies = [
1108
+
"fastrand",
1109
+
"futures-core",
1110
+
"futures-io",
1111
+
"parking",
1112
+
"pin-project-lite",
1113
+
]
1114
+
1115
+
[[package]]
992
1116
name = "futures-macro"
993
1117
version = "0.3.31"
994
1118
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1027
1151
"pin-project-lite",
1028
1152
"pin-utils",
1029
1153
"slab",
1154
+
]
1155
+
1156
+
[[package]]
1157
+
name = "generator"
1158
+
version = "0.8.7"
1159
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1160
+
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
1161
+
dependencies = [
1162
+
"cc",
1163
+
"cfg-if",
1164
+
"libc",
1165
+
"log",
1166
+
"rustversion",
1167
+
"windows",
1030
1168
]
1031
1169
1032
1170
[[package]]
···
1274
1412
]
1275
1413
1276
1414
[[package]]
1415
+
name = "http-range-header"
1416
+
version = "0.4.2"
1417
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1418
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
1419
+
1420
+
[[package]]
1277
1421
name = "httparse"
1278
1422
version = "1.10.1"
1279
1423
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1299
1443
"http",
1300
1444
"http-body",
1301
1445
"httparse",
1446
+
"httpdate",
1302
1447
"itoa",
1303
1448
"pin-project-lite",
1304
1449
"pin-utils",
···
1362
1507
"js-sys",
1363
1508
"log",
1364
1509
"wasm-bindgen",
1365
-
"windows-core",
1510
+
"windows-core 0.62.2",
1366
1511
]
1367
1512
1368
1513
[[package]]
···
1635
1780
"bon",
1636
1781
"bytes",
1637
1782
"chrono",
1783
+
"ciborium",
1638
1784
"cid",
1785
+
"futures",
1639
1786
"getrandom 0.2.16",
1640
1787
"getrandom 0.3.4",
1641
1788
"http",
···
1645
1792
"miette",
1646
1793
"multibase",
1647
1794
"multihash",
1795
+
"n0-future",
1648
1796
"ouroboros",
1649
1797
"p256",
1650
1798
"rand 0.9.2",
···
1658
1806
"smol_str",
1659
1807
"thiserror 2.0.17",
1660
1808
"tokio",
1809
+
"tokio-tungstenite-wasm",
1661
1810
"tokio-util",
1662
1811
"trait-variant",
1663
1812
"url",
···
1856
2005
source = "registry+https://github.com/rust-lang/crates.io-index"
1857
2006
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
1858
2007
dependencies = [
1859
-
"spin",
2008
+
"spin 0.9.8",
1860
2009
]
1861
2010
1862
2011
[[package]]
···
1916
2065
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
1917
2066
1918
2067
[[package]]
2068
+
name = "loom"
2069
+
version = "0.7.2"
2070
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2071
+
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
2072
+
dependencies = [
2073
+
"cfg-if",
2074
+
"generator",
2075
+
"scoped-tls",
2076
+
"tracing",
2077
+
"tracing-subscriber",
2078
+
]
2079
+
2080
+
[[package]]
1919
2081
name = "lru-cache"
1920
2082
version = "0.1.2"
1921
2083
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1974
2136
]
1975
2137
1976
2138
[[package]]
2139
+
name = "matchers"
2140
+
version = "0.2.0"
2141
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2142
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
2143
+
dependencies = [
2144
+
"regex-automata",
2145
+
]
2146
+
2147
+
[[package]]
2148
+
name = "matchit"
2149
+
version = "0.7.3"
2150
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2151
+
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
2152
+
2153
+
[[package]]
1977
2154
name = "memchr"
1978
2155
version = "2.7.6"
1979
2156
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2108
2285
]
2109
2286
2110
2287
[[package]]
2288
+
name = "n0-future"
2289
+
version = "0.1.3"
2290
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2291
+
checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794"
2292
+
dependencies = [
2293
+
"cfg_aliases",
2294
+
"derive_more",
2295
+
"futures-buffered",
2296
+
"futures-lite",
2297
+
"futures-util",
2298
+
"js-sys",
2299
+
"pin-project",
2300
+
"send_wrapper",
2301
+
"tokio",
2302
+
"tokio-util",
2303
+
"wasm-bindgen",
2304
+
"wasm-bindgen-futures",
2305
+
"web-time",
2306
+
]
2307
+
2308
+
[[package]]
2111
2309
name = "ndk-context"
2112
2310
version = "0.1.1"
2113
2311
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2130
2328
]
2131
2329
2132
2330
[[package]]
2331
+
name = "nu-ansi-term"
2332
+
version = "0.50.3"
2333
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2334
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
2335
+
dependencies = [
2336
+
"windows-sys 0.61.2",
2337
+
]
2338
+
2339
+
[[package]]
2133
2340
name = "num-bigint-dig"
2134
2341
version = "0.8.5"
2135
2342
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2247
2454
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
2248
2455
2249
2456
[[package]]
2457
+
name = "openssl-probe"
2458
+
version = "0.1.6"
2459
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2460
+
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
2461
+
2462
+
[[package]]
2250
2463
name = "option-ext"
2251
2464
version = "0.2.0"
2252
2465
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2305
2518
]
2306
2519
2307
2520
[[package]]
2521
+
name = "parking"
2522
+
version = "2.2.1"
2523
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2524
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
2525
+
2526
+
[[package]]
2308
2527
name = "parking_lot"
2309
2528
version = "0.12.5"
2310
2529
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2378
2597
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
2379
2598
dependencies = [
2380
2599
"siphasher",
2600
+
]
2601
+
2602
+
[[package]]
2603
+
name = "pin-project"
2604
+
version = "1.1.10"
2605
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2606
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
2607
+
dependencies = [
2608
+
"pin-project-internal",
2609
+
]
2610
+
2611
+
[[package]]
2612
+
name = "pin-project-internal"
2613
+
version = "1.1.10"
2614
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2615
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
2616
+
dependencies = [
2617
+
"proc-macro2",
2618
+
"quote",
2619
+
"syn 2.0.108",
2381
2620
]
2382
2621
2383
2622
[[package]]
···
2752
2991
"tokio",
2753
2992
"tokio-rustls",
2754
2993
"tokio-util",
2755
-
"tower",
2756
-
"tower-http",
2994
+
"tower 0.5.2",
2995
+
"tower-http 0.6.6",
2757
2996
"tower-service",
2758
2997
"url",
2759
2998
"wasm-bindgen",
···
2877
3116
]
2878
3117
2879
3118
[[package]]
3119
+
name = "rustls-native-certs"
3120
+
version = "0.8.2"
3121
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3122
+
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
3123
+
dependencies = [
3124
+
"openssl-probe",
3125
+
"rustls-pki-types",
3126
+
"schannel",
3127
+
"security-framework",
3128
+
]
3129
+
3130
+
[[package]]
2880
3131
name = "rustls-pki-types"
2881
3132
version = "1.13.0"
2882
3133
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2925
3176
]
2926
3177
2927
3178
[[package]]
3179
+
name = "schannel"
3180
+
version = "0.1.28"
3181
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3182
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
3183
+
dependencies = [
3184
+
"windows-sys 0.61.2",
3185
+
]
3186
+
3187
+
[[package]]
2928
3188
name = "schemars"
2929
3189
version = "0.9.0"
2930
3190
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2949
3209
]
2950
3210
2951
3211
[[package]]
3212
+
name = "scoped-tls"
3213
+
version = "1.0.1"
3214
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3215
+
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
3216
+
3217
+
[[package]]
2952
3218
name = "scopeguard"
2953
3219
version = "1.2.0"
2954
3220
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2969
3235
]
2970
3236
2971
3237
[[package]]
3238
+
name = "security-framework"
3239
+
version = "3.5.1"
3240
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3241
+
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
3242
+
dependencies = [
3243
+
"bitflags",
3244
+
"core-foundation 0.10.1",
3245
+
"core-foundation-sys",
3246
+
"libc",
3247
+
"security-framework-sys",
3248
+
]
3249
+
3250
+
[[package]]
3251
+
name = "security-framework-sys"
3252
+
version = "2.15.0"
3253
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3254
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
3255
+
dependencies = [
3256
+
"core-foundation-sys",
3257
+
"libc",
3258
+
]
3259
+
3260
+
[[package]]
3261
+
name = "send_wrapper"
3262
+
version = "0.6.0"
3263
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3264
+
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
3265
+
3266
+
[[package]]
2972
3267
name = "serde"
2973
3268
version = "1.0.228"
2974
3269
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3047
3342
]
3048
3343
3049
3344
[[package]]
3345
+
name = "serde_path_to_error"
3346
+
version = "0.1.20"
3347
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3348
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
3349
+
dependencies = [
3350
+
"itoa",
3351
+
"serde",
3352
+
"serde_core",
3353
+
]
3354
+
3355
+
[[package]]
3050
3356
name = "serde_repr"
3051
3357
version = "0.1.20"
3052
3358
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3101
3407
]
3102
3408
3103
3409
[[package]]
3410
+
name = "sha1"
3411
+
version = "0.10.6"
3412
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3413
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
3414
+
dependencies = [
3415
+
"cfg-if",
3416
+
"cpufeatures",
3417
+
"digest",
3418
+
]
3419
+
3420
+
[[package]]
3104
3421
name = "sha1_smol"
3105
3422
version = "1.0.1"
3106
3423
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3118
3435
]
3119
3436
3120
3437
[[package]]
3438
+
name = "sharded-slab"
3439
+
version = "0.1.7"
3440
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3441
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
3442
+
dependencies = [
3443
+
"lazy_static",
3444
+
]
3445
+
3446
+
[[package]]
3121
3447
name = "shellexpand"
3122
3448
version = "3.1.1"
3123
3449
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3210
3536
version = "0.9.8"
3211
3537
source = "registry+https://github.com/rust-lang/crates.io-index"
3212
3538
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
3539
+
3540
+
[[package]]
3541
+
name = "spin"
3542
+
version = "0.10.0"
3543
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3544
+
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
3213
3545
3214
3546
[[package]]
3215
3547
name = "spki"
···
3465
3797
]
3466
3798
3467
3799
[[package]]
3800
+
name = "thread_local"
3801
+
version = "1.1.9"
3802
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3803
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
3804
+
dependencies = [
3805
+
"cfg-if",
3806
+
]
3807
+
3808
+
[[package]]
3468
3809
name = "threadpool"
3469
3810
version = "1.8.1"
3470
3811
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3582
3923
]
3583
3924
3584
3925
[[package]]
3926
+
name = "tokio-tungstenite"
3927
+
version = "0.24.0"
3928
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3929
+
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
3930
+
dependencies = [
3931
+
"futures-util",
3932
+
"log",
3933
+
"rustls",
3934
+
"rustls-native-certs",
3935
+
"rustls-pki-types",
3936
+
"tokio",
3937
+
"tokio-rustls",
3938
+
"tungstenite",
3939
+
]
3940
+
3941
+
[[package]]
3942
+
name = "tokio-tungstenite-wasm"
3943
+
version = "0.4.0"
3944
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3945
+
checksum = "e21a5c399399c3db9f08d8297ac12b500e86bca82e930253fdc62eaf9c0de6ae"
3946
+
dependencies = [
3947
+
"futures-channel",
3948
+
"futures-util",
3949
+
"http",
3950
+
"httparse",
3951
+
"js-sys",
3952
+
"rustls",
3953
+
"thiserror 1.0.69",
3954
+
"tokio",
3955
+
"tokio-tungstenite",
3956
+
"wasm-bindgen",
3957
+
"web-sys",
3958
+
]
3959
+
3960
+
[[package]]
3585
3961
name = "tokio-util"
3586
3962
version = "0.7.16"
3587
3963
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3590
3966
"bytes",
3591
3967
"futures-core",
3592
3968
"futures-sink",
3969
+
"futures-util",
3593
3970
"pin-project-lite",
3594
3971
"tokio",
3595
3972
]
3596
3973
3597
3974
[[package]]
3598
3975
name = "tower"
3976
+
version = "0.4.13"
3977
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3978
+
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
3979
+
dependencies = [
3980
+
"tower-layer",
3981
+
"tower-service",
3982
+
"tracing",
3983
+
]
3984
+
3985
+
[[package]]
3986
+
name = "tower"
3599
3987
version = "0.5.2"
3600
3988
source = "registry+https://github.com/rust-lang/crates.io-index"
3601
3989
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
···
3607
3995
"tokio",
3608
3996
"tower-layer",
3609
3997
"tower-service",
3998
+
"tracing",
3999
+
]
4000
+
4001
+
[[package]]
4002
+
name = "tower-http"
4003
+
version = "0.5.2"
4004
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4005
+
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
4006
+
dependencies = [
4007
+
"async-compression",
4008
+
"bitflags",
4009
+
"bytes",
4010
+
"futures-core",
4011
+
"futures-util",
4012
+
"http",
4013
+
"http-body",
4014
+
"http-body-util",
4015
+
"http-range-header",
4016
+
"httpdate",
4017
+
"mime",
4018
+
"mime_guess",
4019
+
"percent-encoding",
4020
+
"pin-project-lite",
4021
+
"tokio",
4022
+
"tokio-util",
4023
+
"tower-layer",
4024
+
"tower-service",
4025
+
"tracing",
3610
4026
]
3611
4027
3612
4028
[[package]]
···
3622
4038
"http-body",
3623
4039
"iri-string",
3624
4040
"pin-project-lite",
3625
-
"tower",
4041
+
"tower 0.5.2",
3626
4042
"tower-layer",
3627
4043
"tower-service",
3628
4044
]
···
3645
4061
source = "registry+https://github.com/rust-lang/crates.io-index"
3646
4062
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
3647
4063
dependencies = [
4064
+
"log",
3648
4065
"pin-project-lite",
3649
4066
"tracing-attributes",
3650
4067
"tracing-core",
···
3668
4085
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
3669
4086
dependencies = [
3670
4087
"once_cell",
4088
+
"valuable",
4089
+
]
4090
+
4091
+
[[package]]
4092
+
name = "tracing-log"
4093
+
version = "0.2.0"
4094
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4095
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
4096
+
dependencies = [
4097
+
"log",
4098
+
"once_cell",
4099
+
"tracing-core",
4100
+
]
4101
+
4102
+
[[package]]
4103
+
name = "tracing-subscriber"
4104
+
version = "0.3.20"
4105
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4106
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
4107
+
dependencies = [
4108
+
"matchers",
4109
+
"nu-ansi-term",
4110
+
"once_cell",
4111
+
"regex-automata",
4112
+
"sharded-slab",
4113
+
"smallvec",
4114
+
"thread_local",
4115
+
"tracing",
4116
+
"tracing-core",
4117
+
"tracing-log",
3671
4118
]
3672
4119
3673
4120
[[package]]
···
3694
4141
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
3695
4142
3696
4143
[[package]]
4144
+
name = "tungstenite"
4145
+
version = "0.24.0"
4146
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4147
+
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
4148
+
dependencies = [
4149
+
"byteorder",
4150
+
"bytes",
4151
+
"data-encoding",
4152
+
"http",
4153
+
"httparse",
4154
+
"log",
4155
+
"rand 0.8.5",
4156
+
"rustls",
4157
+
"rustls-pki-types",
4158
+
"sha1",
4159
+
"thiserror 1.0.69",
4160
+
"utf-8",
4161
+
]
4162
+
4163
+
[[package]]
3697
4164
name = "twoway"
3698
4165
version = "0.1.8"
3699
4166
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3745
4212
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
3746
4213
3747
4214
[[package]]
4215
+
name = "unicode-xid"
4216
+
version = "0.2.6"
4217
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4218
+
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
4219
+
4220
+
[[package]]
3748
4221
name = "unsigned-varint"
3749
4222
version = "0.8.0"
3750
4223
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3791
4264
version = "0.2.2"
3792
4265
source = "registry+https://github.com/rust-lang/crates.io-index"
3793
4266
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
4267
+
4268
+
[[package]]
4269
+
name = "valuable"
4270
+
version = "0.1.1"
4271
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4272
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
3794
4273
3795
4274
[[package]]
3796
4275
name = "version_check"
···
3976
4455
]
3977
4456
3978
4457
[[package]]
4458
+
name = "windows"
4459
+
version = "0.61.3"
4460
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4461
+
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
4462
+
dependencies = [
4463
+
"windows-collections",
4464
+
"windows-core 0.61.2",
4465
+
"windows-future",
4466
+
"windows-link 0.1.3",
4467
+
"windows-numerics",
4468
+
]
4469
+
4470
+
[[package]]
4471
+
name = "windows-collections"
4472
+
version = "0.2.0"
4473
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4474
+
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
4475
+
dependencies = [
4476
+
"windows-core 0.61.2",
4477
+
]
4478
+
4479
+
[[package]]
4480
+
name = "windows-core"
4481
+
version = "0.61.2"
4482
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4483
+
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
4484
+
dependencies = [
4485
+
"windows-implement",
4486
+
"windows-interface",
4487
+
"windows-link 0.1.3",
4488
+
"windows-result 0.3.4",
4489
+
"windows-strings 0.4.2",
4490
+
]
4491
+
4492
+
[[package]]
3979
4493
name = "windows-core"
3980
4494
version = "0.62.2"
3981
4495
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3989
4503
]
3990
4504
3991
4505
[[package]]
4506
+
name = "windows-future"
4507
+
version = "0.2.1"
4508
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4509
+
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
4510
+
dependencies = [
4511
+
"windows-core 0.61.2",
4512
+
"windows-link 0.1.3",
4513
+
"windows-threading",
4514
+
]
4515
+
4516
+
[[package]]
3992
4517
name = "windows-implement"
3993
4518
version = "0.60.2"
3994
4519
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4023
4548
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
4024
4549
4025
4550
[[package]]
4551
+
name = "windows-numerics"
4552
+
version = "0.2.0"
4553
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4554
+
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
4555
+
dependencies = [
4556
+
"windows-core 0.61.2",
4557
+
"windows-link 0.1.3",
4558
+
]
4559
+
4560
+
[[package]]
4026
4561
name = "windows-registry"
4027
4562
version = "0.5.3"
4028
4563
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4175
4710
"windows_x86_64_gnu 0.53.1",
4176
4711
"windows_x86_64_gnullvm 0.53.1",
4177
4712
"windows_x86_64_msvc 0.53.1",
4713
+
]
4714
+
4715
+
[[package]]
4716
+
name = "windows-threading"
4717
+
version = "0.1.0"
4718
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4719
+
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
4720
+
dependencies = [
4721
+
"windows-link 0.1.3",
4178
4722
]
4179
4723
4180
4724
[[package]]
···
4371
4915
name = "wisp-cli"
4372
4916
version = "0.1.0"
4373
4917
dependencies = [
4918
+
"axum",
4374
4919
"base64 0.22.1",
4375
4920
"bytes",
4921
+
"chrono",
4376
4922
"clap",
4377
4923
"flate2",
4378
4924
"futures",
···
4387
4933
"mime_guess",
4388
4934
"multibase",
4389
4935
"multihash",
4936
+
"n0-future",
4390
4937
"reqwest",
4391
4938
"rustversion",
4392
4939
"serde",
···
4394
4941
"sha2",
4395
4942
"shellexpand",
4396
4943
"tokio",
4944
+
"tower 0.4.13",
4945
+
"tower-http 0.5.2",
4946
+
"url",
4397
4947
"walkdir",
4398
4948
]
4399
4949
+7
-1
cli/Cargo.toml
+7
-1
cli/Cargo.toml
···
11
11
jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["loopback"] }
12
12
jacquard-oauth = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
13
13
jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
14
-
jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
14
+
jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["websocket"] }
15
15
jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["dns"] }
16
16
jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
17
17
jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
···
33
33
multihash = "0.19.3"
34
34
multibase = "0.9"
35
35
sha2 = "0.10"
36
+
axum = "0.7"
37
+
tower-http = { version = "0.5", features = ["fs", "compression-gzip"] }
38
+
tower = "0.4"
39
+
n0-future = "0.1"
40
+
chrono = "0.4"
41
+
url = "2.5"
+71
cli/src/download.rs
+71
cli/src/download.rs
···
1
+
use base64::Engine;
2
+
use bytes::Bytes;
3
+
use flate2::read::GzDecoder;
4
+
use jacquard_common::types::blob::BlobRef;
5
+
use miette::IntoDiagnostic;
6
+
use std::io::Read;
7
+
use url::Url;
8
+
9
+
/// Download a blob from the PDS
10
+
pub async fn download_blob(pds_url: &Url, blob_ref: &BlobRef<'_>, did: &str) -> miette::Result<Bytes> {
11
+
// Extract CID from blob ref
12
+
let cid = blob_ref.blob().r#ref.to_string();
13
+
14
+
// Construct blob download URL
15
+
// The correct endpoint is: /xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
16
+
let blob_url = pds_url
17
+
.join(&format!("/xrpc/com.atproto.sync.getBlob?did={}&cid={}", did, cid))
18
+
.into_diagnostic()?;
19
+
20
+
let client = reqwest::Client::new();
21
+
let response = client
22
+
.get(blob_url)
23
+
.send()
24
+
.await
25
+
.into_diagnostic()?;
26
+
27
+
if !response.status().is_success() {
28
+
return Err(miette::miette!(
29
+
"Failed to download blob: {}",
30
+
response.status()
31
+
));
32
+
}
33
+
34
+
let bytes = response.bytes().await.into_diagnostic()?;
35
+
Ok(bytes)
36
+
}
37
+
38
+
/// Decompress and decode a blob (base64 + gzip)
39
+
pub fn decompress_blob(data: &[u8], is_base64: bool, is_gzipped: bool) -> miette::Result<Vec<u8>> {
40
+
let mut current_data = data.to_vec();
41
+
42
+
// First, decode base64 if needed
43
+
if is_base64 {
44
+
current_data = base64::prelude::BASE64_STANDARD
45
+
.decode(¤t_data)
46
+
.into_diagnostic()?;
47
+
}
48
+
49
+
// Then, decompress gzip if needed
50
+
if is_gzipped {
51
+
let mut decoder = GzDecoder::new(¤t_data[..]);
52
+
let mut decompressed = Vec::new();
53
+
decoder.read_to_end(&mut decompressed).into_diagnostic()?;
54
+
current_data = decompressed;
55
+
}
56
+
57
+
Ok(current_data)
58
+
}
59
+
60
+
/// Download and decompress a blob
61
+
pub async fn download_and_decompress_blob(
62
+
pds_url: &Url,
63
+
blob_ref: &BlobRef<'_>,
64
+
did: &str,
65
+
is_base64: bool,
66
+
is_gzipped: bool,
67
+
) -> miette::Result<Vec<u8>> {
68
+
let data = download_blob(pds_url, blob_ref, did).await?;
69
+
decompress_blob(&data, is_base64, is_gzipped)
70
+
}
71
+
+109
-16
cli/src/main.rs
+109
-16
cli/src/main.rs
···
2
2
mod place_wisp;
3
3
mod cid;
4
4
mod blob_map;
5
+
mod metadata;
6
+
mod download;
7
+
mod pull;
8
+
mod serve;
5
9
6
-
use clap::Parser;
10
+
use clap::{Parser, Subcommand};
7
11
use jacquard::CowStr;
8
12
use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession, AgentSession};
9
13
use jacquard::oauth::client::OAuthClient;
···
23
27
use place_wisp::fs::*;
24
28
25
29
#[derive(Parser, Debug)]
26
-
#[command(author, version, about = "Deploy a static site to wisp.place")]
30
+
#[command(author, version, about = "wisp.place CLI tool")]
27
31
struct Args {
32
+
#[command(subcommand)]
33
+
command: Option<Commands>,
34
+
35
+
// Deploy arguments (when no subcommand is specified)
28
36
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
29
-
input: CowStr<'static>,
37
+
#[arg(global = true, conflicts_with = "command")]
38
+
input: Option<CowStr<'static>>,
30
39
31
40
/// Path to the directory containing your static site
32
-
#[arg(short, long, default_value = ".")]
33
-
path: PathBuf,
41
+
#[arg(short, long, global = true, conflicts_with = "command")]
42
+
path: Option<PathBuf>,
34
43
35
44
/// Site name (defaults to directory name)
36
-
#[arg(short, long)]
45
+
#[arg(short, long, global = true, conflicts_with = "command")]
37
46
site: Option<String>,
38
47
39
-
/// Path to auth store file (will be created if missing, only used with OAuth)
40
-
#[arg(long, default_value = "/tmp/wisp-oauth-session.json")]
41
-
store: String,
48
+
/// Path to auth store file
49
+
#[arg(long, global = true, conflicts_with = "command")]
50
+
store: Option<String>,
42
51
43
-
/// App Password for authentication (alternative to OAuth)
44
-
#[arg(long)]
52
+
/// App Password for authentication
53
+
#[arg(long, global = true, conflicts_with = "command")]
45
54
password: Option<CowStr<'static>>,
46
55
}
47
56
57
+
#[derive(Subcommand, Debug)]
58
+
enum Commands {
59
+
/// Deploy a static site to wisp.place (default command)
60
+
Deploy {
61
+
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
62
+
input: CowStr<'static>,
63
+
64
+
/// Path to the directory containing your static site
65
+
#[arg(short, long, default_value = ".")]
66
+
path: PathBuf,
67
+
68
+
/// Site name (defaults to directory name)
69
+
#[arg(short, long)]
70
+
site: Option<String>,
71
+
72
+
/// Path to auth store file (will be created if missing, only used with OAuth)
73
+
#[arg(long, default_value = "/tmp/wisp-oauth-session.json")]
74
+
store: String,
75
+
76
+
/// App Password for authentication (alternative to OAuth)
77
+
#[arg(long)]
78
+
password: Option<CowStr<'static>>,
79
+
},
80
+
/// Pull a site from the PDS to a local directory
81
+
Pull {
82
+
/// Handle (e.g., alice.bsky.social) or DID
83
+
input: CowStr<'static>,
84
+
85
+
/// Site name (record key)
86
+
#[arg(short, long)]
87
+
site: String,
88
+
89
+
/// Output directory for the downloaded site
90
+
#[arg(short, long, default_value = ".")]
91
+
output: PathBuf,
92
+
},
93
+
/// Serve a site locally with real-time firehose updates
94
+
Serve {
95
+
/// Handle (e.g., alice.bsky.social) or DID
96
+
input: CowStr<'static>,
97
+
98
+
/// Site name (record key)
99
+
#[arg(short, long)]
100
+
site: String,
101
+
102
+
/// Output directory for the site files
103
+
#[arg(short, long, default_value = ".")]
104
+
output: PathBuf,
105
+
106
+
/// Port to serve on
107
+
#[arg(short, long, default_value = "8080")]
108
+
port: u16,
109
+
},
110
+
}
111
+
48
112
#[tokio::main]
49
113
async fn main() -> miette::Result<()> {
50
114
let args = Args::parse();
51
115
52
-
// Dispatch to appropriate authentication method
53
-
if let Some(password) = args.password {
54
-
run_with_app_password(args.input, password, args.path, args.site).await
55
-
} else {
56
-
run_with_oauth(args.input, args.store, args.path, args.site).await
116
+
match args.command {
117
+
Some(Commands::Deploy { input, path, site, store, password }) => {
118
+
// Dispatch to appropriate authentication method
119
+
if let Some(password) = password {
120
+
run_with_app_password(input, password, path, site).await
121
+
} else {
122
+
run_with_oauth(input, store, path, site).await
123
+
}
124
+
}
125
+
Some(Commands::Pull { input, site, output }) => {
126
+
pull::pull_site(input, CowStr::from(site), output).await
127
+
}
128
+
Some(Commands::Serve { input, site, output, port }) => {
129
+
serve::serve_site(input, CowStr::from(site), output, port).await
130
+
}
131
+
None => {
132
+
// Legacy mode: if input is provided, assume deploy command
133
+
if let Some(input) = args.input {
134
+
let path = args.path.unwrap_or_else(|| PathBuf::from("."));
135
+
let store = args.store.unwrap_or_else(|| "/tmp/wisp-oauth-session.json".to_string());
136
+
137
+
// Dispatch to appropriate authentication method
138
+
if let Some(password) = args.password {
139
+
run_with_app_password(input, password, path, args.site).await
140
+
} else {
141
+
run_with_oauth(input, store, path, args.site).await
142
+
}
143
+
} else {
144
+
// No command and no input, show help
145
+
use clap::CommandFactory;
146
+
Args::command().print_help().into_diagnostic()?;
147
+
Ok(())
148
+
}
149
+
}
57
150
}
58
151
}
59
152
+46
cli/src/metadata.rs
+46
cli/src/metadata.rs
···
1
+
use serde::{Deserialize, Serialize};
2
+
use std::collections::HashMap;
3
+
use std::path::Path;
4
+
use miette::IntoDiagnostic;
5
+
6
+
/// Metadata tracking file CIDs for incremental updates
7
+
#[derive(Debug, Clone, Serialize, Deserialize)]
8
+
pub struct SiteMetadata {
9
+
/// Record CID from the PDS
10
+
pub record_cid: String,
11
+
/// Map of file paths to their blob CIDs
12
+
pub file_cids: HashMap<String, String>,
13
+
/// Timestamp when the site was last synced
14
+
pub last_sync: i64,
15
+
}
16
+
17
+
impl SiteMetadata {
18
+
pub fn new(record_cid: String, file_cids: HashMap<String, String>) -> Self {
19
+
Self {
20
+
record_cid,
21
+
file_cids,
22
+
last_sync: chrono::Utc::now().timestamp(),
23
+
}
24
+
}
25
+
26
+
/// Load metadata from a directory
27
+
pub fn load(dir: &Path) -> miette::Result<Option<Self>> {
28
+
let metadata_path = dir.join(".wisp-metadata.json");
29
+
if !metadata_path.exists() {
30
+
return Ok(None);
31
+
}
32
+
33
+
let contents = std::fs::read_to_string(&metadata_path).into_diagnostic()?;
34
+
let metadata: SiteMetadata = serde_json::from_str(&contents).into_diagnostic()?;
35
+
Ok(Some(metadata))
36
+
}
37
+
38
+
/// Save metadata to a directory
39
+
pub fn save(&self, dir: &Path) -> miette::Result<()> {
40
+
let metadata_path = dir.join(".wisp-metadata.json");
41
+
let contents = serde_json::to_string_pretty(self).into_diagnostic()?;
42
+
std::fs::write(&metadata_path, contents).into_diagnostic()?;
43
+
Ok(())
44
+
}
45
+
}
46
+
+305
cli/src/pull.rs
+305
cli/src/pull.rs
···
1
+
use crate::blob_map;
2
+
use crate::download;
3
+
use crate::metadata::SiteMetadata;
4
+
use crate::place_wisp::fs::*;
5
+
use jacquard::CowStr;
6
+
use jacquard::prelude::IdentityResolver;
7
+
use jacquard_common::types::string::Did;
8
+
use jacquard_common::xrpc::XrpcExt;
9
+
use jacquard_identity::PublicResolver;
10
+
use miette::IntoDiagnostic;
11
+
use std::collections::HashMap;
12
+
use std::path::{Path, PathBuf};
13
+
use url::Url;
14
+
15
+
/// Pull a site from the PDS to a local directory
16
+
pub async fn pull_site(
17
+
input: CowStr<'static>,
18
+
rkey: CowStr<'static>,
19
+
output_dir: PathBuf,
20
+
) -> miette::Result<()> {
21
+
println!("Pulling site {} from {}...", rkey, input);
22
+
23
+
// Resolve handle to DID if needed
24
+
let resolver = PublicResolver::default();
25
+
let did = if input.starts_with("did:") {
26
+
Did::new(&input).into_diagnostic()?
27
+
} else {
28
+
// It's a handle, resolve it
29
+
let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?;
30
+
resolver.resolve_handle(&handle).await.into_diagnostic()?
31
+
};
32
+
33
+
// Resolve PDS endpoint for the DID
34
+
let pds_url = resolver.pds_for_did(&did).await.into_diagnostic()?;
35
+
println!("Resolved PDS: {}", pds_url);
36
+
37
+
// Fetch the place.wisp.fs record
38
+
39
+
println!("Fetching record from PDS...");
40
+
let client = reqwest::Client::new();
41
+
42
+
// Use com.atproto.repo.getRecord
43
+
use jacquard::api::com_atproto::repo::get_record::GetRecord;
44
+
use jacquard_common::types::string::Rkey as RkeyType;
45
+
let rkey_parsed = RkeyType::new(&rkey).into_diagnostic()?;
46
+
47
+
use jacquard_common::types::ident::AtIdentifier;
48
+
use jacquard_common::types::string::RecordKey;
49
+
let request = GetRecord::new()
50
+
.repo(AtIdentifier::Did(did.clone()))
51
+
.collection(CowStr::from("place.wisp.fs"))
52
+
.rkey(RecordKey::from(rkey_parsed))
53
+
.build();
54
+
55
+
let response = client
56
+
.xrpc(pds_url.clone())
57
+
.send(&request)
58
+
.await
59
+
.into_diagnostic()?;
60
+
61
+
let record_output = response.into_output().into_diagnostic()?;
62
+
let record_cid = record_output.cid.as_ref().map(|c| c.to_string()).unwrap_or_default();
63
+
64
+
// Parse the record value as Fs
65
+
use jacquard_common::types::value::from_data;
66
+
let fs_record: Fs = from_data(&record_output.value).into_diagnostic()?;
67
+
68
+
let file_count = fs_record.file_count.map(|c| c.to_string()).unwrap_or_else(|| "?".to_string());
69
+
println!("Found site '{}' with {} files", fs_record.site, file_count);
70
+
71
+
// Load existing metadata for incremental updates
72
+
let existing_metadata = SiteMetadata::load(&output_dir)?;
73
+
let existing_file_cids = existing_metadata
74
+
.as_ref()
75
+
.map(|m| m.file_cids.clone())
76
+
.unwrap_or_default();
77
+
78
+
// Extract blob map from the new manifest
79
+
let new_blob_map = blob_map::extract_blob_map(&fs_record.root);
80
+
let new_file_cids: HashMap<String, String> = new_blob_map
81
+
.iter()
82
+
.map(|(path, (_blob_ref, cid))| (path.clone(), cid.clone()))
83
+
.collect();
84
+
85
+
// Clean up any leftover temp directories from previous failed attempts
86
+
let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new("."));
87
+
let output_name = output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy();
88
+
let temp_prefix = format!(".tmp-{}-", output_name);
89
+
90
+
if let Ok(entries) = parent.read_dir() {
91
+
for entry in entries.flatten() {
92
+
let name = entry.file_name();
93
+
if name.to_string_lossy().starts_with(&temp_prefix) {
94
+
let _ = std::fs::remove_dir_all(entry.path());
95
+
}
96
+
}
97
+
}
98
+
99
+
// Check if we need to update (but only if output directory actually exists with files)
100
+
if let Some(metadata) = &existing_metadata {
101
+
if metadata.record_cid == record_cid {
102
+
// Verify that the output directory actually exists and has content
103
+
let has_content = output_dir.exists() &&
104
+
output_dir.read_dir()
105
+
.map(|mut entries| entries.any(|e| {
106
+
if let Ok(entry) = e {
107
+
!entry.file_name().to_string_lossy().starts_with(".wisp-metadata")
108
+
} else {
109
+
false
110
+
}
111
+
}))
112
+
.unwrap_or(false);
113
+
114
+
if has_content {
115
+
println!("Site is already up to date!");
116
+
return Ok(());
117
+
}
118
+
}
119
+
}
120
+
121
+
// Create temporary directory for atomic update
122
+
// Place temp dir in parent directory to avoid issues with non-existent output_dir
123
+
let parent = output_dir.parent().unwrap_or_else(|| std::path::Path::new("."));
124
+
let temp_dir_name = format!(
125
+
".tmp-{}-{}",
126
+
output_dir.file_name().unwrap_or_else(|| std::ffi::OsStr::new("site")).to_string_lossy(),
127
+
chrono::Utc::now().timestamp()
128
+
);
129
+
let temp_dir = parent.join(temp_dir_name);
130
+
std::fs::create_dir_all(&temp_dir).into_diagnostic()?;
131
+
132
+
println!("Downloading files...");
133
+
let mut downloaded = 0;
134
+
let mut reused = 0;
135
+
136
+
// Download files recursively
137
+
let download_result = download_directory(
138
+
&fs_record.root,
139
+
&temp_dir,
140
+
&pds_url,
141
+
did.as_str(),
142
+
&new_blob_map,
143
+
&existing_file_cids,
144
+
&output_dir,
145
+
String::new(),
146
+
&mut downloaded,
147
+
&mut reused,
148
+
)
149
+
.await;
150
+
151
+
// If download failed, clean up temp directory
152
+
if let Err(e) = download_result {
153
+
let _ = std::fs::remove_dir_all(&temp_dir);
154
+
return Err(e);
155
+
}
156
+
157
+
println!(
158
+
"Downloaded {} files, reused {} files",
159
+
downloaded, reused
160
+
);
161
+
162
+
// Save metadata
163
+
let metadata = SiteMetadata::new(record_cid, new_file_cids);
164
+
metadata.save(&temp_dir)?;
165
+
166
+
// Move files from temp to output directory
167
+
let output_abs = std::fs::canonicalize(&output_dir).unwrap_or_else(|_| output_dir.clone());
168
+
let current_dir = std::env::current_dir().into_diagnostic()?;
169
+
170
+
// Special handling for pulling to current directory
171
+
if output_abs == current_dir {
172
+
// Move files from temp to current directory
173
+
for entry in std::fs::read_dir(&temp_dir).into_diagnostic()? {
174
+
let entry = entry.into_diagnostic()?;
175
+
let dest = current_dir.join(entry.file_name());
176
+
177
+
// Remove existing file/dir if it exists
178
+
if dest.exists() {
179
+
if dest.is_dir() {
180
+
std::fs::remove_dir_all(&dest).into_diagnostic()?;
181
+
} else {
182
+
std::fs::remove_file(&dest).into_diagnostic()?;
183
+
}
184
+
}
185
+
186
+
// Move from temp to current dir
187
+
std::fs::rename(entry.path(), dest).into_diagnostic()?;
188
+
}
189
+
190
+
// Clean up temp directory
191
+
std::fs::remove_dir_all(&temp_dir).into_diagnostic()?;
192
+
} else {
193
+
// If output directory exists and has content, remove it first
194
+
if output_dir.exists() {
195
+
std::fs::remove_dir_all(&output_dir).into_diagnostic()?;
196
+
}
197
+
198
+
// Ensure parent directory exists
199
+
if let Some(parent) = output_dir.parent() {
200
+
if !parent.as_os_str().is_empty() && !parent.exists() {
201
+
std::fs::create_dir_all(parent).into_diagnostic()?;
202
+
}
203
+
}
204
+
205
+
// Rename temp to final location
206
+
match std::fs::rename(&temp_dir, &output_dir) {
207
+
Ok(_) => {},
208
+
Err(e) => {
209
+
// Clean up temp directory on failure
210
+
let _ = std::fs::remove_dir_all(&temp_dir);
211
+
return Err(miette::miette!("Failed to move temp directory: {}", e));
212
+
}
213
+
}
214
+
}
215
+
216
+
println!("✓ Site pulled successfully to {}", output_dir.display());
217
+
218
+
Ok(())
219
+
}
220
+
221
+
/// Recursively download a directory
222
+
fn download_directory<'a>(
223
+
dir: &'a Directory<'_>,
224
+
output_dir: &'a Path,
225
+
pds_url: &'a Url,
226
+
did: &'a str,
227
+
new_blob_map: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
228
+
existing_file_cids: &'a HashMap<String, String>,
229
+
existing_output_dir: &'a Path,
230
+
path_prefix: String,
231
+
downloaded: &'a mut usize,
232
+
reused: &'a mut usize,
233
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send + 'a>> {
234
+
Box::pin(async move {
235
+
for entry in &dir.entries {
236
+
let entry_name = entry.name.as_str();
237
+
let current_path = if path_prefix.is_empty() {
238
+
entry_name.to_string()
239
+
} else {
240
+
format!("{}/{}", path_prefix, entry_name)
241
+
};
242
+
243
+
match &entry.node {
244
+
EntryNode::File(file) => {
245
+
let output_path = output_dir.join(entry_name);
246
+
247
+
// Check if file CID matches existing
248
+
if let Some((_blob_ref, new_cid)) = new_blob_map.get(¤t_path) {
249
+
if let Some(existing_cid) = existing_file_cids.get(¤t_path) {
250
+
if existing_cid == new_cid {
251
+
// File unchanged, copy from existing directory
252
+
let existing_path = existing_output_dir.join(¤t_path);
253
+
if existing_path.exists() {
254
+
std::fs::copy(&existing_path, &output_path).into_diagnostic()?;
255
+
*reused += 1;
256
+
println!(" ✓ Reused {}", current_path);
257
+
continue;
258
+
}
259
+
}
260
+
}
261
+
}
262
+
263
+
// File is new or changed, download it
264
+
println!(" ↓ Downloading {}", current_path);
265
+
let data = download::download_and_decompress_blob(
266
+
pds_url,
267
+
&file.blob,
268
+
did,
269
+
file.base64.unwrap_or(false),
270
+
file.encoding.as_ref().map(|e| e.as_str() == "gzip").unwrap_or(false),
271
+
)
272
+
.await?;
273
+
274
+
std::fs::write(&output_path, data).into_diagnostic()?;
275
+
*downloaded += 1;
276
+
}
277
+
EntryNode::Directory(subdir) => {
278
+
let subdir_path = output_dir.join(entry_name);
279
+
std::fs::create_dir_all(&subdir_path).into_diagnostic()?;
280
+
281
+
download_directory(
282
+
subdir,
283
+
&subdir_path,
284
+
pds_url,
285
+
did,
286
+
new_blob_map,
287
+
existing_file_cids,
288
+
existing_output_dir,
289
+
current_path,
290
+
downloaded,
291
+
reused,
292
+
)
293
+
.await?;
294
+
}
295
+
EntryNode::Unknown(_) => {
296
+
// Skip unknown node types
297
+
println!(" ⚠ Skipping unknown node type for {}", current_path);
298
+
}
299
+
}
300
+
}
301
+
302
+
Ok(())
303
+
})
304
+
}
305
+
+202
cli/src/serve.rs
+202
cli/src/serve.rs
···
1
+
use crate::pull::pull_site;
2
+
use axum::Router;
3
+
use jacquard::CowStr;
4
+
use jacquard_common::jetstream::{CommitOperation, JetstreamMessage, JetstreamParams};
5
+
use jacquard_common::types::string::Did;
6
+
use jacquard_common::xrpc::{SubscriptionClient, TungsteniteSubscriptionClient};
7
+
use miette::IntoDiagnostic;
8
+
use n0_future::StreamExt;
9
+
use std::path::PathBuf;
10
+
use std::sync::Arc;
11
+
use tokio::sync::RwLock;
12
+
use tower_http::compression::CompressionLayer;
13
+
use tower_http::services::ServeDir;
14
+
use url::Url;
15
+
16
+
/// Shared state for the server
17
+
#[derive(Clone)]
18
+
struct ServerState {
19
+
did: CowStr<'static>,
20
+
rkey: CowStr<'static>,
21
+
output_dir: PathBuf,
22
+
last_cid: Arc<RwLock<Option<String>>>,
23
+
}
24
+
25
+
/// Serve a site locally with real-time firehose updates
26
+
pub async fn serve_site(
27
+
input: CowStr<'static>,
28
+
rkey: CowStr<'static>,
29
+
output_dir: PathBuf,
30
+
port: u16,
31
+
) -> miette::Result<()> {
32
+
println!("Serving site {} from {} on port {}...", rkey, input, port);
33
+
34
+
// Resolve handle to DID if needed
35
+
use jacquard_identity::PublicResolver;
36
+
use jacquard::prelude::IdentityResolver;
37
+
38
+
let resolver = PublicResolver::default();
39
+
let did = if input.starts_with("did:") {
40
+
Did::new(&input).into_diagnostic()?
41
+
} else {
42
+
// It's a handle, resolve it
43
+
let handle = jacquard_common::types::string::Handle::new(&input).into_diagnostic()?;
44
+
resolver.resolve_handle(&handle).await.into_diagnostic()?
45
+
};
46
+
47
+
println!("Resolved to DID: {}", did.as_str());
48
+
49
+
// Create output directory if it doesn't exist
50
+
std::fs::create_dir_all(&output_dir).into_diagnostic()?;
51
+
52
+
// Initial pull of the site
53
+
println!("Performing initial pull...");
54
+
let did_str = CowStr::from(did.as_str().to_string());
55
+
pull_site(did_str.clone(), rkey.clone(), output_dir.clone()).await?;
56
+
57
+
// Create shared state
58
+
let state = ServerState {
59
+
did: did_str.clone(),
60
+
rkey: rkey.clone(),
61
+
output_dir: output_dir.clone(),
62
+
last_cid: Arc::new(RwLock::new(None)),
63
+
};
64
+
65
+
// Start firehose listener in background
66
+
let firehose_state = state.clone();
67
+
tokio::spawn(async move {
68
+
if let Err(e) = watch_firehose(firehose_state).await {
69
+
eprintln!("Firehose error: {}", e);
70
+
}
71
+
});
72
+
73
+
// Create HTTP server with gzip compression
74
+
let app = Router::new()
75
+
.fallback_service(
76
+
ServeDir::new(&output_dir)
77
+
.precompressed_gzip()
78
+
)
79
+
.layer(CompressionLayer::new())
80
+
.with_state(state);
81
+
82
+
let addr = format!("0.0.0.0:{}", port);
83
+
let listener = tokio::net::TcpListener::bind(&addr)
84
+
.await
85
+
.into_diagnostic()?;
86
+
87
+
println!("\n✓ Server running at http://localhost:{}", port);
88
+
println!(" Watching for updates on the firehose...\n");
89
+
90
+
axum::serve(listener, app).await.into_diagnostic()?;
91
+
92
+
Ok(())
93
+
}
94
+
95
+
/// Watch the firehose for updates to the specific site
96
+
fn watch_firehose(state: ServerState) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send>> {
97
+
Box::pin(async move {
98
+
let jetstream_url = Url::parse("wss://jetstream1.us-east.fire.hose.cam")
99
+
.into_diagnostic()?;
100
+
101
+
println!("[Firehose] Connecting to Jetstream...");
102
+
103
+
// Create subscription client
104
+
let client = TungsteniteSubscriptionClient::from_base_uri(jetstream_url);
105
+
106
+
// Subscribe with no filters (we'll filter manually)
107
+
// Jetstream doesn't support filtering by collection in the params builder
108
+
let params = JetstreamParams::new().build();
109
+
110
+
let stream = client.subscribe(¶ms).await.into_diagnostic()?;
111
+
println!("[Firehose] Connected! Watching for updates...");
112
+
113
+
// Convert to typed message stream
114
+
let (_sink, mut messages) = stream.into_stream();
115
+
116
+
loop {
117
+
match messages.next().await {
118
+
Some(Ok(msg)) => {
119
+
if let Err(e) = handle_firehose_message(&state, msg).await {
120
+
eprintln!("[Firehose] Error handling message: {}", e);
121
+
}
122
+
}
123
+
Some(Err(e)) => {
124
+
eprintln!("[Firehose] Stream error: {}", e);
125
+
// Try to reconnect after a delay
126
+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
127
+
return Box::pin(watch_firehose(state)).await;
128
+
}
129
+
None => {
130
+
println!("[Firehose] Stream ended, reconnecting...");
131
+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
132
+
return Box::pin(watch_firehose(state)).await;
133
+
}
134
+
}
135
+
}
136
+
})
137
+
}
138
+
139
+
/// Handle a firehose message
140
+
async fn handle_firehose_message(
141
+
state: &ServerState,
142
+
msg: JetstreamMessage<'_>,
143
+
) -> miette::Result<()> {
144
+
match msg {
145
+
JetstreamMessage::Commit {
146
+
did,
147
+
commit,
148
+
..
149
+
} => {
150
+
// Check if this is our site
151
+
if did.as_str() == state.did.as_str()
152
+
&& commit.collection.as_str() == "place.wisp.fs"
153
+
&& commit.rkey.as_str() == state.rkey.as_str()
154
+
{
155
+
match commit.operation {
156
+
CommitOperation::Create | CommitOperation::Update => {
157
+
let new_cid = commit.cid.as_ref().map(|c| c.to_string());
158
+
159
+
// Check if CID changed
160
+
let should_update = {
161
+
let last_cid = state.last_cid.read().await;
162
+
new_cid != *last_cid
163
+
};
164
+
165
+
if should_update {
166
+
println!("\n[Update] Detected change to site {} (CID: {:?})", state.rkey, new_cid);
167
+
println!("[Update] Pulling latest version...");
168
+
169
+
// Pull the updated site
170
+
match pull_site(
171
+
state.did.clone(),
172
+
state.rkey.clone(),
173
+
state.output_dir.clone(),
174
+
)
175
+
.await
176
+
{
177
+
Ok(_) => {
178
+
// Update last CID
179
+
let mut last_cid = state.last_cid.write().await;
180
+
*last_cid = new_cid;
181
+
println!("[Update] ✓ Site updated successfully!\n");
182
+
}
183
+
Err(e) => {
184
+
eprintln!("[Update] Failed to pull site: {}", e);
185
+
}
186
+
}
187
+
}
188
+
}
189
+
CommitOperation::Delete => {
190
+
println!("\n[Update] Site {} was deleted", state.rkey);
191
+
}
192
+
}
193
+
}
194
+
}
195
+
_ => {
196
+
// Ignore identity and account messages
197
+
}
198
+
}
199
+
200
+
Ok(())
201
+
}
202
+