commit a2c677b2bb41cd36a18944a6004fa65f2b4629c4
parent 2d190c4e00885cd30953b2646b47d5c4e502cf5b
Author: triesap <tyson@radroots.org>
Date: Sat, 27 Jun 2026 02:36:20 +0000
terminal: add seller renderers
- register farm and listing terminal renderers for seller workflows
- render farm setup, location, readiness, and publish receipts from source fields
- render listing tables, validation output, app export, and publish receipts
- source-gate listing publish proof output and keep machine formats unchanged
Diffstat:
5 files changed, 819 insertions(+), 4 deletions(-)
diff --git a/src/out/terminal/registry.rs b/src/out/terminal/registry.rs
@@ -64,7 +64,9 @@ pub fn terminal_renderer_registry() -> TerminalRendererRegistry {
let registry = crate::out::terminal::renderers::account::register(registry);
let registry = crate::out::terminal::renderers::runtime::register(registry);
let registry = crate::out::terminal::renderers::store::register(registry);
- crate::out::terminal::renderers::sync::register(registry)
+ let registry = crate::out::terminal::renderers::sync::register(registry);
+ let registry = crate::out::terminal::renderers::farm::register(registry);
+ crate::out::terminal::renderers::listing::register(registry)
}
#[cfg(test)]
@@ -100,7 +102,7 @@ mod tests {
}
#[test]
- fn core_runtime_registry_covers_rcld_04_operations() {
+ fn registry_covers_core_and_seller_operations() {
let registry = terminal_renderer_registry();
let expected = [
"workspace.init",
@@ -128,6 +130,26 @@ mod tests {
"sync.pull",
"sync.push",
"sync.watch",
+ "farm.create",
+ "farm.get",
+ "farm.rebind",
+ "farm.profile.update",
+ "farm.location.set",
+ "farm.location.get",
+ "farm.location.clear",
+ "farm.fulfillment.update",
+ "farm.readiness.check",
+ "farm.publish",
+ "listing.create",
+ "listing.get",
+ "listing.list",
+ "listing.app.list",
+ "listing.app.export",
+ "listing.update",
+ "listing.validate",
+ "listing.rebind",
+ "listing.publish",
+ "listing.archive",
];
assert_eq!(registry.len(), expected.len());
diff --git a/src/out/terminal/renderers/common.rs b/src/out/terminal/renderers/common.rs
@@ -132,8 +132,19 @@ pub(crate) fn push_count_field(
value: &Value,
path: &[&str],
) {
- if let Some(value) = number_path(value, path) {
- push_field(document, label, value.to_string());
+ if let Some(value) = number_label_path(value, path) {
+ push_field(document, label, value);
+ }
+}
+
+pub(crate) fn push_number_field(
+ document: &mut TerminalDocument,
+ label: &str,
+ value: &Value,
+ path: &[&str],
+) {
+ if let Some(value) = number_label_path(value, path) {
+ push_field(document, label, value);
}
}
@@ -181,6 +192,36 @@ pub(crate) fn number_path(value: &Value, path: &[&str]) -> Option<i64> {
current.as_i64()
}
+pub(crate) fn number_label_path(value: &Value, path: &[&str]) -> Option<String> {
+ let mut current = value;
+ for segment in path {
+ current = current.get(*segment)?;
+ }
+ number_label(current)
+}
+
+pub(crate) fn number_label(value: &Value) -> Option<String> {
+ if let Some(value) = value.as_i64() {
+ return Some(value.to_string());
+ }
+ if let Some(value) = value.as_u64() {
+ return Some(value.to_string());
+ }
+ value
+ .as_f64()
+ .filter(|value| value.is_finite())
+ .map(|value| {
+ let mut rendered = format!("{value:.6}");
+ while rendered.contains('.') && rendered.ends_with('0') {
+ rendered.pop();
+ }
+ if rendered.ends_with('.') {
+ rendered.pop();
+ }
+ rendered
+ })
+}
+
pub(crate) fn bool_path(value: &Value, path: &[&str]) -> Option<bool> {
let mut current = value;
for segment in path {
diff --git a/src/out/terminal/renderers/farm.rs b/src/out/terminal/renderers/farm.rs
@@ -0,0 +1,368 @@
+use serde_json::Value;
+
+use crate::out::envelope::OutputEnvelope;
+use crate::out::terminal::layout::TerminalDocument;
+use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
+use crate::out::terminal::renderer::TerminalRenderContext;
+use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
+use crate::out::terminal::values::{relay_summary, transport_label};
+
+use super::common;
+
+pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
+ registry
+ .register("farm.create", &FARM_RENDERER)
+ .register("farm.get", &FARM_RENDERER)
+ .register("farm.rebind", &FARM_RENDERER)
+ .register("farm.profile.update", &FARM_RENDERER)
+ .register("farm.location.set", &FARM_RENDERER)
+ .register("farm.location.get", &FARM_RENDERER)
+ .register("farm.location.clear", &FARM_RENDERER)
+ .register("farm.fulfillment.update", &FARM_RENDERER)
+ .register("farm.readiness.check", &FARM_RENDERER)
+ .register("farm.publish", &FARM_RENDERER)
+}
+
+struct FarmRenderer;
+
+static FARM_RENDERER: FarmRenderer = FarmRenderer;
+
+impl TerminalOperationRenderer for FarmRenderer {
+ fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
+ if !envelope.errors.is_empty() {
+ return common::generic_terminal_document(envelope);
+ }
+ let result = common::display_source(envelope);
+ match envelope.operation_id.as_str() {
+ "farm.location.set" | "farm.location.get" | "farm.location.clear" => {
+ farm_location_document(envelope, result)
+ }
+ "farm.readiness.check" => farm_readiness_document(envelope, result),
+ "farm.publish" => farm_publish_document(envelope, result),
+ "farm.get" => farm_get_document(envelope, result),
+ "farm.rebind" => farm_rebind_document(envelope, result),
+ "farm.profile.update" | "farm.fulfillment.update" => {
+ farm_update_document(envelope, result)
+ }
+ _ => farm_create_document(envelope, result),
+ }
+ }
+}
+
+fn farm_create_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document =
+ common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
+ push_farm_summary_fields(&mut document, result);
+ document
+}
+
+fn farm_get_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Farm");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Scope", result, &["scope"]);
+ common::push_path_field(&mut document, "Path", result, &["path"]);
+ push_farm_document_fields(&mut document, result);
+ document
+}
+
+fn farm_rebind_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Farm rebound");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Scope", result, &["scope"]);
+ common::push_path_field(&mut document, "Path", result, &["path"]);
+ common::push_path_field(&mut document, "From", result, &["from_seller_account_id"]);
+ common::push_path_field(&mut document, "To", result, &["to_seller_account_id"]);
+ common::push_bool_field(
+ &mut document,
+ "Seller changed",
+ result,
+ &["seller_pubkey_changed"],
+ );
+ push_farm_summary_fields(&mut document, result);
+ document
+}
+
+fn farm_update_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document =
+ common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Field", result, &["field"]);
+ common::push_path_field(&mut document, "Value", result, &["value"]);
+ push_farm_summary_fields(&mut document, result);
+ document
+}
+
+fn farm_location_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document =
+ common::document_with_title(envelope, farm_title(envelope.operation_id.as_str()));
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
+ common::push_path_field(&mut document, "Label", result, &["label"]);
+ common::push_number_field(
+ &mut document,
+ "Latitude",
+ result,
+ &["exact_location", "lat"],
+ );
+ common::push_number_field(
+ &mut document,
+ "Longitude",
+ result,
+ &["exact_location", "lng"],
+ );
+ common::push_path_field(
+ &mut document,
+ "Locality",
+ result,
+ &["public_locality", "primary"],
+ );
+ common::push_path_field(&mut document, "City", result, &["public_locality", "city"]);
+ common::push_path_field(
+ &mut document,
+ "Region",
+ result,
+ &["public_locality", "region"],
+ );
+ common::push_path_field(
+ &mut document,
+ "Country",
+ result,
+ &["public_locality", "country"],
+ );
+ common::push_count_field(&mut document, "GeoNames", result, &["geonames_feature_id"]);
+ common::push_bool_field(&mut document, "Cleared", result, &["cleared"]);
+ common::push_verbose_path_field(
+ &mut document,
+ "GeoNames DB",
+ result,
+ &["geonames_database_path"],
+ );
+ push_location_candidates(&mut document, result);
+ document
+}
+
+fn farm_readiness_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document =
+ common::document_with_title(envelope, common::title_for(envelope, "Farm readiness"));
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Scope", result, &["scope"]);
+ common::push_path_field(&mut document, "Account", result, &["account_state"]);
+ common::push_path_field(
+ &mut document,
+ "Listings",
+ result,
+ &["listing_defaults_state"],
+ );
+ common::push_path_field(&mut document, "Publish", result, &["publish_state"]);
+ if let Some(mode) = common::string(result, &["publish_transport"]) {
+ common::push_field(&mut document, "Transport", transport_label(mode.as_str()));
+ }
+ common::push_bool_field(&mut document, "Executable", result, &["publish_executable"]);
+ push_farm_summary_fields(&mut document, result);
+ push_missing_section(&mut document, result);
+ document
+}
+
+fn farm_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Farm published");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
+ common::push_path_field(&mut document, "Account", result, &["seller_account_id"]);
+ common::push_path_field(&mut document, "Profile", result, &["profile", "state"]);
+ common::push_path_field(&mut document, "Farm publish", result, &["farm", "state"]);
+ push_publish_component_fields(
+ &mut document,
+ "Profile relays",
+ "Profile event",
+ result,
+ &["profile"],
+ );
+ push_publish_component_fields(
+ &mut document,
+ "Farm relays",
+ "Farm event",
+ result,
+ &["farm"],
+ );
+ push_publish_components_section(&mut document, result);
+ document
+}
+
+fn push_farm_summary_fields(document: &mut TerminalDocument, result: &Value) {
+ common::push_path_field(document, "State", result, &["state"]);
+ common::push_path_field(document, "Source", result, &["source"]);
+ common::push_path_field(document, "Scope", result, &["config", "scope"]);
+ common::push_path_field(document, "Farm", result, &["config", "farm_d_tag"]);
+ common::push_path_field(document, "Name", result, &["config", "name"]);
+ common::push_path_field(
+ document,
+ "Location",
+ result,
+ &["config", "location_primary"],
+ );
+ common::push_path_field(document, "Delivery", result, &["config", "delivery_method"]);
+ common::push_path_field(document, "Path", result, &["config", "path"]);
+}
+
+fn push_farm_document_fields(document: &mut TerminalDocument, result: &Value) {
+ common::push_path_field(
+ document,
+ "Farm",
+ result,
+ &["document", "selection", "farm_d_tag"],
+ );
+ common::push_path_field(
+ document,
+ "Account",
+ result,
+ &["document", "selection", "seller_account_id"],
+ );
+ common::push_path_field(document, "Name", result, &["document", "farm", "name"]);
+ common::push_path_field(
+ document,
+ "Display",
+ result,
+ &["document", "profile", "display_name"],
+ );
+ common::push_path_field(
+ document,
+ "Location",
+ result,
+ &["document", "listing_defaults", "location", "primary"],
+ );
+ common::push_path_field(
+ document,
+ "Delivery",
+ result,
+ &["document", "listing_defaults", "delivery_method"],
+ );
+}
+
+fn push_location_candidates(document: &mut TerminalDocument, result: &Value) {
+ let rows = common::array(result, &["candidates"])
+ .into_iter()
+ .flatten()
+ .map(|candidate| {
+ TerminalTableRow::new(vec![
+ common::number_label_path(candidate, &["geonames_feature_id"]).unwrap_or_default(),
+ common::string(candidate, &["display_name"]).unwrap_or_default(),
+ common::number_label_path(candidate, &["exact_location", "lat"])
+ .unwrap_or_default(),
+ common::number_label_path(candidate, &["exact_location", "lng"])
+ .unwrap_or_default(),
+ ])
+ })
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return;
+ }
+ document.sections.push(common::table_section(
+ "GeoNames candidates",
+ vec![
+ TerminalTableColumn::new("ID", 2, 10),
+ TerminalTableColumn::new("Name", 8, 32),
+ TerminalTableColumn::new("Lat", 3, 10),
+ TerminalTableColumn::new("Lng", 3, 11),
+ ],
+ rows,
+ "No GeoNames candidates",
+ ));
+}
+
+fn push_missing_section(document: &mut TerminalDocument, result: &Value) {
+ let rows = common::array(result, &["missing"])
+ .into_iter()
+ .flatten()
+ .filter_map(Value::as_str)
+ .map(|value| TerminalTableRow::new(vec![value.to_owned()]))
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return;
+ }
+ document.sections.push(common::table_section(
+ "Missing",
+ vec![TerminalTableColumn::new("Item", 8, 48)],
+ rows,
+ "No missing requirements",
+ ));
+}
+
+fn push_publish_component_fields(
+ document: &mut TerminalDocument,
+ relay_label: &str,
+ event_label: &str,
+ result: &Value,
+ path: &[&str],
+) {
+ let Some(component) = nested_value(result, path) else {
+ return;
+ };
+ if let Some(summary) = relay_component_summary(component) {
+ common::push_field(document, relay_label, summary);
+ }
+ common::push_path_field(document, event_label, component, &["event_id"]);
+ common::push_path_field(document, "Job", component, &["job_id"]);
+}
+
+fn push_publish_components_section(document: &mut TerminalDocument, result: &Value) {
+ let rows = ["profile", "farm"]
+ .into_iter()
+ .filter_map(|component_name| {
+ let component = result.get(component_name)?;
+ Some(TerminalTableRow::new(vec![
+ component_name.to_owned(),
+ common::string(component, &["state"]).unwrap_or_default(),
+ relay_component_summary(component).unwrap_or_default(),
+ ]))
+ })
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return;
+ }
+ document.sections.push(common::table_section(
+ "Publish components",
+ vec![
+ TerminalTableColumn::new("Part", 4, 8),
+ TerminalTableColumn::new("State", 5, 16),
+ TerminalTableColumn::new("Relays", 8, 28),
+ ],
+ rows,
+ "No publish components",
+ ));
+}
+
+fn relay_component_summary(value: &Value) -> Option<String> {
+ let acknowledged = common::array(value, &["acknowledged_relays"])
+ .map(Vec::len)
+ .unwrap_or(0);
+ let failed = common::array(value, &["failed_relays"])
+ .map(Vec::len)
+ .unwrap_or(0);
+ let target = common::array(value, &["target_relays"])
+ .map(Vec::len)
+ .unwrap_or(0);
+ (acknowledged > 0 || failed > 0 || target > 0)
+ .then(|| relay_summary(acknowledged, failed, "acknowledged"))
+}
+
+fn nested_value<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
+ let mut current = value;
+ for segment in path {
+ current = current.get(*segment)?;
+ }
+ Some(current)
+}
+
+fn farm_title(operation_id: &str) -> &'static str {
+ match operation_id {
+ "farm.create" => "Farm created",
+ "farm.rebind" => "Farm rebound",
+ "farm.profile.update" => "Farm profile updated",
+ "farm.location.set" => "Farm location set",
+ "farm.location.get" => "Farm location",
+ "farm.location.clear" => "Farm location cleared",
+ "farm.fulfillment.update" => "Farm fulfillment updated",
+ "farm.publish" => "Farm published",
+ _ => "Farm",
+ }
+}
diff --git a/src/out/terminal/renderers/listing.rs b/src/out/terminal/renderers/listing.rs
@@ -0,0 +1,382 @@
+use serde_json::Value;
+
+use crate::out::envelope::OutputEnvelope;
+use crate::out::terminal::layout::TerminalDocument;
+use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
+use crate::out::terminal::renderer::TerminalRenderContext;
+use crate::out::terminal::tables::{TerminalTableColumn, TerminalTableRow};
+use crate::out::terminal::values::{proof_summary, relay_summary, transport_label};
+
+use super::common;
+
+pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
+ registry
+ .register("listing.create", &LISTING_RENDERER)
+ .register("listing.get", &LISTING_RENDERER)
+ .register("listing.list", &LISTING_RENDERER)
+ .register("listing.app.list", &LISTING_RENDERER)
+ .register("listing.app.export", &LISTING_RENDERER)
+ .register("listing.update", &LISTING_RENDERER)
+ .register("listing.validate", &LISTING_RENDERER)
+ .register("listing.rebind", &LISTING_RENDERER)
+ .register("listing.publish", &LISTING_RENDERER)
+ .register("listing.archive", &LISTING_RENDERER)
+}
+
+struct ListingRenderer;
+
+static LISTING_RENDERER: ListingRenderer = ListingRenderer;
+
+impl TerminalOperationRenderer for ListingRenderer {
+ fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
+ if !envelope.errors.is_empty() {
+ return common::generic_terminal_document(envelope);
+ }
+ let result = common::display_source(envelope);
+ match envelope.operation_id.as_str() {
+ "listing.list" => listing_list_document(envelope, result),
+ "listing.app.list" => listing_app_list_document(envelope, result),
+ "listing.app.export" => listing_app_export_document(envelope, result),
+ "listing.validate" => listing_validate_document(envelope, result),
+ "listing.rebind" => listing_rebind_document(envelope, result),
+ "listing.publish" => listing_publish_document(envelope, result),
+ "listing.archive" | "listing.update" => listing_mutation_document(envelope, result),
+ "listing.get" => listing_get_document(envelope, result),
+ _ => listing_create_document(envelope, result),
+ }
+ }
+}
+
+fn listing_create_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing draft created");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "Key", result, &["key"]);
+ common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
+ common::push_path_field(&mut document, "Delivery", result, &["delivery_method"]);
+ common::push_path_field(&mut document, "Location", result, &["location_primary"]);
+ common::push_path_field(&mut document, "File", result, &["file"]);
+ document
+}
+
+fn listing_get_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "Key", result, &["product_key"]);
+ common::push_path_field(&mut document, "Title", result, &["title"]);
+ common::push_path_field(&mut document, "Category", result, &["category"]);
+ common::push_path_field(&mut document, "Location", result, &["location_primary"]);
+ common::push_path_field(&mut document, "Available", result, &["available", "label"]);
+ common::push_path_field(&mut document, "Currency", result, &["price", "currency"]);
+ common::push_number_field(&mut document, "Price", result, &["price", "amount"]);
+ document
+}
+
+fn listing_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listings");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_count_field(&mut document, "Count", result, &["count"]);
+ common::push_verbose_path_field(&mut document, "Drafts", result, &["draft_dir"]);
+ let rows = common::array(result, &["listings"])
+ .into_iter()
+ .flatten()
+ .map(|listing| {
+ TerminalTableRow::new(vec![
+ common::string(listing, &["id"]).unwrap_or_default(),
+ common::string(listing, &["title"])
+ .or_else(|| common::string(listing, &["product_key"]))
+ .unwrap_or_default(),
+ common::string(listing, &["state"]).unwrap_or_default(),
+ common::string(listing, &["file"]).unwrap_or_default(),
+ ])
+ })
+ .collect::<Vec<_>>();
+ document.sections.push(common::table_section(
+ "Listings",
+ vec![
+ TerminalTableColumn::new("Listing", 8, 22),
+ TerminalTableColumn::new("Title", 5, 24),
+ TerminalTableColumn::new("State", 5, 12),
+ TerminalTableColumn::new("File", 4, 34),
+ ],
+ rows,
+ "No listing drafts found",
+ ));
+ document
+}
+
+fn listing_app_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing app records");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_count_field(&mut document, "Count", result, &["count"]);
+ common::push_bool_field(&mut document, "More", result, &["has_more"]);
+ let rows = common::array(result, &["records"])
+ .into_iter()
+ .flatten()
+ .map(|record| {
+ TerminalTableRow::new(vec![
+ common::string(record, &["record_id"]).unwrap_or_default(),
+ common::string(record, &["title"])
+ .or_else(|| common::string(record, &["listing_id"]))
+ .unwrap_or_default(),
+ common::string(record, &["status"]).unwrap_or_default(),
+ common::bool_path(record, &["exportable"])
+ .map(|value| if value { "yes" } else { "no" }.to_owned())
+ .unwrap_or_default(),
+ ])
+ })
+ .collect::<Vec<_>>();
+ document.sections.push(common::table_section(
+ "Records",
+ vec![
+ TerminalTableColumn::new("Record", 8, 22),
+ TerminalTableColumn::new("Listing", 8, 24),
+ TerminalTableColumn::new("Status", 6, 14),
+ TerminalTableColumn::new("Export", 6, 6),
+ ],
+ rows,
+ "No app-authored listing records found",
+ ));
+ document
+}
+
+fn listing_app_export_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing app export");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Record", result, &["record_id"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
+ common::push_bool_field(&mut document, "Valid", result, &["valid"]);
+ common::push_path_field(&mut document, "File", result, &["file"]);
+ push_issues_section(&mut document, result);
+ document
+}
+
+fn listing_validate_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let title = if common::bool_path(result, &["valid"]) == Some(true) {
+ "Listing valid"
+ } else {
+ "Listing validation"
+ };
+ let mut document = common::document_with_title(envelope, title);
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_bool_field(&mut document, "Valid", result, &["valid"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "Farm", result, &["farm_d_tag"]);
+ common::push_path_field(&mut document, "File", result, &["file"]);
+ push_issues_section(&mut document, result);
+ document
+}
+
+fn listing_rebind_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing rebound");
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "From", result, &["from_seller_account_id"]);
+ common::push_path_field(&mut document, "To", result, &["to_seller_account_id"]);
+ common::push_path_field(&mut document, "Farm", result, &["to_farm_d_tag"]);
+ common::push_bool_field(
+ &mut document,
+ "Seller changed",
+ result,
+ &["seller_pubkey_changed"],
+ );
+ common::push_bool_field(
+ &mut document,
+ "Address changed",
+ result,
+ &["listing_addr_changed"],
+ );
+ common::push_path_field(&mut document, "File", result, &["file"]);
+ document
+}
+
+fn listing_mutation_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document =
+ common::document_with_title(envelope, listing_title(envelope.operation_id.as_str()));
+ common::push_path_field(&mut document, "State", result, &["state"]);
+ common::push_path_field(&mut document, "Listing", result, &["listing_id"]);
+ common::push_path_field(&mut document, "Address", result, &["listing_addr"]);
+ common::push_path_field(&mut document, "Account", result, &["seller_account_id"]);
+ common::push_path_field(&mut document, "Event", result, &["event_id"]);
+ common::push_path_field(&mut document, "File", result, &["file"]);
+ push_relay_field(&mut document, result);
+ document
+}
+
+fn listing_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
+ let mut document = common::document_with_title(envelope, "Listing published");
+ if let Some(listing) = first_string(
+ result,
+ &[
+ &["listing_id"],
+ &["listing", "id"],
+ &["listing", "d_tag"],
+ &["resource", "id"],
+ &["listing_addr"],
+ &["listing_address"],
+ ],
+ ) {
+ common::push_field(&mut document, "Listing", listing);
+ }
+ if let Some(mode) = first_string(
+ result,
+ &[
+ &["publish", "mode"],
+ &["publish_transport"],
+ &["transport"],
+ &["source"],
+ ],
+ ) {
+ common::push_field(&mut document, "Transport", transport_label(mode.as_str()));
+ }
+ push_relay_field(&mut document, result);
+ if let Some(event_id) = first_string(
+ result,
+ &[
+ &["event_id"],
+ &["publish", "event_id"],
+ &["event", "id"],
+ &["event", "event_id"],
+ &["receipt", "event_id"],
+ ],
+ ) {
+ common::push_field(&mut document, "Event", event_id);
+ }
+ if let Some(proof) = proof_summary(result) {
+ common::push_field(&mut document, "Proof", proof);
+ }
+ document
+}
+
+fn push_issues_section(document: &mut TerminalDocument, result: &Value) {
+ let rows = common::array(result, &["issues"])
+ .into_iter()
+ .flatten()
+ .map(|issue| {
+ TerminalTableRow::new(vec![
+ common::string(issue, &["field"]).unwrap_or_default(),
+ common::string(issue, &["message"]).unwrap_or_default(),
+ ])
+ })
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return;
+ }
+ document.sections.push(common::table_section(
+ "Issues",
+ vec![
+ TerminalTableColumn::new("Field", 5, 18),
+ TerminalTableColumn::new("Message", 7, 54),
+ ],
+ rows,
+ "No validation issues",
+ ));
+}
+
+fn push_relay_field(document: &mut TerminalDocument, result: &Value) {
+ if let Some(summary) = relay_field_summary(result) {
+ common::push_field(document, "Relays", summary);
+ }
+}
+
+fn relay_field_summary(result: &Value) -> Option<String> {
+ let acknowledged = common::array(result, &["acknowledged_relays"])
+ .map(Vec::len)
+ .or_else(|| {
+ common::number_path(result, &["publish", "acknowledged_count"])
+ .and_then(|value| usize::try_from(value).ok())
+ })
+ .or_else(|| {
+ common::number_path(result, &["receipt", "acknowledged_count"])
+ .and_then(|value| usize::try_from(value).ok())
+ })
+ .unwrap_or(0);
+ let failed = common::array(result, &["failed_relays"])
+ .map(Vec::len)
+ .or_else(|| {
+ common::number_path(result, &["publish", "failed_count"])
+ .and_then(|value| usize::try_from(value).ok())
+ })
+ .or_else(|| {
+ common::number_path(result, &["receipt", "failed_count"])
+ .and_then(|value| usize::try_from(value).ok())
+ })
+ .unwrap_or(0);
+ let target = common::array(result, &["target_relays"])
+ .map(Vec::len)
+ .unwrap_or(0);
+ (acknowledged > 0 || failed > 0 || target > 0)
+ .then(|| relay_summary(acknowledged, failed, "acknowledged"))
+}
+
+fn first_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
+ paths.iter().find_map(|path| common::string(value, path))
+}
+
+fn listing_title(operation_id: &str) -> &'static str {
+ match operation_id {
+ "listing.update" => "Listing updated",
+ "listing.archive" => "Listing archived",
+ _ => "Listing",
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::out::envelope::EnvelopeContext;
+ use crate::out::terminal::renderer::render_terminal_document;
+ use serde_json::json;
+
+ use super::*;
+
+ #[test]
+ fn publish_receipt_omits_absent_proof_source() {
+ let envelope = OutputEnvelope::success(
+ "listing.publish",
+ json!({
+ "state": "published",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
+ "source": "SDK listing publish · configured signer",
+ "acknowledged_relays": ["wss://relay.example"],
+ "failed_relays": [],
+ "event_id": "9f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac12",
+ "actions": ["radroots listing get AAAAAAAAAAAAAAAAAAAAAg"]
+ }),
+ EnvelopeContext::new("req_listing_publish", false),
+ );
+ let document = LISTING_RENDERER.render(&envelope, &TerminalRenderContext::default());
+ let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
+
+ assert!(rendered.contains("✓ Listing published"));
+ assert!(rendered.contains("Listing"));
+ assert!(rendered.contains("Relays"));
+ assert!(rendered.contains("Event"));
+ assert!(!rendered.contains("Proof"));
+ assert!(!rendered.contains("verified"));
+ }
+
+ #[test]
+ fn publish_receipt_requires_verified_true_for_verified_claim() {
+ let envelope = OutputEnvelope::success(
+ "listing.publish",
+ json!({
+ "state": "published",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
+ "proof_verification": {
+ "state": "valid",
+ "cryptographic_proof_verified": false,
+ "proof_system": "SP1"
+ }
+ }),
+ EnvelopeContext::new("req_listing_publish", false),
+ );
+ let document = LISTING_RENDERER.render(&envelope, &TerminalRenderContext::default());
+ let rendered = render_terminal_document(&document, &TerminalRenderContext::default());
+
+ assert!(rendered.contains("Proof"));
+ assert!(!rendered.contains("Proof verified"));
+ assert!(!rendered.contains("verified ·"));
+ }
+}
diff --git a/src/out/terminal/renderers/mod.rs b/src/out/terminal/renderers/mod.rs
@@ -1,7 +1,9 @@
pub mod account;
pub mod common;
pub mod config;
+pub mod farm;
pub mod health;
+pub mod listing;
pub mod runtime;
pub mod store;
pub mod sync;