app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 3cac7164b8a790f3968dfd0f881e9c32795caee7
parent d3179eec10fa50d2b85bcfa51a6cf087d1320ff7
Author: triesap <tyson@radroots.org>
Date:   Tue, 21 Apr 2026 19:23:54 +0000

app: add pack day host bridge planner

- add a launcher-owned pack day host handoff module for macos open commands
- plan reveal_bundle and open_pack_sheet directly from the exported bundle paths
- classify missing target launch and nonzero exit failures in focused tests
- export the new launcher module without wiring runtime or ui actions yet

Diffstat:
Mcrates/launchers/desktop/src/lib.rs | 1+
Acrates/launchers/desktop/src/pack_day_host_handoff.rs | 525+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 526 insertions(+), 0 deletions(-)

diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs @@ -3,6 +3,7 @@ mod accounts; mod app; mod menus; +pub mod pack_day_host_handoff; mod remote_signer; mod runtime; #[cfg(test)] diff --git a/crates/launchers/desktop/src/pack_day_host_handoff.rs b/crates/launchers/desktop/src/pack_day_host_handoff.rs @@ -0,0 +1,525 @@ +use std::io; +use std::path::{Component, Path, PathBuf}; +#[cfg(target_os = "macos")] +use std::process::Command; + +use radroots_app_models::{PackDayExportArtifactKind, PackDayExportBundle, PackDayHostHandoffKind}; +use thiserror::Error; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PackDayHostHandoffCommandPlan { + pub kind: PackDayHostHandoffKind, + pub target_path: PathBuf, + pub command_program: &'static str, + pub command_args: Vec<String>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PackDayHostHandoffCommandResult { + success: bool, + exit_code: Option<i32>, + stderr: String, +} + +impl PackDayHostHandoffCommandResult { + #[cfg(test)] + fn succeeded() -> Self { + Self { + success: true, + exit_code: Some(0), + stderr: String::new(), + } + } + + #[cfg(test)] + fn failed(exit_code: Option<i32>, stderr: impl Into<String>) -> Self { + Self { + success: false, + exit_code, + stderr: stderr.into(), + } + } +} + +#[derive(Debug, Error)] +pub enum PackDayHostHandoffError { + #[error("pack day export bundle directory does not exist: {path}")] + MissingBundleDirectory { path: PathBuf }, + #[error("pack day export bundle is missing required artifact {artifact_kind:?} for {kind:?}")] + MissingArtifactReference { + kind: PackDayHostHandoffKind, + artifact_kind: PackDayExportArtifactKind, + }, + #[error("pack day export artifact path is invalid for {kind:?}: {relative_path}")] + InvalidArtifactRelativePath { + kind: PackDayHostHandoffKind, + relative_path: String, + }, + #[error("pack day host handoff target does not exist for {kind:?}: {path}")] + MissingTargetPath { + kind: PackDayHostHandoffKind, + path: PathBuf, + }, + #[error("pack day host handoff target must be a file for {kind:?}: {path}")] + InvalidTargetFile { + kind: PackDayHostHandoffKind, + path: PathBuf, + }, + #[error("pack day host handoff is only supported on macos")] + UnsupportedPlatform, + #[error("failed to launch macos host command {program} for {kind:?}: {source}")] + CommandLaunch { + kind: PackDayHostHandoffKind, + program: String, + source: io::Error, + }, + #[error("macos host command {program} for {kind:?} exited with code {exit_code:?}: {stderr}")] + CommandFailed { + kind: PackDayHostHandoffKind, + program: String, + exit_code: Option<i32>, + stderr: String, + }, +} + +impl PartialEq for PackDayHostHandoffError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::MissingBundleDirectory { path: left }, + Self::MissingBundleDirectory { path: right }, + ) => left == right, + ( + Self::MissingArtifactReference { + kind: left_kind, + artifact_kind: left_artifact, + }, + Self::MissingArtifactReference { + kind: right_kind, + artifact_kind: right_artifact, + }, + ) => left_kind == right_kind && left_artifact == right_artifact, + ( + Self::InvalidArtifactRelativePath { + kind: left_kind, + relative_path: left_path, + }, + Self::InvalidArtifactRelativePath { + kind: right_kind, + relative_path: right_path, + }, + ) => left_kind == right_kind && left_path == right_path, + ( + Self::MissingTargetPath { + kind: left_kind, + path: left_path, + }, + Self::MissingTargetPath { + kind: right_kind, + path: right_path, + }, + ) => left_kind == right_kind && left_path == right_path, + ( + Self::InvalidTargetFile { + kind: left_kind, + path: left_path, + }, + Self::InvalidTargetFile { + kind: right_kind, + path: right_path, + }, + ) => left_kind == right_kind && left_path == right_path, + (Self::UnsupportedPlatform, Self::UnsupportedPlatform) => true, + ( + Self::CommandLaunch { + kind: left_kind, + program: left_program, + source: left_source, + }, + Self::CommandLaunch { + kind: right_kind, + program: right_program, + source: right_source, + }, + ) => { + left_kind == right_kind + && left_program == right_program + && left_source.kind() == right_source.kind() + && left_source.to_string() == right_source.to_string() + } + ( + Self::CommandFailed { + kind: left_kind, + program: left_program, + exit_code: left_code, + stderr: left_stderr, + }, + Self::CommandFailed { + kind: right_kind, + program: right_program, + exit_code: right_code, + stderr: right_stderr, + }, + ) => { + left_kind == right_kind + && left_program == right_program + && left_code == right_code + && left_stderr == right_stderr + } + _ => false, + } + } +} + +impl Eq for PackDayHostHandoffError {} + +pub fn plan_pack_day_host_handoff( + bundle: &PackDayExportBundle, + kind: PackDayHostHandoffKind, +) -> Result<PackDayHostHandoffCommandPlan, PackDayHostHandoffError> { + let bundle_directory = PathBuf::from(&bundle.bundle_directory); + if !bundle_directory.is_dir() { + return Err(PackDayHostHandoffError::MissingBundleDirectory { + path: bundle_directory, + }); + } + + let target_path = match kind { + PackDayHostHandoffKind::RevealBundle => bundle_directory.clone(), + PackDayHostHandoffKind::OpenPackSheet => { + resolve_bundle_artifact_path(bundle, PackDayExportArtifactKind::PackSheet, kind)? + } + }; + + let command_args = match kind { + PackDayHostHandoffKind::RevealBundle => { + vec!["-R".to_owned(), target_path.to_string_lossy().into_owned()] + } + PackDayHostHandoffKind::OpenPackSheet => { + vec![target_path.to_string_lossy().into_owned()] + } + }; + + Ok(PackDayHostHandoffCommandPlan { + kind, + target_path, + command_program: "open", + command_args, + }) +} + +pub fn execute_pack_day_host_handoff_plan( + plan: &PackDayHostHandoffCommandPlan, +) -> Result<(), PackDayHostHandoffError> { + #[cfg(target_os = "macos")] + { + execute_pack_day_host_handoff_plan_with(plan, run_macos_host_command) + } + + #[cfg(not(target_os = "macos"))] + { + let _ = plan; + Err(PackDayHostHandoffError::UnsupportedPlatform) + } +} + +fn resolve_bundle_artifact_path( + bundle: &PackDayExportBundle, + artifact_kind: PackDayExportArtifactKind, + kind: PackDayHostHandoffKind, +) -> Result<PathBuf, PackDayHostHandoffError> { + let Some(artifact) = bundle + .artifacts + .iter() + .find(|artifact| artifact.kind == artifact_kind) + else { + return Err(PackDayHostHandoffError::MissingArtifactReference { + kind, + artifact_kind, + }); + }; + + let relative_path = Path::new(&artifact.relative_path); + if relative_path.is_absolute() + || relative_path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) + { + return Err(PackDayHostHandoffError::InvalidArtifactRelativePath { + kind, + relative_path: artifact.relative_path.clone(), + }); + } + + let path = PathBuf::from(&bundle.bundle_directory).join(relative_path); + if !path.exists() { + return Err(PackDayHostHandoffError::MissingTargetPath { kind, path }); + } + if !path.is_file() { + return Err(PackDayHostHandoffError::InvalidTargetFile { kind, path }); + } + + Ok(path) +} + +fn execute_pack_day_host_handoff_plan_with( + plan: &PackDayHostHandoffCommandPlan, + run_command: impl FnOnce( + &PackDayHostHandoffCommandPlan, + ) -> Result<PackDayHostHandoffCommandResult, io::Error>, +) -> Result<(), PackDayHostHandoffError> { + let result = run_command(plan).map_err(|source| PackDayHostHandoffError::CommandLaunch { + kind: plan.kind, + program: plan.command_program.to_owned(), + source, + })?; + + if result.success { + return Ok(()); + } + + Err(PackDayHostHandoffError::CommandFailed { + kind: plan.kind, + program: plan.command_program.to_owned(), + exit_code: result.exit_code, + stderr: result.stderr, + }) +} + +#[cfg(target_os = "macos")] +fn run_macos_host_command( + plan: &PackDayHostHandoffCommandPlan, +) -> Result<PackDayHostHandoffCommandResult, io::Error> { + let output = Command::new(plan.command_program) + .args(&plan.command_args) + .output()?; + + Ok(PackDayHostHandoffCommandResult { + success: output.status.success(), + exit_code: output.status.code(), + stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(), + }) +} + +#[cfg(test)] +mod tests { + use super::{ + execute_pack_day_host_handoff_plan_with, plan_pack_day_host_handoff, + PackDayHostHandoffCommandResult, PackDayHostHandoffError, + }; + use radroots_app_models::{ + PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, + PackDayHostHandoffKind, + }; + use std::fs; + use std::io; + use std::path::PathBuf; + use uuid::Uuid; + + struct TestDirectory { + path: PathBuf, + } + + impl TestDirectory { + fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "radroots_app_pack_day_host_handoff_{}", + Uuid::new_v4() + )); + fs::create_dir_all(&path).expect("test directory should create"); + Self { path } + } + + fn path(&self) -> &PathBuf { + &self.path + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } + } + + fn sample_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle { + PackDayExportBundle { + fulfillment_window_id: radroots_app_models::FulfillmentWindowId::new(), + generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), + bundle_directory: bundle_directory.to_string_lossy().into_owned(), + artifacts: vec![ + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PackSheet, + relative_path: "pack_sheet.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PickupRoster, + relative_path: "pickup_roster.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::CustomerLabels, + relative_path: "customer_labels.txt".to_owned(), + }, + ], + } + } + + #[test] + fn reveal_bundle_plan_uses_open_reveal_for_the_bundle_directory() { + let temp_dir = TestDirectory::new(); + let bundle = sample_bundle(temp_dir.path()); + + let plan = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::RevealBundle) + .expect("reveal plan should build"); + + assert_eq!(plan.kind, PackDayHostHandoffKind::RevealBundle); + assert_eq!(plan.target_path, temp_dir.path().clone()); + assert_eq!(plan.command_program, "open"); + assert_eq!( + plan.command_args, + vec![ + "-R".to_owned(), + temp_dir.path().to_string_lossy().into_owned(), + ] + ); + } + + #[test] + fn open_pack_sheet_plan_targets_the_exported_pack_sheet() { + let temp_dir = TestDirectory::new(); + let pack_sheet_path = temp_dir.path().join("pack_sheet.txt"); + fs::write(&pack_sheet_path, "pack day").expect("pack sheet should write"); + let bundle = sample_bundle(temp_dir.path()); + + let plan = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::OpenPackSheet) + .expect("open plan should build"); + + assert_eq!(plan.kind, PackDayHostHandoffKind::OpenPackSheet); + assert_eq!(plan.target_path, pack_sheet_path.clone()); + assert_eq!(plan.command_program, "open"); + assert_eq!( + plan.command_args, + vec![pack_sheet_path.to_string_lossy().into_owned()] + ); + } + + #[test] + fn planning_fails_when_the_bundle_directory_is_missing() { + let bundle_directory = std::env::temp_dir().join(format!( + "radroots_app_pack_day_host_handoff_missing_{}", + Uuid::new_v4() + )); + let bundle = sample_bundle(&bundle_directory); + + let error = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::RevealBundle) + .expect_err("missing bundle directory should fail"); + + assert_eq!( + error, + PackDayHostHandoffError::MissingBundleDirectory { + path: bundle_directory, + } + ); + } + + #[test] + fn planning_fails_when_pack_sheet_reference_is_missing() { + let temp_dir = TestDirectory::new(); + let mut bundle = sample_bundle(temp_dir.path()); + bundle + .artifacts + .retain(|artifact| artifact.kind != PackDayExportArtifactKind::PackSheet); + + let error = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::OpenPackSheet) + .expect_err("missing pack sheet artifact should fail"); + + assert_eq!( + error, + PackDayHostHandoffError::MissingArtifactReference { + kind: PackDayHostHandoffKind::OpenPackSheet, + artifact_kind: PackDayExportArtifactKind::PackSheet, + } + ); + } + + #[test] + fn planning_fails_when_pack_sheet_relative_path_is_invalid() { + let temp_dir = TestDirectory::new(); + let mut bundle = sample_bundle(temp_dir.path()); + bundle.artifacts[0].relative_path = "../pack_sheet.txt".to_owned(); + + let error = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::OpenPackSheet) + .expect_err("invalid relative path should fail"); + + assert_eq!( + error, + PackDayHostHandoffError::InvalidArtifactRelativePath { + kind: PackDayHostHandoffKind::OpenPackSheet, + relative_path: "../pack_sheet.txt".to_owned(), + } + ); + } + + #[test] + fn execution_classifies_command_launch_failures() { + let temp_dir = TestDirectory::new(); + let bundle = sample_bundle(temp_dir.path()); + let plan = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::RevealBundle) + .expect("reveal plan should build"); + + let error = execute_pack_day_host_handoff_plan_with(&plan, |_| { + Err(io::Error::new(io::ErrorKind::NotFound, "open missing")) + }) + .expect_err("launch failure should classify"); + + assert!(matches!( + error, + PackDayHostHandoffError::CommandLaunch { + kind: PackDayHostHandoffKind::RevealBundle, + .. + } + )); + } + + #[test] + fn execution_classifies_nonzero_exit_failures() { + let temp_dir = TestDirectory::new(); + let bundle = sample_bundle(temp_dir.path()); + let plan = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::RevealBundle) + .expect("reveal plan should build"); + + let error = execute_pack_day_host_handoff_plan_with(&plan, |_| { + Ok(PackDayHostHandoffCommandResult::failed( + Some(1), + "finder unavailable", + )) + }) + .expect_err("nonzero exit should classify"); + + assert_eq!( + error, + PackDayHostHandoffError::CommandFailed { + kind: PackDayHostHandoffKind::RevealBundle, + program: "open".to_owned(), + exit_code: Some(1), + stderr: "finder unavailable".to_owned(), + } + ); + } + + #[test] + fn execution_accepts_successful_runs() { + let temp_dir = TestDirectory::new(); + let bundle = sample_bundle(temp_dir.path()); + let plan = plan_pack_day_host_handoff(&bundle, PackDayHostHandoffKind::RevealBundle) + .expect("reveal plan should build"); + + let result = execute_pack_day_host_handoff_plan_with(&plan, |_| { + Ok(PackDayHostHandoffCommandResult::succeeded()) + }); + + assert_eq!(result, Ok(())); + } +}