mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Add --ssh flag to arc run for Daytona SSH access
Creates SSH access after sandbox init and displays the connection command in the progress output, aligned under the sandbox detail line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6dc359f867
commit
97d370b727
6 changed files with 107 additions and 1 deletions
|
|
@ -127,6 +127,10 @@ pub struct RunArgs {
|
|||
/// Skip retro generation after the run
|
||||
#[arg(long)]
|
||||
pub no_retro: bool,
|
||||
|
||||
/// Create SSH access to the Daytona sandbox and print the connection command
|
||||
#[arg(long)]
|
||||
pub ssh: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -562,6 +566,9 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
} => {
|
||||
format!("[ASSETS_CAPTURED] node={node_id} files_copied={files_copied} total_bytes={} files_skipped={files_skipped}", HumanBytes(*total_bytes))
|
||||
}
|
||||
WorkflowRunEvent::SshAccessReady { ssh_command } => {
|
||||
format!("[SSH_ACCESS_READY] {ssh_command}")
|
||||
}
|
||||
};
|
||||
format!("{}", styles.dim.apply_to(body))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,6 +315,9 @@ impl ProgressUI {
|
|||
WorkflowRunEvent::Agent { stage, event } => {
|
||||
self.on_agent_event(stage, event);
|
||||
}
|
||||
WorkflowRunEvent::SshAccessReady { ssh_command } => {
|
||||
self.on_ssh_access_ready(ssh_command);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -378,6 +381,21 @@ impl ProgressUI {
|
|||
}
|
||||
}
|
||||
|
||||
// ── SSH access ──────────────────────────────────────────────────────
|
||||
|
||||
fn on_ssh_access_ready(&mut self, ssh_command: &str) {
|
||||
match &self.renderer {
|
||||
ProgressRenderer::Tty(tty) => {
|
||||
let bar = tty.multi.add(ProgressBar::new_spinner());
|
||||
bar.set_style(style_sandbox_detail());
|
||||
bar.finish_with_message(ssh_command.to_string());
|
||||
}
|
||||
ProgressRenderer::Plain => {
|
||||
eprintln!(" {ssh_command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Setup ───────────────────────────────────────────────────────────
|
||||
|
||||
fn on_setup_started(&mut self, command_count: usize) {
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@ pub async fn run_command(
|
|||
// Wrap emitter in Arc now so we can share it with exec env callbacks
|
||||
let emitter = Arc::new(emitter);
|
||||
|
||||
let mut daytona_sandbox_ref: Option<Arc<crate::daytona_sandbox::DaytonaSandbox>> = None;
|
||||
let sandbox: Arc<dyn Sandbox> = match sandbox_provider {
|
||||
SandboxProvider::Docker => {
|
||||
let config = DockerSandboxConfig {
|
||||
|
|
@ -436,7 +437,9 @@ pub async fn run_command(
|
|||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
let daytona_arc = Arc::new(env);
|
||||
daytona_sandbox_ref = Some(Arc::clone(&daytona_arc));
|
||||
daytona_arc
|
||||
}
|
||||
SandboxProvider::Local => {
|
||||
let mut env = LocalSandbox::new(cwd);
|
||||
|
|
@ -482,6 +485,30 @@ pub async fn run_command(
|
|||
(None, None, None)
|
||||
};
|
||||
|
||||
// Create SSH access if requested
|
||||
if args.ssh {
|
||||
if let Some(ref daytona) = daytona_sandbox_ref {
|
||||
match daytona.create_ssh_access(Some(60.0)).await {
|
||||
Ok(ssh_info) => {
|
||||
emitter.emit(&crate::event::WorkflowRunEvent::SshAccessReady {
|
||||
ssh_command: ssh_info.ssh_command,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} Failed to create SSH access: {e}",
|
||||
styles.yellow.apply_to("Warning:"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} --ssh only works with --sandbox daytona, skipping.",
|
||||
styles.yellow.apply_to("Warning:"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Run setup commands inside the sandbox (once, not per-stage)
|
||||
if !setup_commands.is_empty() {
|
||||
emitter.emit(&crate::event::WorkflowRunEvent::SetupStarted {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,17 @@ impl DaytonaSandbox {
|
|||
self.event_callback = Some(cb);
|
||||
}
|
||||
|
||||
pub async fn create_ssh_access(
|
||||
&self,
|
||||
expires_in_minutes: Option<f64>,
|
||||
) -> Result<daytona_sdk::api_types::SshAccessDto, String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
sandbox
|
||||
.create_ssh_access(expires_in_minutes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create SSH access: {e}"))
|
||||
}
|
||||
|
||||
fn emit(&self, event: SandboxEvent) {
|
||||
event.trace();
|
||||
if let Some(ref cb) = self.event_callback {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,9 @@ pub enum WorkflowRunEvent {
|
|||
total_bytes: u64,
|
||||
files_skipped: usize,
|
||||
},
|
||||
SshAccessReady {
|
||||
ssh_command: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkflowRunEvent {
|
||||
|
|
@ -444,6 +447,9 @@ impl WorkflowRunEvent {
|
|||
files_copied, total_bytes, files_skipped, "Assets captured"
|
||||
);
|
||||
}
|
||||
Self::SshAccessReady { ssh_command } => {
|
||||
info!(ssh_command, "SSH access ready");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1145,3 +1145,40 @@ async fn daytona_asset_collection() {
|
|||
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_ssh_access() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let ssh_info = env.create_ssh_access(Some(60.0)).await.unwrap();
|
||||
assert!(
|
||||
!ssh_info.ssh_command.is_empty(),
|
||||
"ssh_command should not be empty"
|
||||
);
|
||||
assert!(
|
||||
ssh_info.ssh_command.contains("ssh"),
|
||||
"ssh_command should contain 'ssh': {}",
|
||||
ssh_info.ssh_command
|
||||
);
|
||||
assert!(
|
||||
!ssh_info.token.is_empty(),
|
||||
"token should not be empty"
|
||||
);
|
||||
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_ssh_access_before_init_fails() {
|
||||
let env = create_env().await;
|
||||
|
||||
let result = env.create_ssh_access(Some(60.0)).await;
|
||||
assert!(result.is_err(), "should fail before initialize()");
|
||||
assert!(
|
||||
result.unwrap_err().contains("not initialized"),
|
||||
"error should mention not initialized"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue