From f067c053d7028e0a052950933ccbf4c40b66473f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Wed, 24 Jun 2026 15:48:49 +0200 Subject: [PATCH] feat(cli): add --output json/yaml to sandbox get, status, and sandbox create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add structured output support to three commands that previously only supported human-readable table output: - `sandbox get --output json/yaml`: emits sandbox detail including policy source, revision, and active policy content via a new `sandbox_detail_to_json` converter. - `status --output json/yaml`: emits gateway connection state via a new `status_to_json` converter with conditional fields (auth, version, error, http_status) matching the human output. - `sandbox create --output json/yaml`: emits sandbox metadata after the sandbox reaches Ready phase, suppressing spinners and ANSI chrome from stdout. Uploads and port forwarding still execute before output. Clap rejects `--output` combined with `--editor` or trailing commands. All three commands reuse the existing `OutputFormat` enum and `print_output_single` helper. Default output (no --output flag) is byte-identical to current behavior. Unit tests cover both converters. Closes #1964 Assisted-By: 🤖 Claude Code Signed-off-by: Roland Huß --- crates/openshell-cli/src/main.rs | 39 +- crates/openshell-cli/src/run.rs | 370 +++++++++++++++--- .../sandbox_create_lifecycle_integration.rs | 12 + .../sandbox_name_fallback_integration.rs | 8 +- docs/sandboxes/manage-gateways.mdx | 6 + docs/sandboxes/manage-sandboxes.mdx | 12 + 6 files changed, 380 insertions(+), 67 deletions(-) diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index fbed00e1a..60ec30d99 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -533,7 +533,11 @@ enum Commands { /// Show gateway status and information. #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] - Status, + Status { + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, + }, /// Manage inference configuration. #[command(after_help = INFERENCE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)] @@ -1336,6 +1340,10 @@ enum SandboxCommands { #[arg(long, value_parser = ["manual", "auto"], default_value = "manual")] approval_mode: String, + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with_all = ["editor", "command", "no_keep"])] + output: OutputFormat, + /// Command to run after "--" (defaults to an interactive shell). #[arg(last = true, allow_hyphen_values = true)] command: Vec, @@ -1349,8 +1357,12 @@ enum SandboxCommands { name: Option, /// Print only the active policy YAML (same policy as the default view; stdout only). - #[arg(long)] + #[arg(long, conflicts_with = "output")] policy_only: bool, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, }, /// List sandboxes. @@ -2079,11 +2091,17 @@ async fn main() -> Result<()> { // ----------------------------------------------------------- // Top-level status // ----------------------------------------------------------- - Some(Commands::Status) => { + Some(Commands::Status { output }) => { if let Ok(ctx) = resolve_gateway(&cli.gateway, &cli.gateway_endpoint) { let mut tls = tls.with_gateway_name(&ctx.name); apply_auth(&mut tls, &ctx.name); - run::gateway_status(&ctx.name, &ctx.endpoint, &tls).await?; + run::gateway_status(&ctx.name, &ctx.endpoint, output.as_str(), &tls).await?; + } else if openshell_cli::output::print_output_single( + output.as_str(), + &(), + |()| serde_json::json!({"status": "not_configured"}), + )? { + // Structured output handled. } else { println!("{}", "Gateway Status".cyan().bold()); println!(); @@ -2612,6 +2630,7 @@ async fn main() -> Result<()> { labels, envs, approval_mode, + output, command, } => { // Resolve --tty / --no-tty into an Option override. @@ -2697,6 +2716,7 @@ async fn main() -> Result<()> { &labels_map, &env_map, &approval_mode, + output.as_str(), &tls, )) .await?; @@ -2769,9 +2789,14 @@ async fn main() -> Result<()> { | SandboxCommands::Download { .. } => { unreachable!() } - SandboxCommands::Get { name, policy_only } => { + SandboxCommands::Get { + name, + policy_only, + output, + } => { let name = resolve_sandbox_name(name, &ctx.name)?; - run::sandbox_get(endpoint, &name, policy_only, &tls).await?; + run::sandbox_get(endpoint, &name, policy_only, output.as_str(), &tls) + .await?; } SandboxCommands::List { limit, @@ -3455,7 +3480,7 @@ mod tests { .expect("global gateway flag should parse with subcommands"); assert_eq!(cli.gateway.as_deref(), Some("demo")); - assert!(matches!(cli.command, Some(Commands::Status))); + assert!(matches!(cli.command, Some(Commands::Status { .. }))); } #[test] diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 1c3fd8a82..c63e0b95c 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -41,7 +41,7 @@ use openshell_core::proto::{ DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest, GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, GetGatewayConfigRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest, - GetProviderRequest, GetSandboxConfigRequest, GetSandboxLogsRequest, + GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, HealthRequest, ImportProviderProfilesRequest, LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest, @@ -505,53 +505,105 @@ fn print_sandbox_header(sandbox: &Sandbox, display: Option<&ProvisioningDisplay> /// Show gateway status. #[allow(clippy::branches_sharing_code)] -pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) -> Result<()> { - println!("{}", "Server Status".cyan().bold()); - println!(); - println!(" {} {}", "Gateway:".dimmed(), gateway_name); - println!(" {} {}", "Server:".dimmed(), server); - if tls.is_bearer_auth() { - println!(" {} Edge (bearer token)", "Auth:".dimmed()); - } - - // Try to connect and get health - match grpc_client(server, tls).await { +pub async fn gateway_status( + gateway_name: &str, + server: &str, + output: &str, + tls: &TlsOptions, +) -> Result<()> { + let is_bearer = tls.is_bearer_auth(); + + // Build status data before any output. + let (status_str, version, error, http_status): ( + &str, + Option, + Option, + Option, + ) = match grpc_client(server, tls).await { Ok(mut client) => match client.health(HealthRequest {}).await { Ok(response) => { let health = response.into_inner(); - println!(" {} {}", "Status:".dimmed(), "Connected".green()); - println!(" {} {}", "Version:".dimmed(), health.version); + ("connected", Some(health.version), None, None) } - Err(e) => { - if let Some(status) = http_health_check(server, tls).await? { - if status.is_success() { - println!(" {} {}", "Status:".dimmed(), "Connected (HTTP)".yellow()); - println!(" {} {}", "HTTP: ".dimmed(), status); - println!(" {} {}", "gRPC error:".dimmed(), e); + Err(e) => http_health_check(server, tls).await?.map_or_else( + || ("error", None, Some(e.to_string()), None), + |http| { + let hs = Some(http.to_string()); + if http.is_success() { + ("connected_http", None, Some(e.to_string()), hs) } else { - println!(" {} {}", "Status:".dimmed(), "Error".red()); - println!(" {} {}", "HTTP:".dimmed(), status); - println!(" {} {}", "gRPC error:".dimmed(), e); + ("error", None, Some(e.to_string()), hs) } + }, + ), + }, + Err(e) => http_health_check(server, tls).await?.map_or_else( + || ("disconnected", None, Some(e.to_string()), None), + |http| { + let hs = Some(http.to_string()); + if http.is_success() { + ("connected_http", None, Some(e.to_string()), hs) } else { - println!(" {} {}", "Status:".dimmed(), "Error".red()); - println!(" {} {}", "Error:".dimmed(), e); + ("disconnected", None, Some(e.to_string()), hs) } + }, + ), + }; + + let json_data = status_to_json( + gateway_name, + server, + is_bearer, + status_str, + &version, + &error, + &http_status, + ); + if crate::output::print_output_single(output, &json_data, Clone::clone)? { + return Ok(()); + } + + // Human-readable output (existing behavior). + println!("{}", "Server Status".cyan().bold()); + println!(); + println!(" {} {}", "Gateway:".dimmed(), gateway_name); + println!(" {} {}", "Server:".dimmed(), server); + if is_bearer { + println!(" {} Edge (bearer token)", "Auth:".dimmed()); + } + match status_str { + "connected" => { + println!(" {} {}", "Status:".dimmed(), "Connected".green()); + if let Some(ref v) = version { + println!(" {} {}", "Version:".dimmed(), v); } - }, - Err(e) => { - if let Some(status) = http_health_check(server, tls).await? { - if status.is_success() { - println!(" {} {}", "Status:".dimmed(), "Connected (HTTP)".yellow()); - println!(" {} {}", "HTTP:".dimmed(), status); + } + "connected_http" => { + println!(" {} {}", "Status:".dimmed(), "Connected (HTTP)".yellow()); + if let Some(ref hs) = http_status { + println!(" {} {}", "HTTP: ".dimmed(), hs); + } + if let Some(ref e) = error { + println!(" {} {}", "gRPC error:".dimmed(), e); + } + } + "error" => { + println!(" {} {}", "Status:".dimmed(), "Error".red()); + if let Some(ref hs) = http_status { + println!(" {} {}", "HTTP:".dimmed(), hs); + if let Some(ref e) = error { println!(" {} {}", "gRPC error:".dimmed(), e); - } else { - println!(" {} {}", "Status:".dimmed(), "Disconnected".red()); - println!(" {} {}", "HTTP:".dimmed(), status); - println!(" {} {}", "Error:".dimmed(), e); } - } else { - println!(" {} {}", "Status:".dimmed(), "Disconnected".red()); + } else if let Some(ref e) = error { + println!(" {} {}", "Error:".dimmed(), e); + } + } + _ => { + println!(" {} {}", "Status:".dimmed(), "Disconnected".red()); + if let Some(ref hs) = http_status { + println!(" {} {}", "HTTP:".dimmed(), hs); + } + if let Some(ref e) = error { println!(" {} {}", "Error:".dimmed(), e); } } @@ -560,6 +612,36 @@ pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) Ok(()) } +fn status_to_json( + gateway_name: &str, + server: &str, + is_bearer: bool, + status: &str, + version: &Option, + error: &Option, + http_status: &Option, +) -> serde_json::Value { + let mut obj = serde_json::json!({ + "gateway": gateway_name, + "server": server, + "status": status, + }); + let map = obj.as_object_mut().expect("json! returns object"); + if is_bearer { + map.insert("auth".into(), serde_json::json!("edge_bearer")); + } + if let Some(v) = version { + map.insert("version".into(), serde_json::json!(v)); + } + if let Some(hs) = http_status { + map.insert("http_status".into(), serde_json::json!(hs)); + } + if let Some(e) = error { + map.insert("error".into(), serde_json::json!(e)); + } + obj +} + /// Set the active gateway. pub fn gateway_use(name: &str) -> Result<()> { // Verify the gateway exists @@ -1768,6 +1850,7 @@ pub async fn sandbox_create( labels: &HashMap, environment: &HashMap, approval_mode: &str, + output: &str, tls: &TlsOptions, ) -> Result<()> { if editor.is_some() && !command.is_empty() { @@ -1917,23 +2000,30 @@ pub async fn sandbox_create( } } + let structured_output = output != "table"; + // Set up display — interactive terminals get a step-based checklist with // spinners; non-interactive (pipes / CI) get timestamped lines. - let mut display = if interactive { + // Suppress all human chrome when structured output is active. + let mut display = if interactive && !structured_output { Some(ProvisioningDisplay::new()) } else { None }; - // Print header - print_sandbox_header(&sandbox, display.as_ref()); - - // Set initial active step on the spinner. - if let Some(d) = display.as_mut() { - d.set_active_step(ProvisioningStep::RequestingSandbox); + if structured_output { + eprintln!("Provisioning sandbox (structured output on stdout)..."); } else { - let ts = format_timestamp(Duration::ZERO); - println!(" {} Requesting compute...", ts.dimmed()); + // Print header + print_sandbox_header(&sandbox, display.as_ref()); + + // Set initial active step on the spinner. + if let Some(d) = display.as_mut() { + d.set_active_step(ProvisioningStep::RequestingSandbox); + } else { + let ts = format_timestamp(Duration::ZERO); + println!(" {} Requesting compute...", ts.dimmed()); + } } // Non-interactive mode: track start time for timestamps. @@ -1967,6 +2057,7 @@ pub async fn sandbox_create( .into_inner(); let mut last_phase = sandbox.phase(); + let mut last_sandbox = sandbox.clone(); let mut last_error_reason = String::new(); let mut last_condition_message = ready_false_condition_message(sandbox.status.as_ref()); // Track whether we have seen a non-Ready phase during the watch. @@ -1996,7 +2087,9 @@ pub async fn sandbox_create( if let Some(d) = display.as_mut() { d.finish_error(&timeout_message); } - println!(); + if !structured_output { + println!(); + } return Err(miette::miette!(timeout_message)); } @@ -2015,7 +2108,9 @@ pub async fn sandbox_create( if let Some(d) = display.as_mut() { d.finish_error(&timeout_message); } - println!(); + if !structured_output { + println!(); + } return Err(miette::miette!(timeout_message)); } }; @@ -2025,6 +2120,7 @@ pub async fn sandbox_create( Some(openshell_core::proto::sandbox_stream_event::Payload::Sandbox(s)) => { let phase = SandboxPhase::try_from(s.phase()).unwrap_or(SandboxPhase::Unknown); last_phase = s.phase(); + last_sandbox = s.clone(); if let Some(message) = ready_false_condition_message(s.status.as_ref()) { last_condition_message = Some(message); } @@ -2207,6 +2303,11 @@ pub async fn sandbox_create( ); } + if structured_output { + crate::output::print_output_single(output, &last_sandbox, sandbox_to_json)?; + return Ok(()); + } + if let Some(editor) = editor { let ssh_gateway_name = effective_tls.gateway_name().unwrap_or(gateway_name); sandbox_connect_editor( @@ -2537,6 +2638,7 @@ pub async fn sandbox_get( server: &str, name: &str, policy_only: bool, + output: &str, tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; @@ -2576,6 +2678,11 @@ pub async fn sandbox_get( return Ok(()); } + let detail_json = sandbox_detail_to_json(&sandbox, &config)?; + if crate::output::print_output_single(output, &detail_json, Clone::clone)? { + return Ok(()); + } + println!("{}", "Sandbox:".cyan().bold()); println!(); let id = if sandbox.object_id().is_empty() { @@ -3364,6 +3471,48 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { }) } +fn sandbox_detail_to_json( + sandbox: &Sandbox, + config: &GetSandboxConfigResponse, +) -> Result { + let mut value = sandbox_to_json(sandbox); + let obj = value + .as_object_mut() + .expect("sandbox_to_json returns object"); + + let policy_source = if config.policy_source == PolicySource::Global as i32 { + "global" + } else { + "sandbox" + }; + obj.insert("policy_source".into(), serde_json::json!(policy_source)); + + let policy_from_global = config.policy_source == PolicySource::Global as i32; + let revision = if policy_from_global { + if config.global_policy_version > 0 { + Some(config.global_policy_version) + } else if config.version > 0 { + Some(config.version) + } else { + None + } + } else if config.version > 0 { + Some(config.version) + } else { + None + }; + obj.insert("revision".into(), serde_json::json!(revision)); + + let policy_json = match config.policy.as_ref() { + Some(p) => openshell_policy::sandbox_policy_to_json_value(p) + .wrap_err("failed to convert policy to JSON")?, + None => serde_json::Value::Null, + }; + obj.insert("policy".into(), policy_json); + + Ok(value) +} + pub async fn sandbox_provider_list(server: &str, name: &str, tls: &TlsOptions) -> Result<()> { let mut client = grpc_client(server, tls).await?; let response = client @@ -6358,10 +6507,7 @@ pub async fn gateway_settings_get(server: &str, json: bool, tls: &TlsOptions) -> Ok(()) } -fn settings_to_json_sandbox( - name: &str, - response: &openshell_core::proto::GetSandboxConfigResponse, -) -> serde_json::Value { +fn settings_to_json_sandbox(name: &str, response: &GetSandboxConfigResponse) -> serde_json::Value { let policy_source = if response.policy_source == PolicySource::Global as i32 { "global" } else { @@ -7777,10 +7923,11 @@ mod tests { PROGRESS_STEP_STARTING_SANDBOX, }; use openshell_core::proto::{ - GpuResourceRequirements, Provider, ProviderCredentialRefresh, - ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, - ProviderCredentialTokenGrant, ProviderProfile, ProviderProfileCredential, - ResourceRequirements, SandboxCondition, SandboxStatus, datamodel::v1::ObjectMeta, + GetSandboxConfigResponse, GpuResourceRequirements, PolicySource, Provider, + ProviderCredentialRefresh, ProviderCredentialRefreshStatus, + ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, ProviderProfile, + ProviderProfileCredential, ResourceRequirements, Sandbox, SandboxCondition, SandboxPhase, + SandboxStatus, datamodel::v1::ObjectMeta, }; struct EnvVarGuard { @@ -9617,4 +9764,115 @@ mod tests { "raw milliseconds field should not exist" ); } + + #[test] + fn sandbox_detail_to_json_includes_policy_fields() { + let mut sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: "sb-123".to_string(), + name: "test-sb".to_string(), + resource_version: 5, + created_at_ms: 1_609_459_200_000, + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + sandbox.set_current_policy_version(2); + + let config = GetSandboxConfigResponse { + policy_source: PolicySource::Global as i32, + global_policy_version: 3, + ..Default::default() + }; + + let json = super::sandbox_detail_to_json(&sandbox, &config).unwrap(); + + assert_eq!(json["id"], "sb-123"); + assert_eq!(json["name"], "test-sb"); + assert_eq!(json["phase"], "Ready"); + assert_eq!(json["policy_source"], "global"); + assert_eq!(json["revision"], 3); + assert!(json["policy"].is_null()); + } + + #[test] + fn sandbox_detail_to_json_sandbox_source_without_policy() { + let sandbox = Sandbox { + metadata: Some(ObjectMeta { + id: "sb-456".to_string(), + name: "no-policy-sb".to_string(), + ..Default::default() + }), + ..Default::default() + }; + let config = GetSandboxConfigResponse { + policy_source: PolicySource::Sandbox as i32, + version: 0, + ..Default::default() + }; + + let json = super::sandbox_detail_to_json(&sandbox, &config).unwrap(); + + assert_eq!(json["policy_source"], "sandbox"); + assert!(json["revision"].is_null()); + assert!(json["policy"].is_null()); + } + + #[test] + fn status_to_json_connected() { + let json = super::status_to_json( + "my-gw", + "http://127.0.0.1:8090", + false, + "connected", + &Some("1.2.3".to_string()), + &None, + &None, + ); + + assert_eq!(json["gateway"], "my-gw"); + assert_eq!(json["server"], "http://127.0.0.1:8090"); + assert_eq!(json["status"], "connected"); + assert_eq!(json["version"], "1.2.3"); + assert!(json.get("auth").is_none()); + assert!(json.get("error").is_none()); + assert!(json.get("http_status").is_none()); + } + + #[test] + fn status_to_json_disconnected_with_error() { + let json = super::status_to_json( + "broken-gw", + "http://10.0.0.1:8090", + false, + "disconnected", + &None, + &Some("connection refused".to_string()), + &None, + ); + + assert_eq!(json["status"], "disconnected"); + assert_eq!(json["error"], "connection refused"); + assert!(json.get("version").is_none()); + } + + #[test] + fn status_to_json_connected_http_with_bearer() { + let json = super::status_to_json( + "edge-gw", + "https://edge.example.com", + true, + "connected_http", + &None, + &Some("gRPC unavailable".to_string()), + &Some("200 OK".to_string()), + ); + + assert_eq!(json["status"], "connected_http"); + assert_eq!(json["auth"], "edge_bearer"); + assert_eq!(json["error"], "gRPC unavailable"); + assert_eq!(json["http_status"], "200 OK"); + assert!(json.get("version").is_none()); + } } diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 207386b84..3df22eb21 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -1129,6 +1129,7 @@ async fn sandbox_create_keeps_command_sessions_by_default() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1172,6 +1173,7 @@ async fn sandbox_create_sends_cpu_and_memory_limits_only() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1249,6 +1251,7 @@ async fn sandbox_create_sends_driver_config_json() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1415,6 +1418,7 @@ async fn sandbox_create_does_not_infer_command_providers_when_v2_enabled() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1473,6 +1477,7 @@ async fn sandbox_create_returns_vm_error_without_waiting_for_timeout() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1527,6 +1532,7 @@ async fn sandbox_create_keeps_waiting_while_vm_progress_arrives() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1573,6 +1579,7 @@ async fn sandbox_create_times_out_when_only_logs_arrive() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1615,6 +1622,7 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1661,6 +1669,7 @@ async fn sandbox_create_deletes_shell_sessions_with_no_keep() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1707,6 +1716,7 @@ async fn sandbox_create_keeps_sandbox_with_hidden_keep_flag() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1753,6 +1763,7 @@ async fn sandbox_create_keeps_sandbox_with_forwarding() { &HashMap::new(), &HashMap::new(), "manual", + "table", &tls, ) .await @@ -1905,6 +1916,7 @@ async fn sandbox_create_sends_environment_variables() { &HashMap::new(), &env_map, "manual", + "table", &tls, ) .await diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index d4052ff68..8ce8a7b09 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -610,7 +610,7 @@ async fn run_server() -> TestServer { async fn sandbox_get_sends_correct_name() { let ts = run_server().await; - run::sandbox_get(&ts.endpoint, "my-sandbox", false, &ts.tls) + run::sandbox_get(&ts.endpoint, "my-sandbox", false, "table", &ts.tls) .await .expect("sandbox_get should succeed"); @@ -627,7 +627,7 @@ async fn sandbox_get_sends_correct_name() { async fn sandbox_get_policy_only_round_trip() { let ts = run_server().await; - run::sandbox_get(&ts.endpoint, "my-sandbox", true, &ts.tls) + run::sandbox_get(&ts.endpoint, "my-sandbox", true, "table", &ts.tls) .await .expect("sandbox_get with policy_only should succeed"); @@ -653,7 +653,7 @@ async fn sandbox_get_with_persisted_last_sandbox() { assert_eq!(resolved, "persisted-sb"); // Call sandbox_get with the resolved name. - run::sandbox_get(&ts.endpoint, &resolved, false, &ts.tls) + run::sandbox_get(&ts.endpoint, &resolved, false, "table", &ts.tls) .await .expect("sandbox_get should succeed"); @@ -758,7 +758,7 @@ async fn explicit_name_takes_precedence_over_persisted() { // Persist one name, but supply a different one explicitly. save_last_sandbox("my-cluster", "old-sandbox").expect("save should succeed"); - run::sandbox_get(&ts.endpoint, "explicit-sandbox", false, &ts.tls) + run::sandbox_get(&ts.endpoint, "explicit-sandbox", false, "table", &ts.tls) .await .expect("sandbox_get should succeed"); diff --git a/docs/sandboxes/manage-gateways.mdx b/docs/sandboxes/manage-gateways.mdx index ce4496e8d..e34a3cee9 100644 --- a/docs/sandboxes/manage-gateways.mdx +++ b/docs/sandboxes/manage-gateways.mdx @@ -110,6 +110,12 @@ Use `openshell status` for a quick health check: openshell status ``` +For automation or scripting, use `--output json` or `--output yaml` to get machine-readable output: + +```shell +openshell status --output json +``` + Use `openshell gateway info` when you need the registered endpoint, gateway metadata, or compute driver details: ```shell diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index fdbf497f5..2b7f59a71 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -20,6 +20,12 @@ Create a sandbox with a single command. For example, to create a sandbox with Cl openshell sandbox create -- claude ``` +For automation, use `--output json` or `--output yaml` to get machine-readable sandbox metadata after creation: + +```shell +openshell sandbox create --output json +``` + Every sandbox requires a gateway. Register or select one before running sandbox commands: ```shell @@ -294,6 +300,12 @@ Get detailed information about a specific sandbox. The output lists **Policy sou openshell sandbox get my-sandbox ``` +For automation, use `--output json` or `--output yaml` to get machine-readable sandbox details: + +```shell +openshell sandbox get my-sandbox --output json +``` + Print only that policy YAML for scripting (same effective policy, no metadata): ```shell