···
6
-
use jacquard::client::{Agent, FileAuthStore, AgentSessionExt};
6
+
use jacquard::client::{Agent, FileAuthStore, AgentSessionExt, MemoryCredentialSession};
use jacquard::oauth::client::OAuthClient;
use jacquard::oauth::loopback::LoopbackConfig;
use jacquard::prelude::IdentityResolver;
···
use flate2::write::GzEncoder;
18
+
use futures::stream::{self, StreamExt};
···
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)
#[arg(long, default_value = "/tmp/wisp-oauth-session.json")]
40
+
/// App Password for authentication (alternative to OAuth)
42
+
password: Option<CowStr<'static>>,
async fn main() -> miette::Result<()> {
let args = Args::parse();
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
53
+
run_with_oauth(args.input, args.store, args.path, args.site).await
57
+
/// Run deployment with app password authentication
58
+
async fn run_with_app_password(
59
+
input: CowStr<'static>,
60
+
password: CowStr<'static>,
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);
68
+
let agent: Agent<_> = Agent::from(session);
69
+
deploy_site(&agent, path, site).await
72
+
/// Run deployment with OAuth authentication
73
+
async fn run_with_oauth(
74
+
input: CowStr<'static>,
77
+
site: Option<String>,
78
+
) -> miette::Result<()> {
79
+
let oauth = OAuthClient::with_default_config(FileAuthStore::new(&store));
46
-
.login_with_local_server(args.input, Default::default(), LoopbackConfig::default())
81
+
.login_with_local_server(input, Default::default(), LoopbackConfig::default())
let agent: Agent<_> = Agent::from(session);
85
+
deploy_site(&agent, path, site).await
88
+
/// Deploy the site using the provided agent
89
+
async fn deploy_site(
90
+
agent: &Agent<impl jacquard::client::AgentSession + IdentityResolver>,
92
+
site: Option<String>,
93
+
) -> miette::Result<()> {
// Verify the path exists
52
-
if !args.path.exists() {
53
-
return Err(miette::miette!("Path does not exist: {}", args.path.display()));
96
+
return Err(miette::miette!("Path does not exist: {}", path.display()));
57
-
let site_name = args.site.unwrap_or_else(|| {
100
+
let site_name = site.unwrap_or_else(|| {
.and_then(|n| n.to_str())
···
println!("Deploying site '{}'...", site_name);
68
-
let root_dir = build_directory(&agent, &args.path).await?;
111
+
let root_dir = build_directory(agent, &path).await?;
let file_count = count_files(&root_dir);
···
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<Directory<'static>>> + 'a>>
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()?;
154
+
// Separate files and directories
155
+
let mut file_tasks = Vec::new();
156
+
let mut dir_tasks = Vec::new();
107
-
for entry in std::fs::read_dir(dir_path).into_diagnostic()? {
108
-
let entry = entry.into_diagnostic()?;
158
+
for entry in dir_entries {
let name = entry.file_name();
let name_str = name.to_str()
112
-
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?;
162
+
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?
if name_str.starts_with('.') {
···
let metadata = entry.metadata().into_diagnostic()?;
173
+
file_tasks.push((name_str, path));
174
+
} else if metadata.is_dir() {
175
+
dir_tasks.push((name_str, path));
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 {
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))
.node(EntryNode::File(Box::new(file_node)))
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)))
188
+
.buffer_unordered(5)
189
+
.collect::<Vec<_>>()
192
+
.collect::<miette::Result<Vec<_>>>()?;
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)))
204
+
// Combine file and directory entries
205
+
let mut entries = file_entries;
206
+
entries.extend(dir_entries);
.r#type(CowStr::from("directory"))