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

app password

Changed files
+190 -176
cli
+94 -153
cli/Cargo.lock
··· 151 151 ] 152 152 153 153 [[package]] 154 - name = "async-lock" 155 - version = "3.4.1" 156 - source = "registry+https://github.com/rust-lang/crates.io-index" 157 - checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 158 - dependencies = [ 159 - "event-listener", 160 - "event-listener-strategy", 161 - "pin-project-lite", 162 - ] 163 - 164 - [[package]] 165 154 name = "async-trait" 166 155 version = "0.1.89" 167 156 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 548 537 checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 549 538 550 539 [[package]] 551 - name = "concurrent-queue" 552 - version = "2.5.0" 553 - source = "registry+https://github.com/rust-lang/crates.io-index" 554 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 555 - dependencies = [ 556 - "crossbeam-utils", 557 - ] 558 - 559 - [[package]] 560 540 name = "const-oid" 561 541 version = "0.9.6" 562 542 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 573 553 version = "0.9.4" 574 554 source = "registry+https://github.com/rust-lang/crates.io-index" 575 555 checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 556 + dependencies = [ 557 + "core-foundation-sys", 558 + "libc", 559 + ] 560 + 561 + [[package]] 562 + name = "core-foundation" 563 + version = "0.10.1" 564 + source = "registry+https://github.com/rust-lang/crates.io-index" 565 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 576 566 dependencies = [ 577 567 "core-foundation-sys", 578 568 "libc", ··· 621 611 ] 622 612 623 613 [[package]] 624 - name = "crossbeam-epoch" 625 - version = "0.9.18" 626 - source = "registry+https://github.com/rust-lang/crates.io-index" 627 - checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 628 - dependencies = [ 629 - "crossbeam-utils", 630 - ] 631 - 632 - [[package]] 633 614 name = "crossbeam-utils" 634 615 version = "0.8.21" 635 616 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 891 872 ] 892 873 893 874 [[package]] 894 - name = "event-listener" 895 - version = "5.4.1" 896 - source = "registry+https://github.com/rust-lang/crates.io-index" 897 - checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 898 - dependencies = [ 899 - "concurrent-queue", 900 - "parking", 901 - "pin-project-lite", 902 - ] 903 - 904 - [[package]] 905 - name = "event-listener-strategy" 906 - version = "0.5.4" 907 - source = "registry+https://github.com/rust-lang/crates.io-index" 908 - checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 909 - dependencies = [ 910 - "event-listener", 911 - "pin-project-lite", 912 - ] 913 - 914 - [[package]] 915 875 name = "fastrand" 916 876 version = "2.3.0" 917 877 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 996 956 ] 997 957 998 958 [[package]] 959 + name = "futures" 960 + version = "0.3.31" 961 + source = "registry+https://github.com/rust-lang/crates.io-index" 962 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 963 + dependencies = [ 964 + "futures-channel", 965 + "futures-core", 966 + "futures-executor", 967 + "futures-io", 968 + "futures-sink", 969 + "futures-task", 970 + "futures-util", 971 + ] 972 + 973 + [[package]] 999 974 name = "futures-channel" 1000 975 version = "0.3.31" 1001 976 source = "registry+https://github.com/rust-lang/crates.io-index" 1002 977 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 1003 978 dependencies = [ 1004 979 "futures-core", 980 + "futures-sink", 1005 981 ] 1006 982 1007 983 [[package]] ··· 1009 985 version = "0.3.31" 1010 986 source = "registry+https://github.com/rust-lang/crates.io-index" 1011 987 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 988 + 989 + [[package]] 990 + name = "futures-executor" 991 + version = "0.3.31" 992 + source = "registry+https://github.com/rust-lang/crates.io-index" 993 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 994 + dependencies = [ 995 + "futures-core", 996 + "futures-task", 997 + "futures-util", 998 + ] 1012 999 1013 1000 [[package]] 1014 1001 name = "futures-io" ··· 1045 1032 source = "registry+https://github.com/rust-lang/crates.io-index" 1046 1033 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 1047 1034 dependencies = [ 1035 + "futures-channel", 1048 1036 "futures-core", 1049 1037 "futures-io", 1050 1038 "futures-macro", ··· 1253 1241 ] 1254 1242 1255 1243 [[package]] 1256 - name = "home" 1257 - version = "0.5.12" 1258 - source = "registry+https://github.com/rust-lang/crates.io-index" 1259 - checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 1260 - dependencies = [ 1261 - "windows-sys 0.61.2", 1262 - ] 1263 - 1264 - [[package]] 1265 1244 name = "html5ever" 1266 1245 version = "0.27.0" 1267 1246 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1634 1613 1635 1614 [[package]] 1636 1615 name = "jacquard" 1637 - version = "0.8.0" 1616 + version = "0.9.0" 1638 1617 dependencies = [ 1639 1618 "bytes", 1640 1619 "getrandom 0.2.16", ··· 1661 1640 1662 1641 [[package]] 1663 1642 name = "jacquard-api" 1664 - version = "0.8.0" 1643 + version = "0.9.0" 1665 1644 dependencies = [ 1666 1645 "bon", 1667 1646 "bytes", ··· 1678 1657 1679 1658 [[package]] 1680 1659 name = "jacquard-common" 1681 - version = "0.8.0" 1660 + version = "0.9.0" 1682 1661 dependencies = [ 1683 1662 "base64 0.22.1", 1684 1663 "bon", ··· 1714 1693 1715 1694 [[package]] 1716 1695 name = "jacquard-derive" 1717 - version = "0.8.0" 1696 + version = "0.9.0" 1718 1697 dependencies = [ 1719 1698 "heck 0.5.0", 1720 1699 "jacquard-lexicon", ··· 1725 1704 1726 1705 [[package]] 1727 1706 name = "jacquard-identity" 1728 - version = "0.8.0" 1707 + version = "0.9.0" 1729 1708 dependencies = [ 1730 1709 "bon", 1731 1710 "bytes", ··· 1735 1714 "jacquard-common", 1736 1715 "jacquard-lexicon", 1737 1716 "miette", 1738 - "moka", 1717 + "mini-moka", 1739 1718 "percent-encoding", 1740 1719 "reqwest", 1741 1720 "serde", ··· 1750 1729 1751 1730 [[package]] 1752 1731 name = "jacquard-lexicon" 1753 - version = "0.8.0" 1732 + version = "0.9.0" 1754 1733 dependencies = [ 1755 1734 "cid", 1756 1735 "dashmap", ··· 1775 1754 1776 1755 [[package]] 1777 1756 name = "jacquard-oauth" 1778 - version = "0.8.0" 1757 + version = "0.9.0" 1779 1758 dependencies = [ 1780 1759 "base64 0.22.1", 1781 1760 "bytes", ··· 1982 1961 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 1983 1962 1984 1963 [[package]] 1985 - name = "malloc_buf" 1986 - version = "0.0.6" 1987 - source = "registry+https://github.com/rust-lang/crates.io-index" 1988 - checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 1989 - dependencies = [ 1990 - "libc", 1991 - ] 1992 - 1993 - [[package]] 1994 1964 name = "markup5ever" 1995 1965 version = "0.12.1" 1996 1966 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2080 2050 ] 2081 2051 2082 2052 [[package]] 2053 + name = "mini-moka" 2054 + version = "0.11.0" 2055 + source = "git+https://github.com/moka-rs/mini-moka?rev=da864e849f5d034f32e02197fee9bb5d5af36d3d#da864e849f5d034f32e02197fee9bb5d5af36d3d" 2056 + dependencies = [ 2057 + "crossbeam-channel", 2058 + "crossbeam-utils", 2059 + "dashmap", 2060 + "smallvec", 2061 + "tagptr", 2062 + "triomphe", 2063 + "web-time", 2064 + ] 2065 + 2066 + [[package]] 2083 2067 name = "minimal-lexical" 2084 2068 version = "0.2.1" 2085 2069 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2107 2091 ] 2108 2092 2109 2093 [[package]] 2110 - name = "moka" 2111 - version = "0.12.11" 2112 - source = "registry+https://github.com/rust-lang/crates.io-index" 2113 - checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" 2114 - dependencies = [ 2115 - "async-lock", 2116 - "crossbeam-channel", 2117 - "crossbeam-epoch", 2118 - "crossbeam-utils", 2119 - "equivalent", 2120 - "event-listener", 2121 - "futures-util", 2122 - "parking_lot", 2123 - "portable-atomic", 2124 - "rustc_version", 2125 - "smallvec", 2126 - "tagptr", 2127 - "uuid", 2128 - ] 2129 - 2130 - [[package]] 2131 2094 name = "multibase" 2132 2095 version = "0.9.2" 2133 2096 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2279 2242 ] 2280 2243 2281 2244 [[package]] 2282 - name = "objc" 2283 - version = "0.2.7" 2245 + name = "objc2" 2246 + version = "0.6.3" 2284 2247 source = "registry+https://github.com/rust-lang/crates.io-index" 2285 - checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 2248 + checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" 2286 2249 dependencies = [ 2287 - "malloc_buf", 2250 + "objc2-encode", 2251 + ] 2252 + 2253 + [[package]] 2254 + name = "objc2-encode" 2255 + version = "4.1.0" 2256 + source = "registry+https://github.com/rust-lang/crates.io-index" 2257 + checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 2258 + 2259 + [[package]] 2260 + name = "objc2-foundation" 2261 + version = "0.3.2" 2262 + source = "registry+https://github.com/rust-lang/crates.io-index" 2263 + checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" 2264 + dependencies = [ 2265 + "bitflags", 2266 + "objc2", 2288 2267 ] 2289 2268 2290 2269 [[package]] ··· 2411 2390 ] 2412 2391 2413 2392 [[package]] 2414 - name = "parking" 2415 - version = "2.2.1" 2416 - source = "registry+https://github.com/rust-lang/crates.io-index" 2417 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2418 - 2419 - [[package]] 2420 2393 name = "parking_lot" 2421 2394 version = "0.12.5" 2422 2395 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2530 2503 version = "0.3.32" 2531 2504 source = "registry+https://github.com/rust-lang/crates.io-index" 2532 2505 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2533 - 2534 - [[package]] 2535 - name = "portable-atomic" 2536 - version = "1.11.1" 2537 - source = "registry+https://github.com/rust-lang/crates.io-index" 2538 - checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2539 2506 2540 2507 [[package]] 2541 2508 name = "potential_utf" ··· 2774 2741 checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 2775 2742 2776 2743 [[package]] 2777 - name = "raw-window-handle" 2778 - version = "0.5.2" 2779 - source = "registry+https://github.com/rust-lang/crates.io-index" 2780 - checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 2781 - 2782 - [[package]] 2783 2744 name = "redox_syscall" 2784 2745 version = "0.5.18" 2785 2746 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2983 2944 checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2984 2945 2985 2946 [[package]] 2986 - name = "rustc_version" 2987 - version = "0.4.1" 2988 - source = "registry+https://github.com/rust-lang/crates.io-index" 2989 - checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2990 - dependencies = [ 2991 - "semver", 2992 - ] 2993 - 2994 - [[package]] 2995 2947 name = "rustix" 2996 2948 version = "1.1.2" 2997 2949 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3126 3078 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3127 3079 dependencies = [ 3128 3080 "bitflags", 3129 - "core-foundation", 3081 + "core-foundation 0.9.4", 3130 3082 "core-foundation-sys", 3131 3083 "libc", 3132 3084 "security-framework-sys", ··· 3141 3093 "core-foundation-sys", 3142 3094 "libc", 3143 3095 ] 3144 - 3145 - [[package]] 3146 - name = "semver" 3147 - version = "1.0.27" 3148 - source = "registry+https://github.com/rust-lang/crates.io-index" 3149 - checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 3150 3096 3151 3097 [[package]] 3152 3098 name = "serde" ··· 3540 3486 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3541 3487 dependencies = [ 3542 3488 "bitflags", 3543 - "core-foundation", 3489 + "core-foundation 0.9.4", 3544 3490 "system-configuration-sys", 3545 3491 ] 3546 3492 ··· 3872 3818 ] 3873 3819 3874 3820 [[package]] 3821 + name = "triomphe" 3822 + version = "0.1.15" 3823 + source = "registry+https://github.com/rust-lang/crates.io-index" 3824 + checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" 3825 + 3826 + [[package]] 3875 3827 name = "try-lock" 3876 3828 version = "0.2.5" 3877 3829 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3977 3929 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3978 3930 3979 3931 [[package]] 3980 - name = "uuid" 3981 - version = "1.18.1" 3982 - source = "registry+https://github.com/rust-lang/crates.io-index" 3983 - checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 3984 - dependencies = [ 3985 - "getrandom 0.3.4", 3986 - "js-sys", 3987 - "wasm-bindgen", 3988 - ] 3989 - 3990 - [[package]] 3991 3932 name = "vcpkg" 3992 3933 version = "0.2.15" 3993 3934 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4126 4067 4127 4068 [[package]] 4128 4069 name = "webbrowser" 4129 - version = "0.8.15" 4070 + version = "1.0.6" 4130 4071 source = "registry+https://github.com/rust-lang/crates.io-index" 4131 - checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" 4072 + checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" 4132 4073 dependencies = [ 4133 - "core-foundation", 4134 - "home", 4074 + "core-foundation 0.10.1", 4135 4075 "jni", 4136 4076 "log", 4137 4077 "ndk-context", 4138 - "objc", 4139 - "raw-window-handle", 4078 + "objc2", 4079 + "objc2-foundation", 4140 4080 "url", 4141 4081 "web-sys", 4142 4082 ] ··· 4577 4517 "bytes", 4578 4518 "clap", 4579 4519 "flate2", 4520 + "futures", 4580 4521 "jacquard", 4581 4522 "jacquard-api", 4582 4523 "jacquard-common",
+1
cli/Cargo.toml
··· 28 28 walkdir = "2.5" 29 29 mime_guess = "2.0" 30 30 bytes = "1.10" 31 + futures = "0.3.31"
+95 -23
cli/src/main.rs
··· 3 3 4 4 use clap::Parser; 5 5 use jacquard::CowStr; 6 - use jacquard::client::{Agent, FileAuthStore, AgentSessionExt}; 6 + use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession}; 7 7 use jacquard::oauth::client::OAuthClient; 8 8 use jacquard::oauth::loopback::LoopbackConfig; 9 9 use jacquard::prelude::IdentityResolver; ··· 15 15 use flate2::write::GzEncoder; 16 16 use std::io::Write; 17 17 use base64::Engine; 18 + use futures::stream::{self, StreamExt}; 18 19 19 20 use place_wisp::fs::*; 20 21 ··· 32 33 #[arg(short, long)] 33 34 site: Option<String>, 34 35 35 - /// Path to auth store file (will be created if missing) 36 + /// Path to auth store file (will be created if missing, only used with OAuth) 36 37 #[arg(long, default_value = "/tmp/wisp-oauth-session.json")] 37 38 store: String, 39 + 40 + /// App Password for authentication (alternative to OAuth) 41 + #[arg(long)] 42 + password: Option<CowStr<'static>>, 38 43 } 39 44 40 45 #[tokio::main] 41 46 async fn main() -> miette::Result<()> { 42 47 let args = Args::parse(); 43 48 44 - let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store)); 49 + // Dispatch to appropriate authentication method 50 + if let Some(password) = args.password { 51 + run_with_app_password(args.input, password, args.path, args.site).await 52 + } else { 53 + run_with_oauth(args.input, args.store, args.path, args.site).await 54 + } 55 + } 56 + 57 + /// Run deployment with app password authentication 58 + async fn run_with_app_password( 59 + input: CowStr<'static>, 60 + password: CowStr<'static>, 61 + path: PathBuf, 62 + site: Option<String>, 63 + ) -> miette::Result<()> { 64 + let (session, auth) = 65 + MemoryCredentialSession::authenticated(input, password, None).await?; 66 + println!("Signed in as {}", auth.handle); 67 + 68 + let agent: Agent<_> = Agent::from(session); 69 + deploy_site(&agent, path, site).await 70 + } 71 + 72 + /// Run deployment with OAuth authentication 73 + async fn run_with_oauth( 74 + input: CowStr<'static>, 75 + store: String, 76 + path: PathBuf, 77 + site: Option<String>, 78 + ) -> miette::Result<()> { 79 + let oauth = OAuthClient::with_default_config(FileAuthStore::new(&store)); 45 80 let session = oauth 46 - .login_with_local_server(args.input, Default::default(), LoopbackConfig::default()) 81 + .login_with_local_server(input, Default::default(), LoopbackConfig::default()) 47 82 .await?; 48 83 49 84 let agent: Agent<_> = Agent::from(session); 85 + deploy_site(&agent, path, site).await 86 + } 50 87 88 + /// Deploy the site using the provided agent 89 + async fn deploy_site( 90 + agent: &Agent<impl jacquard::client::AgentSession + IdentityResolver>, 91 + path: PathBuf, 92 + site: Option<String>, 93 + ) -> miette::Result<()> { 51 94 // Verify the path exists 52 - if !args.path.exists() { 53 - return Err(miette::miette!("Path does not exist: {}", args.path.display())); 95 + if !path.exists() { 96 + return Err(miette::miette!("Path does not exist: {}", path.display())); 54 97 } 55 98 56 99 // Get site name 57 - let site_name = args.site.unwrap_or_else(|| { 58 - args.path 100 + let site_name = site.unwrap_or_else(|| { 101 + path 59 102 .file_name() 60 103 .and_then(|n| n.to_str()) 61 104 .unwrap_or("site") ··· 65 108 println!("Deploying site '{}'...", site_name); 66 109 67 110 // Build directory tree 68 - let root_dir = build_directory(&agent, &args.path).await?; 111 + let root_dir = build_directory(agent, &path).await?; 69 112 70 113 // Count total files 71 114 let file_count = count_files(&root_dir); ··· 102 145 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<Directory<'static>>> + 'a>> 103 146 { 104 147 Box::pin(async move { 105 - let mut entries = Vec::new(); 148 + // Collect all directory entries first 149 + let dir_entries: Vec<_> = std::fs::read_dir(dir_path) 150 + .into_diagnostic()? 151 + .collect::<Result<Vec<_>, _>>() 152 + .into_diagnostic()?; 153 + 154 + // Separate files and directories 155 + let mut file_tasks = Vec::new(); 156 + let mut dir_tasks = Vec::new(); 106 157 107 - for entry in std::fs::read_dir(dir_path).into_diagnostic()? { 108 - let entry = entry.into_diagnostic()?; 158 + for entry in dir_entries { 109 159 let path = entry.path(); 110 160 let name = entry.file_name(); 111 161 let name_str = name.to_str() 112 - .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?; 162 + .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))? 163 + .to_string(); 113 164 114 165 // Skip hidden files 115 166 if name_str.starts_with('.') { ··· 119 170 let metadata = entry.metadata().into_diagnostic()?; 120 171 121 172 if metadata.is_file() { 173 + file_tasks.push((name_str, path)); 174 + } else if metadata.is_dir() { 175 + dir_tasks.push((name_str, path)); 176 + } 177 + } 178 + 179 + // Process files concurrently with a limit of 5 180 + let file_entries: Vec<Entry> = stream::iter(file_tasks) 181 + .map(|(name, path)| async move { 122 182 let file_node = process_file(agent, &path).await?; 123 - entries.push(Entry::new() 124 - .name(CowStr::from(name_str.to_string())) 183 + Ok::<_, miette::Report>(Entry::new() 184 + .name(CowStr::from(name)) 125 185 .node(EntryNode::File(Box::new(file_node))) 126 - .build()); 127 - } else if metadata.is_dir() { 128 - let subdir = build_directory(agent, &path).await?; 129 - entries.push(Entry::new() 130 - .name(CowStr::from(name_str.to_string())) 131 - .node(EntryNode::Directory(Box::new(subdir))) 132 - .build()); 133 - } 186 + .build()) 187 + }) 188 + .buffer_unordered(5) 189 + .collect::<Vec<_>>() 190 + .await 191 + .into_iter() 192 + .collect::<miette::Result<Vec<_>>>()?; 193 + 194 + // Process directories recursively (sequentially to avoid too much nesting) 195 + let mut dir_entries = Vec::new(); 196 + for (name, path) in dir_tasks { 197 + let subdir = build_directory(agent, &path).await?; 198 + dir_entries.push(Entry::new() 199 + .name(CowStr::from(name)) 200 + .node(EntryNode::Directory(Box::new(subdir))) 201 + .build()); 134 202 } 203 + 204 + // Combine file and directory entries 205 + let mut entries = file_entries; 206 + entries.extend(dir_entries); 135 207 136 208 Ok(Directory::new() 137 209 .r#type(CowStr::from("directory"))