agentmux_srv/server/app_api/bundle/
export.rs1use super::*;
7
8#[derive(serde::Deserialize, Default, ts_rs::TS)]
19#[ts(export, export_to = "../../../frontend/types/rpc/", rename = "BundleExportReq")]
20pub(super) struct ExportReq {
21 pub(crate) id: String,
22 #[serde(default)]
23 #[ts(as = "Option<String>", optional)]
24 pub(crate) format: String,
25 #[serde(default)]
28 #[ts(as = "Option<Vec<String>>", optional)]
29 pub(crate) widgets: Vec<String>,
30}
31
32#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
35#[ts(export, export_to = "../../../frontend/types/rpc/")]
36pub(crate) struct BundleExportResponse {
37 pub(crate) root_slug: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 #[ts(optional)]
41 pub(crate) files: Option<Vec<crate::backend::bundle_export::BundleExportFile>>,
42 pub(crate) skipped_skills: Vec<String>,
43 pub(crate) warnings: Vec<String>,
44 pub(crate) missing_skill_ids: Vec<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 #[ts(optional)]
48 pub(crate) zip_base64: Option<String>,
49}
50
51pub(super) fn register_bundle_export(engine: &Arc<WshRpcEngine>, state: &AppState) {
52 let id_store = state.id_store.clone();
53 let mstore = state.mstore.clone();
54 let identity_store = state.identity_store.clone();
55 engine.register_typed(
56 COMMAND_BUNDLE_EXPORT,
57 move |req: ExportReq, _ctx| {
58 let id_store = id_store.clone();
59 let mstore = mstore.clone();
60 let identity_store = identity_store.clone();
61 async move { bundle_export_impl(&id_store, &mstore, &identity_store, req) }
62 },
63 );
64}
65
66pub(super) fn bundle_export_impl(
72 id_store: &crate::backend::storage::store::Store,
73 mstore: &crate::backend::storage::store::Store,
74 identity_store: &crate::backend::storage::store::Store,
75 req: ExportReq,
76) -> Result<BundleExportResponse, String> {
77 let bundle = id_store
78 .bundle_get(&req.id)
79 .map_err(|e| format!("bundle.export: {e}"))?
80 .ok_or_else(|| format!("bundle.export: no bundle with id {}", req.id))?;
81
82 let components = resolve_bundle_components(mstore, identity_store, &bundle.id)
85 .map_err(|e| format!("bundle.export: {e}"))?;
86
87 let missing_skill_ids: Vec<String> = Vec::new();
94
95 let export = crate::backend::bundle_export::export_bundle(&bundle, &components.skills);
96
97 let mut all_warnings = export.warnings.clone();
98
99 if bound_agent_has_native_memory(id_store, mstore, &bundle.id) {
101 all_warnings.push(MEMORY_NOT_EXPORTED_WARNING.to_string());
102 }
103
104 if !req.widgets.is_empty() && req.format != "zip" {
105 return Err("bundle.export: widgets travel only in a zip (format: \"zip\")".to_string());
106 }
107 if req.format == "zip" {
108 let zip_bytes = if req.widgets.is_empty() {
109 crate::backend::bundle_export::zip_bundle_export(&export)
110 } else {
111 let svc = crate::backend::widget_packages::service().ok_or("bundle.export: widget packages aren't available")?;
112 let mut widgets = Vec::new();
113 for id in &req.widgets {
114 let (hash, files) = svc.approved_package(id).map_err(|e| format!("bundle.export: {e}"))?;
115 widgets.push((id.clone(), hash, files));
116 }
117 crate::backend::bundle_widgets::zip_export_with_widgets(&export, &widgets)
118 }
119 .map_err(|e| format!("bundle.export: {e}"))?;
120 use base64::Engine as _;
121 let encoded = base64::engine::general_purpose::STANDARD.encode(&zip_bytes);
122 return Ok(BundleExportResponse {
123 root_slug: export.root_slug,
124 files: None,
125 skipped_skills: export.skipped_skills,
126 warnings: all_warnings,
127 missing_skill_ids,
128 zip_base64: Some(encoded),
129 });
130 }
131
132 Ok(BundleExportResponse {
133 root_slug: export.root_slug,
134 files: Some(export.files),
135 skipped_skills: export.skipped_skills,
136 warnings: all_warnings,
137 missing_skill_ids,
138 zip_base64: None,
139 })
140}