From e180e1a33fcce5991c9949f4d31bd9dce68f42c1 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 15:26:19 -0500 Subject: [PATCH 1/7] feat(segkit): add doctor command with --fix for auto-remediation Replaces `segkit setup` with `segkit doctor` / `segkit doctor --fix`. Doctor checks for devbox, Homebrew, and applesimutils, and --fix auto-installs anything missing. Also adds Homebrew bin dirs to PATH during iOS plugin init so applesimutils is discoverable in --pure mode. Co-Authored-By: Claude Opus 4.6 --- plugins/ios/virtenv/scripts/init/doctor.sh | 10 +- plugins/ios/virtenv/scripts/platform/core.sh | 8 +- plugins/ios/virtenv/scripts/user/doctor.sh | 7 + segkit/src/doctor.rs | 247 +++++++++++++++++++ segkit/src/main.rs | 12 +- segkit/src/setup.rs | 55 ----- segkit/tests/cli.rs | 8 +- 7 files changed, 282 insertions(+), 65 deletions(-) create mode 100644 segkit/src/doctor.rs delete mode 100644 segkit/src/setup.rs diff --git a/plugins/ios/virtenv/scripts/init/doctor.sh b/plugins/ios/virtenv/scripts/init/doctor.sh index 99b4b98..e0410f1 100644 --- a/plugins/ios/virtenv/scripts/init/doctor.sh +++ b/plugins/ios/virtenv/scripts/init/doctor.sh @@ -44,7 +44,15 @@ else DOCTOR_CHECKS_PASSED=$((DOCTOR_CHECKS_PASSED + 1)) fi -# Check 3: Device lock file +# Check 3: applesimutils (required for Detox iOS testing) +if command -v applesimutils >/dev/null 2>&1; then + DOCTOR_CHECKS_PASSED=$((DOCTOR_CHECKS_PASSED + 1)) +else + issues+=("applesimutils not installed (run: segkit setup)") + DOCTOR_CHECKS_WARNED=$((DOCTOR_CHECKS_WARNED + 1)) +fi + +# Check 4: Device lock file config_dir=$(doctor_resolve_config_dir 2>/dev/null || echo "${IOS_CONFIG_DIR:-./devbox.d/ios}") devices_dir="${IOS_DEVICES_DIR:-${config_dir}/devices}" lock_file="${devices_dir}/devices.lock" diff --git a/plugins/ios/virtenv/scripts/platform/core.sh b/plugins/ios/virtenv/scripts/platform/core.sh index 46bf6c9..55f8e2c 100644 --- a/plugins/ios/virtenv/scripts/platform/core.sh +++ b/plugins/ios/virtenv/scripts/platform/core.sh @@ -212,11 +212,17 @@ ios_setup_native_toolchain() { done IFS="$_ntc_oifs" - # Prepend Xcode and system tool paths + # Prepend Xcode, system, and Homebrew tool paths PATH="/usr/bin:/bin:/usr/sbin:/sbin" if [ -n "${DEVELOPER_DIR:-}" ]; then PATH="$DEVELOPER_DIR/usr/bin:$PATH" fi + # Include Homebrew paths for tools like applesimutils + for _ntc_brew_dir in /opt/homebrew/bin /usr/local/bin; do + if [ -d "$_ntc_brew_dir" ]; then + PATH="$PATH:$_ntc_brew_dir" + fi + done PATH="$PATH:$_ntc_clean" export PATH fi diff --git a/plugins/ios/virtenv/scripts/user/doctor.sh b/plugins/ios/virtenv/scripts/user/doctor.sh index e0f9220..a98638e 100644 --- a/plugins/ios/virtenv/scripts/user/doctor.sh +++ b/plugins/ios/virtenv/scripts/user/doctor.sh @@ -95,6 +95,13 @@ doctor_check_command "xcodebuild" "xcodebuild" "false" doctor_check_command "xcbeautify" "xcbeautify (for pretty build output)" "false" doctor_check_command "pod" "CocoaPods" "false" +# applesimutils (required for Detox iOS testing) +if command -v applesimutils >/dev/null 2>&1; then + doctor_check_pass "applesimutils (for Detox iOS testing)" +else + doctor_check_warn "applesimutils" "Not installed. Run: segkit setup" +fi + # Check jq (needed for config operations) doctor_check_command "jq" "jq (for config operations)" "false" diff --git a/segkit/src/doctor.rs b/segkit/src/doctor.rs new file mode 100644 index 0000000..da92ea2 --- /dev/null +++ b/segkit/src/doctor.rs @@ -0,0 +1,247 @@ +use std::process::{Command, ExitCode}; + +const DEVBOX_INSTALL_URL: &str = "https://get.jetify.com/devbox"; +const HOMEBREW_INSTALL_URL: &str = + "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"; + +fn is_installed(cmd: &str) -> bool { + which::which(cmd).is_ok() +} + +fn is_macos() -> bool { + cfg!(target_os = "macos") +} + +fn homebrew_bin_dir() -> Option<&'static str> { + if !is_macos() { + return None; + } + if std::path::Path::new("/opt/homebrew/bin/brew").exists() { + Some("/opt/homebrew/bin") + } else if std::path::Path::new("/usr/local/bin/brew").exists() { + Some("/usr/local/bin") + } else { + None + } +} + +fn ensure_homebrew_in_path() { + if let Some(brew_dir) = homebrew_bin_dir() { + let path = std::env::var("PATH").unwrap_or_default(); + if !path.split(':').any(|p| p == brew_dir) { + // SAFETY: segkit is single-threaded at this point + unsafe { + std::env::set_var("PATH", format!("{}:{}", brew_dir, path)); + } + } + } +} + +// ============================================================================ +// Installation functions +// ============================================================================ + +fn install_devbox() -> Result<(), String> { + let status = if is_installed("curl") { + Command::new("sh") + .args(["-c", &format!("curl -fsSL {DEVBOX_INSTALL_URL} | bash")]) + .status() + } else if is_installed("wget") { + Command::new("sh") + .args(["-c", &format!("wget -qO- {DEVBOX_INSTALL_URL} | bash")]) + .status() + } else { + return Err("neither curl nor wget found".into()); + }; + + match status { + Ok(s) if s.success() => Ok(()), + Ok(s) => Err(format!("installer exited with code {}", s.code().unwrap_or(-1))), + Err(e) => Err(format!("failed to run installer: {e}")), + } +} + +fn install_homebrew() -> Result<(), String> { + let status = Command::new("bash") + .args([ + "-c", + &format!("NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL {HOMEBREW_INSTALL_URL})\""), + ]) + .status(); + + match status { + Ok(s) if s.success() => { + ensure_homebrew_in_path(); + Ok(()) + } + Ok(s) => Err(format!("installer exited with code {}", s.code().unwrap_or(-1))), + Err(e) => Err(format!("failed to run installer: {e}")), + } +} + +fn install_applesimutils() -> Result<(), String> { + ensure_homebrew_in_path(); + + let brew = homebrew_bin_dir() + .map(|d| format!("{}/brew", d)) + .unwrap_or_else(|| "brew".into()); + + let status = Command::new(&brew).args(["tap", "wix/brew"]).status(); + match status { + Ok(s) if !s.success() => { + return Err(format!("brew tap wix/brew failed (code {})", s.code().unwrap_or(-1))); + } + Err(e) => return Err(format!("failed to run brew tap: {e}")), + _ => {} + } + + let status = Command::new(&brew) + .args(["install", "applesimutils"]) + .status(); + + match status { + Ok(s) if s.success() => Ok(()), + Ok(s) => Err(format!("brew install failed (code {})", s.code().unwrap_or(-1))), + Err(e) => Err(format!("failed to run brew install: {e}")), + } +} + +// ============================================================================ +// Check results +// ============================================================================ + +enum CheckStatus { + Ok, + Missing, + Fixed, + Failed(String), +} + +struct CheckResult { + name: &'static str, + status: CheckStatus, +} + +// ============================================================================ +// Individual checks +// ============================================================================ + +fn check_devbox(fix: bool) -> CheckResult { + let name = "devbox"; + if is_installed("devbox") { + return CheckResult { name, status: CheckStatus::Ok }; + } + if !fix { + return CheckResult { name, status: CheckStatus::Missing }; + } + match install_devbox() { + Ok(()) if is_installed("devbox") => CheckResult { name, status: CheckStatus::Fixed }, + Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, + Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + } +} + +fn check_homebrew(fix: bool) -> CheckResult { + let name = "homebrew"; + if !is_macos() { + return CheckResult { name, status: CheckStatus::Ok }; + } + + ensure_homebrew_in_path(); + if is_installed("brew") { + return CheckResult { name, status: CheckStatus::Ok }; + } + if !fix { + return CheckResult { name, status: CheckStatus::Missing }; + } + match install_homebrew() { + Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, + Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + } +} + +fn check_applesimutils(fix: bool) -> CheckResult { + let name = "applesimutils"; + if !is_macos() { + return CheckResult { name, status: CheckStatus::Ok }; + } + + ensure_homebrew_in_path(); + if is_installed("applesimutils") { + return CheckResult { name, status: CheckStatus::Ok }; + } + if !fix { + return CheckResult { name, status: CheckStatus::Missing }; + } + if homebrew_bin_dir().is_none() { + return CheckResult { + name, + status: CheckStatus::Failed("Homebrew not available".into()), + }; + } + match install_applesimutils() { + Ok(()) => { + ensure_homebrew_in_path(); + CheckResult { name, status: CheckStatus::Fixed } + } + Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + } +} + +// ============================================================================ +// Public entry point +// ============================================================================ + +pub fn run(fix: bool) -> ExitCode { + if fix { + println!("Checking and fixing dependencies..."); + } else { + println!("Checking dependencies..."); + } + + let results = vec![ + check_devbox(fix), + check_homebrew(fix), + check_applesimutils(fix), + ]; + + let mut any_missing = false; + let mut any_fixed = false; + let mut any_failed = false; + + for r in &results { + match &r.status { + CheckStatus::Ok => println!(" \u{2713} {}", r.name), + CheckStatus::Missing => { + println!(" \u{2717} {} (not installed)", r.name); + any_missing = true; + } + CheckStatus::Fixed => { + println!(" \u{2713} {} (just installed)", r.name); + any_fixed = true; + } + CheckStatus::Failed(e) => { + println!(" \u{2717} {} — {}", r.name, e); + any_failed = true; + } + } + } + + if any_failed { + eprintln!("\nSome dependencies could not be installed."); + return ExitCode::FAILURE; + } + + if any_missing { + eprintln!("\nMissing dependencies detected. Run `segkit doctor --fix` to install them."); + return ExitCode::FAILURE; + } + + if any_fixed { + println!("\nDependencies installed. Restart your shell and retry your command."); + return ExitCode::from(2); + } + + println!("\nAll dependencies OK."); + ExitCode::SUCCESS +} diff --git a/segkit/src/main.rs b/segkit/src/main.rs index 465b825..f9cc988 100644 --- a/segkit/src/main.rs +++ b/segkit/src/main.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; use clap::{Parser, Subcommand}; mod delegate; -mod setup; +mod doctor; #[derive(Parser)] #[command(name = "segkit", version, about = "Segment SDK developer toolkit")] @@ -34,8 +34,12 @@ enum Commands { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, }, - /// Check and install required dependencies (devbox) - Setup, + /// Check environment health and report missing dependencies + Doctor { + /// Automatically install missing dependencies + #[arg(long)] + fix: bool, + }, } fn main() -> ExitCode { @@ -46,7 +50,7 @@ fn main() -> ExitCode { Some(Commands::Ios { args }) => delegate::run("ios.sh", &args), Some(Commands::Rn { args }) => delegate::run("rn.sh", &args), Some(Commands::Metro { args }) => delegate::run("metro.sh", &args), - Some(Commands::Setup) => setup::run(), + Some(Commands::Doctor { fix }) => doctor::run(fix), None => { println!("segkit {}", env!("CARGO_PKG_VERSION")); ExitCode::SUCCESS diff --git a/segkit/src/setup.rs b/segkit/src/setup.rs deleted file mode 100644 index d1041ff..0000000 --- a/segkit/src/setup.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::process::{Command, ExitCode}; - -const DEVBOX_INSTALL_URL: &str = "https://get.jetify.com/devbox"; - -fn is_installed(cmd: &str) -> bool { - which::which(cmd).is_ok() -} - -fn install_devbox() -> Result<(), String> { - eprintln!("segkit: devbox not found, installing via {DEVBOX_INSTALL_URL}"); - - let curl_available = is_installed("curl"); - let wget_available = is_installed("wget"); - - let status = if curl_available { - Command::new("sh") - .args(["-c", &format!("curl -fsSL {DEVBOX_INSTALL_URL} | bash")]) - .status() - } else if wget_available { - Command::new("sh") - .args(["-c", &format!("wget -qO- {DEVBOX_INSTALL_URL} | bash")]) - .status() - } else { - return Err("neither curl nor wget found — cannot download devbox installer".into()); - }; - - match status { - Ok(s) if s.success() => Ok(()), - Ok(s) => Err(format!( - "devbox installer exited with code {}", - s.code().unwrap_or(-1) - )), - Err(e) => Err(format!("failed to run installer: {e}")), - } -} - -pub fn run() -> ExitCode { - if is_installed("devbox") { - println!("devbox: installed"); - } else { - if let Err(e) = install_devbox() { - eprintln!("segkit: {e}"); - return ExitCode::FAILURE; - } - if !is_installed("devbox") { - eprintln!( - "segkit: devbox installed but not found on PATH — you may need to restart your shell" - ); - return ExitCode::FAILURE; - } - println!("devbox: installed"); - } - - ExitCode::SUCCESS -} diff --git a/segkit/tests/cli.rs b/segkit/tests/cli.rs index 2d3f418..c41d313 100644 --- a/segkit/tests/cli.rs +++ b/segkit/tests/cli.rs @@ -24,7 +24,7 @@ fn help_flag() { .stdout(predicate::str::contains("ios")) .stdout(predicate::str::contains("rn")) .stdout(predicate::str::contains("metro")) - .stdout(predicate::str::contains("setup")); + .stdout(predicate::str::contains("doctor")); } #[test] @@ -81,9 +81,9 @@ fn metro_subcommand_without_script_fails_gracefully() { } #[test] -fn setup_detects_devbox() { +fn doctor_checks_dependencies() { segkit() - .arg("setup") + .arg("doctor") .assert() - .stdout(predicate::str::contains("devbox:")); + .stdout(predicate::str::contains("devbox")); } From c96d4dc408ceffe0b8f39ec3c5a3a02af39c30ef Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 15:50:12 -0500 Subject: [PATCH 2/7] feat(ios): run segkit doctor --fix in init hook Ensures Homebrew and applesimutils are installed on first devbox shell entry. Subsequent runs are a no-op since deps are already present. Co-Authored-By: Claude Opus 4.6 --- plugins/ios/virtenv/scripts/init/init-hook.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/ios/virtenv/scripts/init/init-hook.sh b/plugins/ios/virtenv/scripts/init/init-hook.sh index 3088c47..1bfc6fc 100644 --- a/plugins/ios/virtenv/scripts/init/init-hook.sh +++ b/plugins/ios/virtenv/scripts/init/init-hook.sh @@ -15,6 +15,11 @@ if [ "$(uname -s)" != "Darwin" ]; then exit 0 fi +# Ensure iOS dependencies (Homebrew, applesimutils) are installed +if command -v segkit >/dev/null 2>&1; then + segkit doctor --fix 2>/dev/null || true +fi + # Find virtenv directory VIRTENV_DIR="${IOS_SCRIPTS_DIR:-}/.." if [ -z "$VIRTENV_DIR" ] || [ "$VIRTENV_DIR" = "/.." ]; then From 3c12bb8e62074235e1b99eb1251ad0856fffde40 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 15:52:29 -0500 Subject: [PATCH 3/7] fix(ios): only shim specific Homebrew binaries, not entire bin dir Symlinks only the tools we need (applesimutils) into a private brew-shims dir rather than adding all of /opt/homebrew/bin to PATH. Prevents Homebrew-installed node/python/etc from shadowing Nix packages. Co-Authored-By: Claude Opus 4.6 --- plugins/ios/virtenv/scripts/platform/core.sh | 21 +++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/ios/virtenv/scripts/platform/core.sh b/plugins/ios/virtenv/scripts/platform/core.sh index 55f8e2c..c0b845a 100644 --- a/plugins/ios/virtenv/scripts/platform/core.sh +++ b/plugins/ios/virtenv/scripts/platform/core.sh @@ -217,12 +217,23 @@ ios_setup_native_toolchain() { if [ -n "${DEVELOPER_DIR:-}" ]; then PATH="$DEVELOPER_DIR/usr/bin:$PATH" fi - # Include Homebrew paths for tools like applesimutils - for _ntc_brew_dir in /opt/homebrew/bin /usr/local/bin; do - if [ -d "$_ntc_brew_dir" ]; then - PATH="$PATH:$_ntc_brew_dir" - fi + # Add specific Homebrew-installed tools needed for iOS development. + # We symlink individual binaries into a private dir rather than adding + # all of /opt/homebrew/bin, to avoid conflicts with user-installed + # packages (e.g. node via Homebrew shadowing the Nix-provided one). + _ntc_brew_shims="${IOS_SCRIPTS_DIR:-/tmp}/../brew-shims" + mkdir -p "$_ntc_brew_shims" 2>/dev/null || true + for _ntc_tool in applesimutils; do + for _ntc_brew_dir in /opt/homebrew/bin /usr/local/bin; do + if [ -x "$_ntc_brew_dir/$_ntc_tool" ] && [ ! -e "$_ntc_brew_shims/$_ntc_tool" ]; then + ln -sf "$_ntc_brew_dir/$_ntc_tool" "$_ntc_brew_shims/$_ntc_tool" 2>/dev/null || true + break + fi + done done + if [ -d "$_ntc_brew_shims" ]; then + PATH="$PATH:$_ntc_brew_shims" + fi PATH="$PATH:$_ntc_clean" export PATH fi From 510a108cbf4191e2ebb8f0fd32cc088a5ad75dad Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 16:01:14 -0500 Subject: [PATCH 4/7] fix(segkit): silent no-op when deps OK, clear message when fixed Doctor --fix now produces no output when all dependencies are present (silent no-op for init hook). When something is installed, prints what was fixed and asks the user to retry their command. Co-Authored-By: Claude Opus 4.6 --- plugins/ios/virtenv/scripts/init/init-hook.sh | 2 +- segkit/src/doctor.rs | 44 +++++++++++++------ segkit/tests/cli.rs | 4 +- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/plugins/ios/virtenv/scripts/init/init-hook.sh b/plugins/ios/virtenv/scripts/init/init-hook.sh index 1bfc6fc..145d310 100644 --- a/plugins/ios/virtenv/scripts/init/init-hook.sh +++ b/plugins/ios/virtenv/scripts/init/init-hook.sh @@ -17,7 +17,7 @@ fi # Ensure iOS dependencies (Homebrew, applesimutils) are installed if command -v segkit >/dev/null 2>&1; then - segkit doctor --fix 2>/dev/null || true + segkit doctor --fix || true fi # Find virtenv directory diff --git a/segkit/src/doctor.rs b/segkit/src/doctor.rs index da92ea2..5d23c60 100644 --- a/segkit/src/doctor.rs +++ b/segkit/src/doctor.rs @@ -193,12 +193,6 @@ fn check_applesimutils(fix: bool) -> CheckResult { // ============================================================================ pub fn run(fix: bool) -> ExitCode { - if fix { - println!("Checking and fixing dependencies..."); - } else { - println!("Checking dependencies..."); - } - let results = vec![ check_devbox(fix), check_homebrew(fix), @@ -211,37 +205,59 @@ pub fn run(fix: bool) -> ExitCode { for r in &results { match &r.status { - CheckStatus::Ok => println!(" \u{2713} {}", r.name), + CheckStatus::Ok => {} CheckStatus::Missing => { - println!(" \u{2717} {} (not installed)", r.name); any_missing = true; } CheckStatus::Fixed => { - println!(" \u{2713} {} (just installed)", r.name); any_fixed = true; } - CheckStatus::Failed(e) => { - println!(" \u{2717} {} — {}", r.name, e); + CheckStatus::Failed(_) => { any_failed = true; } } } + // Only print output if there's something to report + if any_missing || any_fixed || any_failed { + for r in &results { + match &r.status { + CheckStatus::Ok => println!(" \u{2713} {}", r.name), + CheckStatus::Missing => { + println!(" \u{2717} {} (not installed)", r.name); + } + CheckStatus::Fixed => { + println!(" \u{2713} {} (installed)", r.name); + } + CheckStatus::Failed(e) => { + println!(" \u{2717} {} — {}", r.name, e); + } + } + } + } + if any_failed { eprintln!("\nSome dependencies could not be installed."); return ExitCode::FAILURE; } if any_missing { - eprintln!("\nMissing dependencies detected. Run `segkit doctor --fix` to install them."); + eprintln!("\nMissing dependencies. Run `segkit doctor --fix` to install them."); return ExitCode::FAILURE; } if any_fixed { - println!("\nDependencies installed. Restart your shell and retry your command."); + let fixed_names: Vec<&str> = results + .iter() + .filter(|r| matches!(r.status, CheckStatus::Fixed)) + .map(|r| r.name) + .collect(); + eprintln!( + "\nInstalled missing dependencies: {}. Please retry your command.", + fixed_names.join(", ") + ); return ExitCode::from(2); } - println!("\nAll dependencies OK."); ExitCode::SUCCESS } diff --git a/segkit/tests/cli.rs b/segkit/tests/cli.rs index c41d313..7f3d62e 100644 --- a/segkit/tests/cli.rs +++ b/segkit/tests/cli.rs @@ -81,9 +81,9 @@ fn metro_subcommand_without_script_fails_gracefully() { } #[test] -fn doctor_checks_dependencies() { +fn doctor_succeeds_when_deps_present() { segkit() .arg("doctor") .assert() - .stdout(predicate::str::contains("devbox")); + .success(); } From f840471705a8927d7c3818def6b31caedaa413c6 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 16:06:47 -0500 Subject: [PATCH 5/7] fix(segkit): exit 0 on successful fix, print progress during installs Doctor now prints "segkit: installing X..." before each install so users know what's happening and can cancel if needed. Exits SUCCESS when deps are fixed (init hook continues). Silent no-op when all deps are already present. Co-Authored-By: Claude Opus 4.6 --- segkit/src/doctor.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/segkit/src/doctor.rs b/segkit/src/doctor.rs index 5d23c60..0ccd98d 100644 --- a/segkit/src/doctor.rs +++ b/segkit/src/doctor.rs @@ -134,8 +134,8 @@ fn check_devbox(fix: bool) -> CheckResult { if !fix { return CheckResult { name, status: CheckStatus::Missing }; } + eprintln!("segkit: installing devbox..."); match install_devbox() { - Ok(()) if is_installed("devbox") => CheckResult { name, status: CheckStatus::Fixed }, Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, } @@ -154,6 +154,7 @@ fn check_homebrew(fix: bool) -> CheckResult { if !fix { return CheckResult { name, status: CheckStatus::Missing }; } + eprintln!("segkit: installing homebrew..."); match install_homebrew() { Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, @@ -179,6 +180,7 @@ fn check_applesimutils(fix: bool) -> CheckResult { status: CheckStatus::Failed("Homebrew not available".into()), }; } + eprintln!("segkit: installing applesimutils..."); match install_applesimutils() { Ok(()) => { ensure_homebrew_in_path(); @@ -218,20 +220,17 @@ pub fn run(fix: bool) -> ExitCode { } } - // Only print output if there's something to report - if any_missing || any_fixed || any_failed { - for r in &results { - match &r.status { - CheckStatus::Ok => println!(" \u{2713} {}", r.name), - CheckStatus::Missing => { - println!(" \u{2717} {} (not installed)", r.name); - } - CheckStatus::Fixed => { - println!(" \u{2713} {} (installed)", r.name); - } - CheckStatus::Failed(e) => { - println!(" \u{2717} {} — {}", r.name, e); - } + for r in &results { + match &r.status { + CheckStatus::Ok => {} + CheckStatus::Missing => { + eprintln!(" \u{2717} {} (not installed)", r.name); + } + CheckStatus::Fixed => { + eprintln!(" \u{2713} {} (installed)", r.name); + } + CheckStatus::Failed(e) => { + eprintln!(" \u{2717} {} — {}", r.name, e); } } } @@ -253,10 +252,10 @@ pub fn run(fix: bool) -> ExitCode { .map(|r| r.name) .collect(); eprintln!( - "\nInstalled missing dependencies: {}. Please retry your command.", + "\nInstalled missing dependencies: {}.", fixed_names.join(", ") ); - return ExitCode::from(2); + return ExitCode::SUCCESS; } ExitCode::SUCCESS From 47e50317e278e9336bcbe818e91f81ee15488aa5 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 16:06:52 -0500 Subject: [PATCH 6/7] chore: update devbox.lock files with segkit flake resolution Co-Authored-By: Claude Opus 4.6 --- examples/ios/devbox.lock | 4 ++++ examples/react-native/devbox.lock | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/examples/ios/devbox.lock b/examples/ios/devbox.lock index 082ee73..2532db5 100644 --- a/examples/ios/devbox.lock +++ b/examples/ios/devbox.lock @@ -393,6 +393,10 @@ "last_modified": "2026-02-15T17:45:47Z", "resolved": "github:NixOS/nixpkgs/ac055f38c798b0d87695240c7b761b82fc7e5bc2?lastModified=1771177547" }, + "github:segment-integrations/mobile-devtools?dir=segkit&ref=main": { + "last_modified": "2026-05-08T18:52:56Z", + "resolved": "github:segment-integrations/mobile-devtools/725aff9a75fa0ae7269f0f6dbf36fa5611b0664c?dir=segkit&lastModified=1778266376" + }, "gnugrep@latest": { "last_modified": "2026-01-23T17:20:52Z", "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#gnugrep", diff --git a/examples/react-native/devbox.lock b/examples/react-native/devbox.lock index 3f53bbe..043ae72 100644 --- a/examples/react-native/devbox.lock +++ b/examples/react-native/devbox.lock @@ -517,6 +517,10 @@ "last_modified": "2026-02-15T17:45:47Z", "resolved": "github:NixOS/nixpkgs/ac055f38c798b0d87695240c7b761b82fc7e5bc2?lastModified=1771177547" }, + "github:segment-integrations/mobile-devtools?dir=segkit&ref=main": { + "last_modified": "2026-05-08T18:52:56Z", + "resolved": "github:segment-integrations/mobile-devtools/725aff9a75fa0ae7269f0f6dbf36fa5611b0664c?dir=segkit&lastModified=1778266376" + }, "gnugrep@latest": { "last_modified": "2026-01-23T17:20:52Z", "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#gnugrep", From 1cd7633a020d40837d3e0ac930fb56fcaa1accfd Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Fri, 8 May 2026 16:21:11 -0500 Subject: [PATCH 7/7] style(segkit): apply cargo fmt formatting Co-Authored-By: Claude Opus 4.6 --- segkit/src/doctor.rs | 90 +++++++++++++++++++++++++++++++++++--------- segkit/tests/cli.rs | 5 +-- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/segkit/src/doctor.rs b/segkit/src/doctor.rs index 0ccd98d..fa390be 100644 --- a/segkit/src/doctor.rs +++ b/segkit/src/doctor.rs @@ -56,7 +56,10 @@ fn install_devbox() -> Result<(), String> { match status { Ok(s) if s.success() => Ok(()), - Ok(s) => Err(format!("installer exited with code {}", s.code().unwrap_or(-1))), + Ok(s) => Err(format!( + "installer exited with code {}", + s.code().unwrap_or(-1) + )), Err(e) => Err(format!("failed to run installer: {e}")), } } @@ -74,7 +77,10 @@ fn install_homebrew() -> Result<(), String> { ensure_homebrew_in_path(); Ok(()) } - Ok(s) => Err(format!("installer exited with code {}", s.code().unwrap_or(-1))), + Ok(s) => Err(format!( + "installer exited with code {}", + s.code().unwrap_or(-1) + )), Err(e) => Err(format!("failed to run installer: {e}")), } } @@ -89,7 +95,10 @@ fn install_applesimutils() -> Result<(), String> { let status = Command::new(&brew).args(["tap", "wix/brew"]).status(); match status { Ok(s) if !s.success() => { - return Err(format!("brew tap wix/brew failed (code {})", s.code().unwrap_or(-1))); + return Err(format!( + "brew tap wix/brew failed (code {})", + s.code().unwrap_or(-1) + )); } Err(e) => return Err(format!("failed to run brew tap: {e}")), _ => {} @@ -101,7 +110,10 @@ fn install_applesimutils() -> Result<(), String> { match status { Ok(s) if s.success() => Ok(()), - Ok(s) => Err(format!("brew install failed (code {})", s.code().unwrap_or(-1))), + Ok(s) => Err(format!( + "brew install failed (code {})", + s.code().unwrap_or(-1) + )), Err(e) => Err(format!("failed to run brew install: {e}")), } } @@ -129,50 +141,86 @@ struct CheckResult { fn check_devbox(fix: bool) -> CheckResult { let name = "devbox"; if is_installed("devbox") { - return CheckResult { name, status: CheckStatus::Ok }; + return CheckResult { + name, + status: CheckStatus::Ok, + }; } if !fix { - return CheckResult { name, status: CheckStatus::Missing }; + return CheckResult { + name, + status: CheckStatus::Missing, + }; } eprintln!("segkit: installing devbox..."); match install_devbox() { - Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, - Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + Ok(()) => CheckResult { + name, + status: CheckStatus::Fixed, + }, + Err(e) => CheckResult { + name, + status: CheckStatus::Failed(e), + }, } } fn check_homebrew(fix: bool) -> CheckResult { let name = "homebrew"; if !is_macos() { - return CheckResult { name, status: CheckStatus::Ok }; + return CheckResult { + name, + status: CheckStatus::Ok, + }; } ensure_homebrew_in_path(); if is_installed("brew") { - return CheckResult { name, status: CheckStatus::Ok }; + return CheckResult { + name, + status: CheckStatus::Ok, + }; } if !fix { - return CheckResult { name, status: CheckStatus::Missing }; + return CheckResult { + name, + status: CheckStatus::Missing, + }; } eprintln!("segkit: installing homebrew..."); match install_homebrew() { - Ok(()) => CheckResult { name, status: CheckStatus::Fixed }, - Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + Ok(()) => CheckResult { + name, + status: CheckStatus::Fixed, + }, + Err(e) => CheckResult { + name, + status: CheckStatus::Failed(e), + }, } } fn check_applesimutils(fix: bool) -> CheckResult { let name = "applesimutils"; if !is_macos() { - return CheckResult { name, status: CheckStatus::Ok }; + return CheckResult { + name, + status: CheckStatus::Ok, + }; } ensure_homebrew_in_path(); if is_installed("applesimutils") { - return CheckResult { name, status: CheckStatus::Ok }; + return CheckResult { + name, + status: CheckStatus::Ok, + }; } if !fix { - return CheckResult { name, status: CheckStatus::Missing }; + return CheckResult { + name, + status: CheckStatus::Missing, + }; } if homebrew_bin_dir().is_none() { return CheckResult { @@ -184,9 +232,15 @@ fn check_applesimutils(fix: bool) -> CheckResult { match install_applesimutils() { Ok(()) => { ensure_homebrew_in_path(); - CheckResult { name, status: CheckStatus::Fixed } + CheckResult { + name, + status: CheckStatus::Fixed, + } } - Err(e) => CheckResult { name, status: CheckStatus::Failed(e) }, + Err(e) => CheckResult { + name, + status: CheckStatus::Failed(e), + }, } } diff --git a/segkit/tests/cli.rs b/segkit/tests/cli.rs index 7f3d62e..af709e8 100644 --- a/segkit/tests/cli.rs +++ b/segkit/tests/cli.rs @@ -82,8 +82,5 @@ fn metro_subcommand_without_script_fails_gracefully() { #[test] fn doctor_succeeds_when_deps_present() { - segkit() - .arg("doctor") - .assert() - .success(); + segkit().arg("doctor").assert().success(); }