1#![forbid(unsafe_code)]
2mod harness;
5
6use std::collections::BTreeMap;
7use std::path::PathBuf;
8
9use schemars::JsonSchema;
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13
14use crate::contracts::diagnostics::DiagnosticRecord;
15use crate::contracts::{
16 ColorMode, CommandPath, ErrorDetailsV1, ErrorEnvelopeV1, ErrorPayloadV1, ExitCode, LogLevel,
17 Namespace, OutputEnvelopeMetaV1, OutputEnvelopeV1, OutputFormat, PrettyMode,
18 ProductCompatibilityWindow, ProductEntrypoint, ProductEntrypointKind, ProductMountDescriptor,
19};
20use crate::shared::output::{emit_error, emit_success, EmitterConfig};
21use crate::shared::version::runtime_semver;
22
23pub use harness::{BijuxCliHarness, HarnessRun, SnapshotHelper};
24
25#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27pub struct FeatureCapabilityDeclaration {
28 pub uses_config: bool,
29 pub uses_history: bool,
30 pub uses_memory: bool,
31 pub uses_plugins: bool,
32 pub supports_completion: bool,
33 pub supports_repl: bool,
34}
35
36impl FeatureCapabilityDeclaration {
37 #[must_use]
39 pub fn capability_labels(&self) -> Vec<String> {
40 let mut labels = Vec::new();
41 if self.uses_config {
42 labels.push("uses_config".to_string());
43 }
44 if self.uses_history {
45 labels.push("uses_history".to_string());
46 }
47 if self.uses_memory {
48 labels.push("uses_memory".to_string());
49 }
50 if self.uses_plugins {
51 labels.push("uses_plugins".to_string());
52 }
53 if self.supports_completion {
54 labels.push("supports_completion".to_string());
55 }
56 if self.supports_repl {
57 labels.push("supports_repl".to_string());
58 }
59 labels
60 }
61}
62
63pub type SdkCompatibilityWindow = ProductCompatibilityWindow;
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68pub struct SdkCompatibilityReport {
69 pub compatible: bool,
70 pub host_cli_version: String,
71 pub min_cli_version: String,
72 pub max_cli_version_exclusive: Option<String>,
73 pub reasons: Vec<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub struct BijuxAppMetadata {
79 pub namespace: String,
80 pub display_name: String,
81 pub aliases: Vec<String>,
82 pub summary: String,
83 pub version: Option<String>,
84 pub entrypoint_kind: ProductEntrypointKind,
85 pub entrypoint: String,
86 pub control_entrypoint_kind: ProductEntrypointKind,
87 pub control_entrypoint: String,
88 pub capabilities: Vec<String>,
89 pub feature_capabilities: FeatureCapabilityDeclaration,
90 pub compatibility: Option<SdkCompatibilityWindow>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ProductMount {
96 namespace: Namespace,
97 display_name: Option<String>,
98 aliases: Vec<String>,
99 runtime_entrypoint: Option<ProductEntrypoint>,
100 control_entrypoint: Option<ProductEntrypoint>,
101 summary: Option<String>,
102 version: Option<String>,
103 capabilities: Vec<String>,
104 feature_capabilities: FeatureCapabilityDeclaration,
105 compatibility: Option<SdkCompatibilityWindow>,
106}
107
108impl ProductMount {
109 pub fn new(raw_namespace: &str) -> Result<Self, String> {
111 Ok(Self {
112 namespace: Namespace::new(raw_namespace)?,
113 display_name: None,
114 aliases: Vec::new(),
115 runtime_entrypoint: None,
116 control_entrypoint: None,
117 summary: None,
118 version: None,
119 capabilities: Vec::new(),
120 feature_capabilities: FeatureCapabilityDeclaration::default(),
121 compatibility: None,
122 })
123 }
124
125 #[must_use]
126 pub fn display_name(mut self, value: impl Into<String>) -> Self {
127 self.display_name = Some(value.into());
128 self
129 }
130
131 #[must_use]
132 pub fn alias(mut self, value: impl Into<String>) -> Self {
133 self.aliases.push(value.into());
134 self
135 }
136
137 #[must_use]
138 pub fn summary(mut self, value: impl Into<String>) -> Self {
139 self.summary = Some(value.into());
140 self
141 }
142
143 #[must_use]
144 pub fn version(mut self, value: impl Into<String>) -> Self {
145 self.version = Some(value.into());
146 self
147 }
148
149 #[must_use]
150 pub fn capability(mut self, value: impl Into<String>) -> Self {
151 self.capabilities.push(value.into());
152 self
153 }
154
155 #[must_use]
156 pub fn feature_capabilities(mut self, value: FeatureCapabilityDeclaration) -> Self {
157 self.feature_capabilities = value;
158 self
159 }
160
161 #[must_use]
162 pub fn compatibility(mut self, value: SdkCompatibilityWindow) -> Self {
163 self.compatibility = Some(value);
164 self
165 }
166
167 #[must_use]
168 pub fn binary(self, command: impl Into<String>) -> Self {
169 self.runtime_entrypoint(ProductEntrypointKind::Binary, command)
170 }
171
172 #[must_use]
173 pub fn python_module(self, module: impl Into<String>) -> Self {
174 self.runtime_entrypoint(ProductEntrypointKind::PythonModule, module)
175 }
176
177 #[must_use]
178 pub fn python_callable(
179 mut self,
180 module: impl Into<String>,
181 function: impl Into<String>,
182 ) -> Self {
183 let module = module.into();
184 self.runtime_entrypoint = Some(ProductEntrypoint {
185 kind: ProductEntrypointKind::PythonModule,
186 command: module.clone(),
187 module: Some(module),
188 function: Some(function.into()),
189 });
190 self
191 }
192
193 #[must_use]
194 pub fn python_console_script(self, command: impl Into<String>) -> Self {
195 self.runtime_entrypoint(ProductEntrypointKind::PythonConsoleScript, command)
196 }
197
198 #[must_use]
199 pub fn plugin_process(self, command: impl Into<String>) -> Self {
200 self.runtime_entrypoint(ProductEntrypointKind::PluginProcess, command)
201 }
202
203 #[must_use]
204 pub fn embedded_rust(self, symbol: impl Into<String>) -> Self {
205 self.runtime_entrypoint(ProductEntrypointKind::EmbeddedRust, symbol)
206 }
207
208 #[must_use]
209 pub fn control_binary(self, command: impl Into<String>) -> Self {
210 self.control_entrypoint(ProductEntrypointKind::Binary, command)
211 }
212
213 #[must_use]
214 pub fn control_python_module(self, module: impl Into<String>) -> Self {
215 self.control_entrypoint(ProductEntrypointKind::PythonModule, module)
216 }
217
218 #[must_use]
219 pub fn control_python_callable(
220 mut self,
221 module: impl Into<String>,
222 function: impl Into<String>,
223 ) -> Self {
224 let module = module.into();
225 self.control_entrypoint = Some(ProductEntrypoint {
226 kind: ProductEntrypointKind::PythonModule,
227 command: module.clone(),
228 module: Some(module),
229 function: Some(function.into()),
230 });
231 self
232 }
233
234 #[must_use]
235 pub fn control_python_console_script(self, command: impl Into<String>) -> Self {
236 self.control_entrypoint(ProductEntrypointKind::PythonConsoleScript, command)
237 }
238
239 #[must_use]
240 pub fn control_plugin_process(self, command: impl Into<String>) -> Self {
241 self.control_entrypoint(ProductEntrypointKind::PluginProcess, command)
242 }
243
244 #[must_use]
245 pub fn control_embedded_rust(self, symbol: impl Into<String>) -> Self {
246 self.control_entrypoint(ProductEntrypointKind::EmbeddedRust, symbol)
247 }
248
249 #[must_use]
250 fn runtime_entrypoint(
251 mut self,
252 kind: ProductEntrypointKind,
253 command: impl Into<String>,
254 ) -> Self {
255 self.runtime_entrypoint =
256 Some(ProductEntrypoint { kind, command: command.into(), module: None, function: None });
257 self
258 }
259
260 #[must_use]
261 fn control_entrypoint(
262 mut self,
263 kind: ProductEntrypointKind,
264 command: impl Into<String>,
265 ) -> Self {
266 self.control_entrypoint =
267 Some(ProductEntrypoint { kind, command: command.into(), module: None, function: None });
268 self
269 }
270
271 #[must_use]
272 pub fn namespace(&self) -> &Namespace {
273 &self.namespace
274 }
275
276 #[must_use]
277 pub fn matches_query(&self, query: &str) -> bool {
278 let normalized = Namespace::normalize(query);
279 self.namespace.as_str() == normalized
280 || self.aliases.iter().any(|alias| Namespace::normalize(alias) == normalized)
281 }
282
283 pub fn build_descriptor(&self) -> Result<ProductMountDescriptor, String> {
285 let runtime_entrypoint = self
286 .runtime_entrypoint
287 .clone()
288 .ok_or_else(|| "product mount runtime entrypoint is required".to_string())?;
289 let control_entrypoint =
290 self.control_entrypoint.clone().unwrap_or_else(|| runtime_entrypoint.clone());
291 let display_name = self
292 .display_name
293 .clone()
294 .unwrap_or_else(|| default_display_name(self.namespace.as_str()));
295 let summary =
296 self.summary.clone().ok_or_else(|| "product mount summary is required".to_string())?;
297 let aliases = self
298 .aliases
299 .iter()
300 .map(|alias| Namespace::new(alias))
301 .collect::<Result<Vec<_>, _>>()?;
302
303 let mut builder = ProductMountDescriptor::builder(self.namespace.clone())
304 .display_name(display_name)
305 .entrypoint_value(runtime_entrypoint)
306 .control_entrypoint_value(control_entrypoint)
307 .help_summary(summary);
308
309 for alias in aliases {
310 builder = builder.alias(alias);
311 }
312 for capability in merged_capabilities(&self.capabilities, &self.feature_capabilities) {
313 builder = builder.capability(capability);
314 }
315 if let Some(version) = &self.version {
316 builder = builder.version(version.clone());
317 }
318 if let Some(compatibility) = &self.compatibility {
319 builder = builder.compatibility(compatibility.clone());
320 }
321 builder.build()
322 }
323
324 pub fn manifest_json(&self) -> Result<String, String> {
326 let descriptor = self.build_descriptor()?;
327 serde_json::to_string_pretty(&descriptor)
328 .map_err(|error| format!("failed to render product mount manifest JSON: {error}"))
329 }
330
331 pub fn metadata(&self) -> Result<BijuxAppMetadata, String> {
333 let descriptor = self.build_descriptor()?;
334 Ok(BijuxAppMetadata {
335 namespace: descriptor.namespace.as_str().to_string(),
336 display_name: descriptor.display_name,
337 aliases: descriptor.aliases.iter().map(|alias| alias.as_str().to_string()).collect(),
338 summary: descriptor.help.summary,
339 version: descriptor.version,
340 entrypoint_kind: descriptor.entrypoint.kind.clone(),
341 entrypoint: descriptor.entrypoint.command.clone(),
342 control_entrypoint_kind: descriptor.control_entrypoint.kind.clone(),
343 control_entrypoint: descriptor.control_entrypoint.command.clone(),
344 capabilities: descriptor.capabilities,
345 feature_capabilities: self.feature_capabilities.clone(),
346 compatibility: self.compatibility.clone(),
347 })
348 }
349
350 pub fn compatibility_report(&self) -> Result<Option<SdkCompatibilityReport>, String> {
352 let Some(window) = &self.compatibility else {
353 return Ok(None);
354 };
355
356 let host_cli_version = runtime_semver().to_string();
357 let host = Version::parse(&host_cli_version)
358 .map_err(|error| format!("host semver is invalid: {error}"))?;
359 let min = Version::parse(&window.min_cli_version)
360 .map_err(|error| format!("min_cli_version is invalid: {error}"))?;
361 let max = window
362 .max_cli_version_exclusive
363 .as_ref()
364 .map(|value| Version::parse(value))
365 .transpose()
366 .map_err(|error| format!("max_cli_version_exclusive is invalid: {error}"))?;
367
368 let mut reasons = Vec::new();
369 if host < min {
370 reasons.push(format!(
371 "host version `{host_cli_version}` is below required minimum `{}`",
372 window.min_cli_version
373 ));
374 }
375 if let Some(max_version) = &max {
376 if host >= *max_version {
377 reasons.push(format!(
378 "host version `{host_cli_version}` is not below exclusive maximum `{}`",
379 max_version
380 ));
381 }
382 }
383
384 Ok(Some(SdkCompatibilityReport {
385 compatible: reasons.is_empty(),
386 host_cli_version,
387 min_cli_version: window.min_cli_version.clone(),
388 max_cli_version_exclusive: window.max_cli_version_exclusive.clone(),
389 reasons,
390 }))
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
396pub struct CommandContext {
397 pub cwd: PathBuf,
398 pub project_root: Option<PathBuf>,
399 pub config_dirs: Vec<PathBuf>,
400 pub output_format: OutputFormat,
401 pub pretty_mode: PrettyMode,
402 pub color_mode: ColorMode,
403 pub verbosity: LogLevel,
404 pub quiet: bool,
405 pub invocation_id: String,
406 pub parent_command: CommandPath,
407}
408
409impl CommandContext {
410 #[must_use]
412 pub fn builder(parent_command: CommandPath) -> CommandContextBuilder {
413 CommandContextBuilder::new(parent_command)
414 }
415
416 pub fn command_path(&self, tail_segments: &[&str]) -> Result<CommandPath, String> {
418 let mut segments = self
419 .parent_command
420 .segments
421 .iter()
422 .map(|segment| segment.as_str().to_string())
423 .collect::<Vec<_>>();
424 segments.extend(tail_segments.iter().map(|segment| segment.to_string()));
425 let refs = segments.iter().map(String::as_str).collect::<Vec<_>>();
426 CommandPath::new(&refs)
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
432pub struct CommandContextBuilder {
433 cwd: PathBuf,
434 project_root: Option<PathBuf>,
435 config_dirs: Vec<PathBuf>,
436 output_format: OutputFormat,
437 pretty_mode: PrettyMode,
438 color_mode: ColorMode,
439 verbosity: LogLevel,
440 quiet: bool,
441 invocation_id: String,
442 parent_command: CommandPath,
443}
444
445impl CommandContextBuilder {
446 fn new(parent_command: CommandPath) -> Self {
447 Self {
448 cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
449 project_root: None,
450 config_dirs: Vec::new(),
451 output_format: OutputFormat::Json,
452 pretty_mode: PrettyMode::Pretty,
453 color_mode: ColorMode::Auto,
454 verbosity: LogLevel::Info,
455 quiet: false,
456 invocation_id: "bijux-sdk-invocation".to_string(),
457 parent_command,
458 }
459 }
460
461 #[must_use]
462 pub fn cwd(mut self, value: impl Into<PathBuf>) -> Self {
463 self.cwd = value.into();
464 self
465 }
466
467 #[must_use]
468 pub fn project_root(mut self, value: impl Into<PathBuf>) -> Self {
469 self.project_root = Some(value.into());
470 self
471 }
472
473 #[must_use]
474 pub fn config_dir(mut self, value: impl Into<PathBuf>) -> Self {
475 self.config_dirs.push(value.into());
476 self
477 }
478
479 #[must_use]
480 pub fn output_format(mut self, value: OutputFormat) -> Self {
481 self.output_format = value;
482 self
483 }
484
485 #[must_use]
486 pub fn pretty_mode(mut self, value: PrettyMode) -> Self {
487 self.pretty_mode = value;
488 self
489 }
490
491 #[must_use]
492 pub fn color_mode(mut self, value: ColorMode) -> Self {
493 self.color_mode = value;
494 self
495 }
496
497 #[must_use]
498 pub fn verbosity(mut self, value: LogLevel) -> Self {
499 self.verbosity = value;
500 self
501 }
502
503 #[must_use]
504 pub fn quiet(mut self, value: bool) -> Self {
505 self.quiet = value;
506 self
507 }
508
509 #[must_use]
510 pub fn invocation_id(mut self, value: impl Into<String>) -> Self {
511 self.invocation_id = value.into();
512 self
513 }
514
515 #[must_use]
516 pub fn build(self) -> CommandContext {
517 CommandContext {
518 cwd: self.cwd,
519 project_root: self.project_root,
520 config_dirs: self.config_dirs,
521 output_format: self.output_format,
522 pretty_mode: self.pretty_mode,
523 color_mode: self.color_mode,
524 verbosity: self.verbosity,
525 quiet: self.quiet,
526 invocation_id: self.invocation_id,
527 parent_command: self.parent_command,
528 }
529 }
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
534pub struct SdkRenderConfig {
535 pub format: OutputFormat,
536 pub pretty_mode: PrettyMode,
537 pub color_mode: ColorMode,
538 pub verbosity: LogLevel,
539 pub quiet: bool,
540 pub no_color: bool,
541}
542
543impl Default for SdkRenderConfig {
544 fn default() -> Self {
545 Self {
546 format: OutputFormat::Json,
547 pretty_mode: PrettyMode::Pretty,
548 color_mode: ColorMode::Never,
549 verbosity: LogLevel::Info,
550 quiet: false,
551 no_color: true,
552 }
553 }
554}
555
556impl From<SdkRenderConfig> for EmitterConfig {
557 fn from(value: SdkRenderConfig) -> Self {
558 Self {
559 format: value.format,
560 pretty: matches!(value.pretty_mode, PrettyMode::Pretty),
561 color: value.color_mode,
562 log_level: value.verbosity,
563 quiet: value.quiet,
564 no_color: value.no_color,
565 }
566 }
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
571#[serde(rename_all = "snake_case")]
572pub enum StreamPolicy {
573 Auto,
574 Always,
575 Never,
576}
577
578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
580#[serde(rename_all = "snake_case", tag = "kind", content = "envelope")]
581pub enum CommandEnvelope {
582 Success(OutputEnvelopeV1),
583 Error(ErrorEnvelopeV1),
584}
585
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
588pub struct CommandResult {
589 pub exit_code: ExitCode,
590 pub envelope: CommandEnvelope,
591 pub stdout_policy: StreamPolicy,
592 pub stderr_policy: StreamPolicy,
593}
594
595impl CommandResult {
596 #[must_use]
598 pub fn success(envelope: OutputEnvelopeV1) -> Self {
599 Self {
600 exit_code: ExitCode::Success,
601 envelope: CommandEnvelope::Success(envelope),
602 stdout_policy: StreamPolicy::Auto,
603 stderr_policy: StreamPolicy::Never,
604 }
605 }
606
607 #[must_use]
609 pub fn failure(exit_code: ExitCode, envelope: ErrorEnvelopeV1) -> Self {
610 Self {
611 exit_code,
612 envelope: CommandEnvelope::Error(envelope),
613 stdout_policy: StreamPolicy::Never,
614 stderr_policy: StreamPolicy::Auto,
615 }
616 }
617
618 #[must_use]
619 pub fn stdout_policy(mut self, value: StreamPolicy) -> Self {
620 self.stdout_policy = value;
621 self
622 }
623
624 #[must_use]
625 pub fn stderr_policy(mut self, value: StreamPolicy) -> Self {
626 self.stderr_policy = value;
627 self
628 }
629
630 pub fn render(&self, cfg: SdkRenderConfig) -> Result<RenderedCommandResult, String> {
632 let emitter = EmitterConfig::from(cfg);
633 match &self.envelope {
634 CommandEnvelope::Success(envelope) => {
635 let mut stdout = String::new();
636 if !matches!(self.stdout_policy, StreamPolicy::Never) {
637 let effective = if matches!(self.stdout_policy, StreamPolicy::Always) {
638 EmitterConfig { quiet: false, ..emitter }
639 } else {
640 emitter
641 };
642 if let Some(rendered) =
643 emit_success(envelope, effective).map_err(|error| error.to_string())?
644 {
645 stdout = rendered.content;
646 }
647 }
648 Ok(RenderedCommandResult {
649 exit_code: self.exit_code,
650 stdout,
651 stderr: String::new(),
652 })
653 }
654 CommandEnvelope::Error(envelope) => {
655 let stderr = if matches!(self.stderr_policy, StreamPolicy::Never) {
656 String::new()
657 } else {
658 emit_error(envelope, emitter).map_err(|error| error.to_string())?.content
659 };
660 Ok(RenderedCommandResult {
661 exit_code: self.exit_code,
662 stdout: String::new(),
663 stderr,
664 })
665 }
666 }
667 }
668}
669
670#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
672pub struct RenderedCommandResult {
673 pub exit_code: ExitCode,
674 pub stdout: String,
675 pub stderr: String,
676}
677
678#[derive(Debug, Clone, PartialEq)]
680pub struct DiagnosticRecordBuilder {
681 id: String,
682 severity: String,
683 message: Option<String>,
684 fields: BTreeMap<String, Value>,
685}
686
687impl DiagnosticRecordBuilder {
688 #[must_use]
689 pub fn new(id: impl Into<String>) -> Self {
690 Self {
691 id: id.into(),
692 severity: "error".to_string(),
693 message: None,
694 fields: BTreeMap::new(),
695 }
696 }
697
698 #[must_use]
699 pub fn severity(mut self, value: impl Into<String>) -> Self {
700 self.severity = value.into();
701 self
702 }
703
704 #[must_use]
705 pub fn message(mut self, value: impl Into<String>) -> Self {
706 self.message = Some(value.into());
707 self
708 }
709
710 #[must_use]
711 pub fn field(mut self, key: impl Into<String>, value: Value) -> Self {
712 self.fields.insert(key.into(), value);
713 self
714 }
715
716 pub fn build(self) -> Result<DiagnosticRecord, String> {
717 if self.id.trim().is_empty() {
718 return Err("diagnostic id cannot be empty".to_string());
719 }
720 if self.severity.trim().is_empty() {
721 return Err("diagnostic severity cannot be empty".to_string());
722 }
723 let message = self.message.ok_or_else(|| "diagnostic message is required".to_string())?;
724 if message.trim().is_empty() {
725 return Err("diagnostic message cannot be empty".to_string());
726 }
727 Ok(DiagnosticRecord { id: self.id, severity: self.severity, message, fields: self.fields })
728 }
729}
730
731#[derive(Debug, Clone, PartialEq)]
733pub struct CommandFailureBuilder {
734 code: String,
735 category: String,
736 message: Option<String>,
737 failure: Option<String>,
738 context: BTreeMap<String, Value>,
739}
740
741impl CommandFailureBuilder {
742 #[must_use]
743 pub fn new(code: impl Into<String>, category: impl Into<String>) -> Self {
744 Self {
745 code: code.into(),
746 category: category.into(),
747 message: None,
748 failure: None,
749 context: BTreeMap::new(),
750 }
751 }
752
753 #[must_use]
754 pub fn message(mut self, value: impl Into<String>) -> Self {
755 self.message = Some(value.into());
756 self
757 }
758
759 #[must_use]
760 pub fn failure(mut self, value: impl Into<String>) -> Self {
761 self.failure = Some(value.into());
762 self
763 }
764
765 #[must_use]
766 pub fn context(mut self, key: impl Into<String>, value: Value) -> Self {
767 self.context.insert(key.into(), value);
768 self
769 }
770
771 pub fn build(self) -> Result<ErrorPayloadV1, String> {
772 let message = self.message.ok_or_else(|| "error message is required".to_string())?;
773 let mut payload = ErrorPayloadV1::new(&self.code, &message, &self.category)?;
774 if self.failure.is_some() || !self.context.is_empty() {
775 payload.details = Some(ErrorDetailsV1 { failure: self.failure, context: self.context });
776 }
777 Ok(payload)
778 }
779}
780
781pub struct OutputEnvelopeHelper;
783
784impl OutputEnvelopeHelper {
785 pub fn success(
786 command: CommandPath,
787 data: Value,
788 timestamp: &str,
789 ) -> Result<OutputEnvelopeV1, String> {
790 let meta = OutputEnvelopeMetaV1::new("v1", command, timestamp)?;
791 Ok(OutputEnvelopeV1::success(data, meta))
792 }
793
794 pub fn failure(
795 command: CommandPath,
796 error: ErrorPayloadV1,
797 timestamp: &str,
798 ) -> Result<ErrorEnvelopeV1, String> {
799 let meta = OutputEnvelopeMetaV1::new("v1", command, timestamp)?;
800 Ok(ErrorEnvelopeV1::failure(error, meta))
801 }
802
803 #[must_use]
804 pub fn json(value: Value) -> Value {
805 value
806 }
807
808 #[must_use]
809 pub fn text(message: impl Into<String>) -> Value {
810 json!({ "message": message.into() })
811 }
812
813 pub fn table(columns: &[&str], rows: &[Vec<Value>]) -> Result<Value, String> {
814 if columns.is_empty() {
815 return Err("table columns cannot be empty".to_string());
816 }
817 for row in rows {
818 if row.len() != columns.len() {
819 return Err("table rows must match the column count".to_string());
820 }
821 }
822 Ok(json!({
823 "kind": "table",
824 "columns": columns,
825 "rows": rows,
826 }))
827 }
828
829 #[must_use]
830 pub fn quiet() -> Value {
831 json!({})
832 }
833}
834
835pub trait BijuxApp {
837 fn mount(&self) -> ProductMount;
838 fn route(&self, argv: &[String], ctx: &CommandContext) -> CommandResult;
839
840 fn namespace(&self) -> String {
841 self.mount().namespace().as_str().to_string()
842 }
843
844 fn metadata(&self) -> Result<BijuxAppMetadata, String> {
845 self.mount().metadata()
846 }
847
848 fn manifest_descriptor(&self) -> Result<ProductMountDescriptor, String> {
849 self.mount().build_descriptor()
850 }
851}
852
853fn default_display_name(namespace: &str) -> String {
854 namespace
855 .split('-')
856 .filter(|segment| !segment.is_empty())
857 .map(|segment| {
858 let mut chars = segment.chars();
859 match chars.next() {
860 Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
861 None => String::new(),
862 }
863 })
864 .collect::<Vec<_>>()
865 .join(" ")
866}
867
868fn merged_capabilities(
869 capabilities: &[String],
870 feature_capabilities: &FeatureCapabilityDeclaration,
871) -> Vec<String> {
872 let mut merged = capabilities.to_vec();
873 merged.extend(feature_capabilities.capability_labels());
874 merged.sort();
875 merged.dedup();
876 merged
877}