Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/aenv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ anyhow = "1"
bytes = "1"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
clap_complete = { version = "4", features = ["unstable-dynamic"] }
crossterm = "0.28"
directories = "5"
envd = { path = "../../thirdparty/envd" }
Expand Down
18 changes: 16 additions & 2 deletions crates/aenv/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,24 @@ impl Client {
}

pub fn new(url: &str, api_key: &str) -> Result<Self> {
Self::new_with_timeouts(
url,
api_key,
Duration::from_secs(5),
Duration::from_secs(120),
)
}

pub fn new_with_timeouts(
url: &str,
api_key: &str,
connect_timeout: Duration,
request_timeout: Duration,
) -> Result<Self> {
let base = url.trim_end_matches('/').to_string();
let agent = ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(5))
.timeout(Duration::from_secs(120))
.timeout_connect(connect_timeout)
.timeout(request_timeout)
.build();
Ok(Self {
agent,
Expand Down
91 changes: 91 additions & 0 deletions crates/aenv/src/commands/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ use anyhow::Result;
use clap::Args as ClapArgs;
use clap::CommandFactory;
use clap::ValueEnum;
use clap_complete::engine::{ArgValueCandidates, CompletionCandidate};
use clap_complete::Shell as ClapShell;
use std::io::Write;
use std::time::Duration;

use crate::client::sandboxes::ListedSandbox;

const DYNAMIC_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
const DYNAMIC_REQUEST_TIMEOUT: Duration = Duration::from_secs(1);

/// Shell to generate completion for.
///
Expand Down Expand Up @@ -37,6 +44,68 @@ pub fn run(args: Args) -> Result<()> {
write_completion(args.shell, &mut std::io::stdout().lock())
}

pub fn running_sandbox_candidates() -> Vec<CompletionCandidate> {
sandbox_candidates(|state| state == Some("running"))
}
Comment on lines +47 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
These callbacks require clap_complete's dynamic completion protocol, but run still emits a static script via clap_complete::generate. A script installed with the advertised aenv completion <shell> command therefore does not invoke the binary at completion time, so these API-backed candidates will not appear. Generate the CompleteEnv registration script here (or otherwise make run use the dynamic engine) so the normal installation path activates these providers.

Suggestion:

Suggested change
pub fn running_sandbox_candidates() -> Vec<CompletionCandidate> {
sandbox_candidates(|state| state == Some("running"))
}
// Generate the shell registration through `CompleteEnv` so completion
// requests are routed back to `Cli::command` and evaluate these providers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickNYU Please check this (but ignore the suggestion). This is a real bug. Perhaps you should make write_completion write the dynamic engine's registration script.


pub fn paused_sandbox_candidates() -> Vec<CompletionCandidate> {
sandbox_candidates(|state| state == Some("paused"))
}

pub fn active_sandbox_candidates() -> Vec<CompletionCandidate> {
sandbox_candidates(|_| true)
}
Comment thread
LSX-s-Software marked this conversation as resolved.

fn sandbox_candidates<F>(state_matches: F) -> Vec<CompletionCandidate>
where
F: Fn(Option<&str>) -> bool,
{
let Ok(credentials) = crate::auth::load() else {
return Vec::new();
};
let Ok(client) = crate::client::Client::new_with_timeouts(
&credentials.url,
&credentials.api_key,
DYNAMIC_CONNECT_TIMEOUT,
DYNAMIC_REQUEST_TIMEOUT,
) else {
return Vec::new();
};
let Ok(sandboxes) = client.list_sandboxes() else {
return Vec::new();
};

let mut candidates = filter_sandboxes(sandboxes, state_matches);
candidates.sort_by(|left, right| left.sandbox_id.cmp(&right.sandbox_id));
candidates
.into_iter()
.map(|sandbox| CompletionCandidate::new(sandbox.sandbox_id))
.collect()
}

fn filter_sandboxes<I, F>(sandboxes: I, state_matches: F) -> Vec<ListedSandbox>
where
I: IntoIterator<Item = ListedSandbox>,
F: Fn(Option<&str>) -> bool,
{
sandboxes
.into_iter()
.filter(|sandbox| state_matches(sandbox.state.as_deref()))
.collect()
}

pub fn add_running_sandbox_candidates() -> ArgValueCandidates {
ArgValueCandidates::new(running_sandbox_candidates)
}

pub fn add_paused_sandbox_candidates() -> ArgValueCandidates {
ArgValueCandidates::new(paused_sandbox_candidates)
}

pub fn add_active_sandbox_candidates() -> ArgValueCandidates {
ArgValueCandidates::new(active_sandbox_candidates)
}

/// Generate the completion script for `shell` and write it to `out`.
///
/// Generation goes through an in-memory buffer first: clap_complete's
Expand All @@ -61,6 +130,28 @@ fn write_completion<W: Write>(shell: Shell, out: &mut W) -> Result<()> {
mod tests {
use super::*;

fn sandbox(id: &str, state: &str) -> ListedSandbox {
ListedSandbox {
sandbox_id: id.to_string(),
template_id: "template".to_string(),
alias: None,
state: Some(state.to_string()),
cpu_count: None,
memory_mib: None,
disk_size_mib: None,
started_at: None,
end_at: None,
}
}

#[test]
fn state_filter_keeps_only_matching_sandboxes() {
let sandboxes = [sandbox("paused", "paused"), sandbox("running", "running")];
let running = filter_sandboxes(sandboxes, |state| state == Some("running"));
assert_eq!(running.len(), 1);
assert_eq!(running[0].sandbox_id, "running");
}

fn generate_for(shell: Shell) -> String {
let mut buf = Vec::new();
write_completion(shell, &mut buf).expect("writing to a Vec cannot fail");
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const SANDBOX_LOST_TIMEOUT: Duration = Duration::from_secs(10);

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_active_sandbox_candidates())]
sandbox_id: String,
}

Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use clap::Args as ClapArgs;

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_active_sandbox_candidates())]
sandbox_id: String,
}

Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use tokio::time::timeout;
aenv download --user app --force <sandbox-id> result.txt ./result.txt")]
pub struct Args {
/// Sandbox ID
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
/// Source file or directory path inside the sandbox
remote_path: String,
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use envd::process::StartResponse;

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
/// Command and arguments to run. Flags intended for the remote command
/// that collide with aenv's own flags can be escaped with a leading `--`.
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/pause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use clap::Args as ClapArgs;

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
}

Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use clap::Args as ClapArgs;

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_paused_sandbox_candidates())]
sandbox_id: String,
/// TTL in seconds from now. Must be longer than the sandbox's current TTL.
#[arg(long, default_value_t = super::DEFAULT_TIMEOUT_SECS)]
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Args {
enum Sub {
/// Create a persistent snapshot from a running sandbox
Create {
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
/// Snapshot name or alias. If omitted, the server returns the generated snapshot ID.
#[arg(long)]
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use clap::Args as ClapArgs;

#[derive(ClapArgs)]
pub struct Args {
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
/// Seconds from now until the sandbox should expire
seconds: u32,
Expand Down
1 change: 1 addition & 0 deletions crates/aenv/src/commands/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use walkdir::WalkDir;
aenv upload --user app <sandbox-id> ./config.json config.json")]
pub struct Args {
/// Sandbox ID
#[arg(add = crate::commands::completion::add_running_sandbox_candidates())]
sandbox_id: String,
/// Local file or directory to upload
local_path: PathBuf,
Expand Down
4 changes: 3 additions & 1 deletion crates/aenv/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::CompleteEnv;

mod auth;
mod client;
Expand Down Expand Up @@ -59,6 +60,7 @@ enum Cmd {
}

fn main() -> Result<()> {
CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse();
match cli.cmd {
Cmd::Auth => commands::auth::run(),
Expand Down
Loading