1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::process;
4use std::sync::Arc;
5
6use harn_vm::event_log::{AnyEventLog, EventLog};
7
8use crate::cli::{
9 PersonaCheckArgs, PersonaControlArgs, PersonaInspectArgs, PersonaListArgs, PersonaSpendArgs,
10 PersonaStatusArgs, PersonaTickArgs, PersonaTriggerArgs,
11};
12use crate::package::{self, PersonaValidationError, ResolvedPersonaManifest};
13
14pub fn list_payload(manifest: Option<&Path>) -> Result<Vec<serde_json::Value>, String> {
18 Ok(package::load_discoverable_personas(manifest)?
19 .iter()
20 .map(discoverable_persona_to_json)
21 .collect())
22}
23
24pub(crate) fn run_list(manifest: Option<&Path>, args: &PersonaListArgs) {
25 if args.json {
26 let personas = list_payload(manifest).unwrap_or_else(|error| fatal(&error));
27 println!(
28 "{}",
29 serde_json::to_string_pretty(&personas)
30 .unwrap_or_else(|error| fatal(&format!("failed to serialize personas: {error}")))
31 );
32 return;
33 }
34
35 let personas =
36 package::load_discoverable_personas(manifest).unwrap_or_else(|error| fatal(&error));
37 if personas.is_empty() {
38 println!("No personas found.");
39 return;
40 }
41
42 println!("Personas:");
43 let name_width = personas
44 .iter()
45 .map(|persona| persona.id.len())
46 .max()
47 .unwrap_or(4);
48 for discovered in &personas {
49 let persona = &discovered.persona;
50 let tier = persona
51 .autonomy_tier
52 .map(|tier| tier.as_str())
53 .unwrap_or("<missing>");
54 let receipts = persona
55 .receipt_policy
56 .map(|policy| policy.as_str())
57 .unwrap_or("<missing>");
58 let entry = persona.entry_workflow.as_deref().unwrap_or("<missing>");
59 println!(
60 " {:<name_width$} tier={tier:<17} receipts={receipts:<8} entry={entry}",
61 discovered.id
62 );
63 }
64}
65
66pub fn check_payload(
70 path: Option<&Path>,
71) -> Result<serde_json::Value, Vec<PersonaValidationError>> {
72 let catalog = load_catalog_validation(path)?;
73 Ok(serde_json::json!({
74 "ok": true,
75 "manifest_path": catalog.manifest_path,
76 "personas": catalog.personas.iter().map(|persona| {
77 serde_json::json!({
78 "name": persona.name.as_deref().unwrap_or_default(),
79 "triggers": &persona.triggers,
80 "tools": &persona.tools,
81 "autonomy": persona.autonomy_tier.map(|tier| tier.as_str()).unwrap_or_default(),
82 "receipts": persona.receipt_policy.map(|policy| policy.as_str()).unwrap_or_default(),
83 })
84 }).collect::<Vec<_>>(),
85 }))
86}
87
88pub(crate) fn run_check(manifest: Option<&Path>, args: &PersonaCheckArgs) {
89 let selected = args.path.as_deref().or(manifest);
90 if args.json {
91 match check_payload(selected) {
92 Ok(payload) => println!(
93 "{}",
94 serde_json::to_string_pretty(&payload).unwrap_or_else(|error| fatal(&format!(
95 "failed to serialize persona check output: {error}"
96 )))
97 ),
98 Err(errors) => {
99 print_validation_errors_json(&errors);
100 process::exit(1);
101 }
102 }
103 return;
104 }
105 let catalog = match load_catalog_validation(selected) {
106 Ok(catalog) => catalog,
107 Err(errors) => fatal(
108 &errors
109 .iter()
110 .map(ToString::to_string)
111 .collect::<Vec<_>>()
112 .join("\n"),
113 ),
114 };
115 println!(
116 "ok: {} persona manifest validates ({} personas)",
117 catalog.manifest_path.display(),
118 catalog.personas.len()
119 );
120}
121
122pub fn inspect_payload(manifest: Option<&Path>, name: &str) -> Result<serde_json::Value, String> {
124 let persona = package::resolve_discoverable_persona(manifest, name)?;
125 Ok(discoverable_persona_to_json(&persona))
126}
127
128pub(crate) fn run_inspect(manifest: Option<&Path>, args: &PersonaInspectArgs) {
129 if args.json {
130 let json = inspect_payload(manifest, &args.name).unwrap_or_else(|error| fatal(&error));
131 println!(
132 "{}",
133 serde_json::to_string_pretty(&json)
134 .unwrap_or_else(|error| fatal(&format!("failed to serialize persona: {error}")))
135 );
136 return;
137 }
138
139 let discovered = package::resolve_discoverable_persona(manifest, &args.name)
140 .unwrap_or_else(|error| fatal(&error));
141 let persona = &discovered.persona;
142
143 println!(
144 "name: {}",
145 persona.name.as_deref().unwrap_or_default()
146 );
147 if let Some(version) = &persona.version {
148 println!("version: {version}");
149 }
150 println!(
151 "description: {}",
152 persona.description.as_deref().unwrap_or_default()
153 );
154 println!(
155 "entry_workflow: {}",
156 persona.entry_workflow.as_deref().unwrap_or_default()
157 );
158 println!("tools: {}", comma_or_dash(&persona.tools));
159 println!("capabilities: {}", comma_or_dash(&persona.capabilities));
160 println!(
161 "autonomy_tier: {}",
162 persona
163 .autonomy_tier
164 .map(|tier| tier.as_str())
165 .unwrap_or_default()
166 );
167 println!(
168 "receipt_policy: {}",
169 persona
170 .receipt_policy
171 .map(|policy| policy.as_str())
172 .unwrap_or_default()
173 );
174 println!("triggers: {}", comma_or_dash(&persona.triggers));
175 println!("schedules: {}", comma_or_dash(&persona.schedules));
176 println!("handoffs: {}", comma_or_dash(&persona.handoffs));
177 println!("context_packs: {}", comma_or_dash(&persona.context_packs));
178 println!("evals: {}", comma_or_dash(&persona.evals));
179 if !persona.steps.is_empty() {
180 println!("steps:");
181 for step in &persona.steps {
182 let mut detail = format!(" - {} ({})", step.name, step.function);
183 if let Some(model) = step.model.as_deref() {
184 detail.push_str(&format!(" model={model}"));
185 }
186 if let Some(budget) = step.budget.as_ref() {
187 if let Some(max_tokens) = budget.max_tokens {
188 detail.push_str(&format!(" max_tokens={max_tokens}"));
189 }
190 if let Some(max_usd) = budget.max_usd {
191 detail.push_str(&format!(" max_usd={max_usd}"));
192 }
193 }
194 if let Some(boundary) = step.error_boundary.as_deref() {
195 detail.push_str(&format!(" error_boundary={boundary}"));
196 }
197 println!("{detail}");
198 }
199 }
200 if !persona.stages.is_empty() {
201 println!("stages:");
202 for stage in &persona.stages {
203 let tools = stage
204 .allowed_tools
205 .as_deref()
206 .map(comma_or_dash)
207 .unwrap_or_else(|| "inherit".to_string());
208 let mut detail = format!(" - {} tools={tools}", stage.name);
209 if let Some(level) = stage.side_effect_level.as_deref() {
210 detail.push_str(&format!(" side_effect={level}"));
211 }
212 if let Some(max) = stage.max_iterations {
213 detail.push_str(&format!(" max_iterations={max}"));
214 }
215 println!("{detail}");
216 }
217 }
218 if let Some(owner) = &persona.owner {
219 println!("owner: {owner}");
220 }
221 if let Some(provenance) = discovered.installed_provenance() {
222 println!("package: {}", provenance.package_alias);
223 }
224 println!("manifest: {}", discovered.manifest_path.display());
225}
226
227pub async fn status_payload(
229 manifest: Option<&Path>,
230 state_dir: &Path,
231 name: &str,
232 at: Option<&str>,
233) -> Result<harn_vm::PersonaStatus, String> {
234 let binding = runtime_binding_or_err(manifest, name)?;
235 let log = open_persona_log(state_dir)?;
236 let now_ms = timestamp_arg(at)?;
237 harn_vm::persona_status(&log, &binding, now_ms).await
238}
239
240pub(crate) async fn run_status(
241 manifest: Option<&Path>,
242 state_dir: &Path,
243 args: &PersonaStatusArgs,
244) -> Result<(), String> {
245 let status = status_payload(manifest, state_dir, &args.name, args.at.as_deref()).await?;
246 print_status(&status, args.json);
247 Ok(())
248}
249
250pub async fn pause_payload(
252 manifest: Option<&Path>,
253 state_dir: &Path,
254 name: &str,
255 at: Option<&str>,
256) -> Result<harn_vm::PersonaStatus, String> {
257 let binding = runtime_binding_or_err(manifest, name)?;
258 let log = open_persona_log(state_dir)?;
259 let now_ms = timestamp_arg(at)?;
260 harn_vm::pause_persona(&log, &binding, now_ms).await
261}
262
263pub(crate) async fn run_pause(
264 manifest: Option<&Path>,
265 state_dir: &Path,
266 args: &PersonaControlArgs,
267) -> Result<(), String> {
268 let status = pause_payload(manifest, state_dir, &args.name, args.at.as_deref()).await?;
269 print_status(&status, args.json);
270 Ok(())
271}
272
273pub async fn resume_payload(
275 manifest: Option<&Path>,
276 state_dir: &Path,
277 name: &str,
278 at: Option<&str>,
279) -> Result<harn_vm::PersonaStatus, String> {
280 let binding = runtime_binding_or_err(manifest, name)?;
281 let log = open_persona_log(state_dir)?;
282 let now_ms = timestamp_arg(at)?;
283 harn_vm::resume_persona(&log, &binding, now_ms).await
284}
285
286pub(crate) async fn run_resume(
287 manifest: Option<&Path>,
288 state_dir: &Path,
289 args: &PersonaControlArgs,
290) -> Result<(), String> {
291 let status = resume_payload(manifest, state_dir, &args.name, args.at.as_deref()).await?;
292 print_status(&status, args.json);
293 Ok(())
294}
295
296pub async fn disable_payload(
298 manifest: Option<&Path>,
299 state_dir: &Path,
300 name: &str,
301 at: Option<&str>,
302) -> Result<harn_vm::PersonaStatus, String> {
303 let binding = runtime_binding_or_err(manifest, name)?;
304 let log = open_persona_log(state_dir)?;
305 let now_ms = timestamp_arg(at)?;
306 harn_vm::disable_persona(&log, &binding, now_ms).await
307}
308
309pub(crate) async fn run_disable(
310 manifest: Option<&Path>,
311 state_dir: &Path,
312 args: &PersonaControlArgs,
313) -> Result<(), String> {
314 let status = disable_payload(manifest, state_dir, &args.name, args.at.as_deref()).await?;
315 print_status(&status, args.json);
316 Ok(())
317}
318
319pub async fn tick_payload(
322 manifest: Option<&Path>,
323 state_dir: &Path,
324 name: &str,
325 at: Option<&str>,
326 cost_usd: f64,
327 tokens: u64,
328) -> Result<harn_vm::PersonaRunReceipt, String> {
329 let binding = runtime_binding_or_err(manifest, name)?;
330 let log = open_persona_log(state_dir)?;
331 let now_ms = timestamp_arg(at)?;
332 let receipt = harn_vm::fire_persona_schedule(
333 &log,
334 &binding,
335 harn_vm::PersonaRunCost {
336 cost_usd,
337 tokens,
338 ..Default::default()
339 },
340 now_ms,
341 )
342 .await?;
343 log.flush().await.map_err(|error| error.to_string())?;
344 Ok(receipt)
345}
346
347pub(crate) async fn run_tick(
348 manifest: Option<&Path>,
349 state_dir: &Path,
350 args: &PersonaTickArgs,
351) -> Result<(), String> {
352 let receipt = tick_payload(
353 manifest,
354 state_dir,
355 &args.name,
356 args.at.as_deref(),
357 args.cost_usd,
358 args.tokens,
359 )
360 .await?;
361 print_receipt(&receipt, args.json);
362 Ok(())
363}
364
365#[allow(clippy::too_many_arguments)]
368pub async fn trigger_payload(
369 manifest: Option<&Path>,
370 state_dir: &Path,
371 name: &str,
372 provider: &str,
373 kind: &str,
374 metadata_pairs: &[String],
375 at: Option<&str>,
376 cost_usd: f64,
377 tokens: u64,
378) -> Result<harn_vm::PersonaRunReceipt, String> {
379 let binding = runtime_binding_or_err(manifest, name)?;
380 let log = open_persona_log(state_dir)?;
381 let now_ms = timestamp_arg(at)?;
382 let metadata = parse_metadata(metadata_pairs)?;
383 let receipt = harn_vm::fire_persona_trigger(
384 &log,
385 &binding,
386 provider,
387 kind,
388 metadata,
389 harn_vm::PersonaRunCost {
390 cost_usd,
391 tokens,
392 ..Default::default()
393 },
394 now_ms,
395 )
396 .await?;
397 log.flush().await.map_err(|error| error.to_string())?;
398 Ok(receipt)
399}
400
401pub(crate) async fn run_trigger(
402 manifest: Option<&Path>,
403 state_dir: &Path,
404 args: &PersonaTriggerArgs,
405) -> Result<(), String> {
406 let receipt = trigger_payload(
407 manifest,
408 state_dir,
409 &args.name,
410 &args.provider,
411 &args.kind,
412 &args.metadata,
413 args.at.as_deref(),
414 args.cost_usd,
415 args.tokens,
416 )
417 .await?;
418 print_receipt(&receipt, args.json);
419 Ok(())
420}
421
422pub async fn spend_payload(
424 manifest: Option<&Path>,
425 state_dir: &Path,
426 name: &str,
427 at: Option<&str>,
428 cost_usd: f64,
429 tokens: u64,
430) -> Result<harn_vm::PersonaBudgetStatus, String> {
431 let binding = runtime_binding_or_err(manifest, name)?;
432 let log = open_persona_log(state_dir)?;
433 let now_ms = timestamp_arg(at)?;
434 let budget = harn_vm::record_persona_spend(
435 &log,
436 &binding,
437 harn_vm::PersonaRunCost {
438 cost_usd,
439 tokens,
440 ..Default::default()
441 },
442 now_ms,
443 )
444 .await?;
445 log.flush().await.map_err(|error| error.to_string())?;
446 Ok(budget)
447}
448
449pub(crate) async fn run_spend(
450 manifest: Option<&Path>,
451 state_dir: &Path,
452 args: &PersonaSpendArgs,
453) -> Result<(), String> {
454 let budget = spend_payload(
455 manifest,
456 state_dir,
457 &args.name,
458 args.at.as_deref(),
459 args.cost_usd,
460 args.tokens,
461 )
462 .await?;
463 if args.json {
464 println!(
465 "{}",
466 serde_json::to_string_pretty(&budget)
467 .unwrap_or_else(|error| fatal(&format!("failed to serialize budget: {error}")))
468 );
469 } else {
470 println!(
471 "budget: spent_today=${:.4} tokens_today={} exhausted={}",
472 budget.spent_today_usd, budget.tokens_today, budget.exhausted
473 );
474 }
475 Ok(())
476}
477
478fn load_catalog_validation(
479 manifest: Option<&Path>,
480) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
481 let result = if let Some(path) = manifest {
482 package::load_personas_from_manifest_path(path).map(Some)
483 } else {
484 package::load_personas_config(None)
485 };
486 match result {
487 Ok(Some(catalog)) => Ok(catalog),
488 Ok(None) => Err(vec![PersonaValidationError {
489 manifest_path: PathBuf::from("harn.toml"),
490 field_path: "harn.toml".to_string(),
491 message: "no harn.toml found; pass --manifest <path> or run inside a Harn project"
492 .to_string(),
493 }]),
494 Err(errors) => Err(errors),
495 }
496}
497
498fn runtime_binding_or_err(
499 manifest: Option<&Path>,
500 name: &str,
501) -> Result<harn_vm::PersonaRuntimeBinding, String> {
502 if let Some(manifest) = manifest {
503 let catalog = package::load_personas_from_manifest_path(manifest).map_err(|errors| {
504 errors
505 .iter()
506 .map(ToString::to_string)
507 .collect::<Vec<_>>()
508 .join("\n")
509 })?;
510 if let Some(extensions) =
511 package::try_load_runtime_extensions_from_manifest(&catalog.manifest_path)
512 .map_err(|error| error.to_string())?
513 {
514 if let Some(resolved) = extensions
515 .runtime_personas
516 .iter()
517 .find(|persona| persona.id == name)
518 {
519 return Ok(crate::package::persona_runtime_binding(
520 &resolved.id,
521 &resolved.persona,
522 ));
523 }
524 }
525 let persona = catalog
526 .personas
527 .iter()
528 .find(|persona| persona.name.as_deref() == Some(name))
529 .ok_or_else(|| format!("active runtime persona '{name}' not found"))?;
530 return Ok(crate::package::persona_runtime_binding(name, persona));
531 }
532
533 let extensions =
534 package::try_load_runtime_extensions(Path::new(".")).map_err(|error| error.to_string())?;
535 let resolved = extensions
536 .runtime_personas
537 .iter()
538 .find(|persona| persona.id == name)
539 .ok_or_else(|| format!("active runtime persona '{name}' not found"))?;
540 Ok(crate::package::persona_runtime_binding(
541 &resolved.id,
542 &resolved.persona,
543 ))
544}
545
546pub(super) fn open_persona_log(state_dir: &Path) -> Result<Arc<AnyEventLog>, String> {
547 let state_dir = absolutize_from_cwd(state_dir)?;
548 std::fs::create_dir_all(&state_dir).map_err(|error| {
549 format!(
550 "failed to create persona state dir {}: {error}",
551 state_dir.display()
552 )
553 })?;
554 harn_vm::event_log::install_default_for_base_dir(&state_dir)
555 .map_err(|error| format!("failed to open persona event log: {error}"))
556}
557
558fn absolutize_from_cwd(path: &Path) -> Result<PathBuf, String> {
559 if path.is_absolute() {
560 return Ok(path.to_path_buf());
561 }
562 std::env::current_dir()
563 .map(|cwd| cwd.join(path))
564 .map_err(|error| format!("failed to read current directory: {error}"))
565}
566
567pub(super) fn timestamp_arg(value: Option<&str>) -> Result<i64, String> {
568 match value {
569 Some(value) => harn_vm::parse_persona_ms(value),
570 None => Ok(harn_vm::persona_now_ms()),
571 }
572}
573
574fn parse_metadata(values: &[String]) -> Result<BTreeMap<String, String>, String> {
575 let mut metadata = BTreeMap::new();
576 for value in values {
577 let Some((key, raw)) = value.split_once('=') else {
578 return Err(format!("metadata '{value}' must use KEY=VALUE syntax"));
579 };
580 let key = key.trim();
581 if key.is_empty() {
582 return Err(format!("metadata '{value}' has an empty key"));
583 }
584 metadata.insert(key.to_string(), raw.to_string());
585 }
586 Ok(metadata)
587}
588
589fn print_status(status: &harn_vm::PersonaStatus, json: bool) {
590 if json {
591 println!(
592 "{}",
593 serde_json::to_string_pretty(status)
594 .unwrap_or_else(|error| fatal(&format!("failed to serialize status: {error}")))
595 );
596 return;
597 }
598 println!("persona: {}", status.name);
599 println!(
600 "template_ref: {}",
601 status.template_ref.as_deref().unwrap_or("-")
602 );
603 println!("state: {}", status.state.as_str());
604 println!("entry_workflow: {}", status.entry_workflow);
605 println!("role: {}", status.role);
606 println!(
607 "assignment: {}",
608 status
609 .current_assignment
610 .as_ref()
611 .map(|assignment| assignment.work_key.as_str())
612 .unwrap_or("-")
613 );
614 println!(
615 "last_run: {}",
616 status.last_run.as_deref().unwrap_or("-")
617 );
618 println!(
619 "next_run: {}",
620 status.next_scheduled_run.as_deref().unwrap_or("-")
621 );
622 println!("queued_events: {}", status.queued_events);
623 if !status.handoff_inbox.is_empty() {
624 println!("handoffs:");
625 for handoff in &status.handoff_inbox {
626 println!(
627 " - {} kind={} from={} task={}",
628 handoff
629 .handoff_id
630 .as_deref()
631 .unwrap_or(handoff.work_key.as_str()),
632 handoff.handoff_kind.as_deref().unwrap_or("-"),
633 handoff.source_persona.as_deref().unwrap_or("-"),
634 handoff.task.as_deref().unwrap_or("-")
635 );
636 }
637 }
638 println!(
639 "active_lease: {}",
640 status
641 .active_lease
642 .as_ref()
643 .map(|lease| lease.id.as_str())
644 .unwrap_or("-")
645 );
646 println!(
647 "budget: spent_today=${:.4} remaining_today={}",
648 status.budget.spent_today_usd,
649 status
650 .budget
651 .remaining_today_usd
652 .map(|value| format!("${value:.4}"))
653 .unwrap_or_else(|| "-".to_string())
654 );
655 if let Some(receipt) = status.value_receipts.last() {
656 println!(
657 "last_receipt: {} paid=${:.4} avoided=${:.4}",
658 receipt.kind.as_str(),
659 receipt.paid_cost_usd,
660 receipt.avoided_cost_usd
661 );
662 }
663 if let Some(error) = &status.last_error {
664 println!("last_error: {error}");
665 }
666}
667
668fn print_receipt(receipt: &harn_vm::PersonaRunReceipt, json: bool) {
669 if json {
670 println!(
671 "{}",
672 serde_json::to_string_pretty(receipt)
673 .unwrap_or_else(|error| fatal(&format!("failed to serialize receipt: {error}")))
674 );
675 } else {
676 println!(
677 "persona={} status={} work_key={} queued={}",
678 receipt.persona, receipt.status, receipt.work_key, receipt.queued
679 );
680 if let Some(error) = &receipt.error {
681 println!("error={error}");
682 }
683 }
684}
685
686fn print_validation_errors_json(errors: &[PersonaValidationError]) {
687 let payload = serde_json::json!({
688 "ok": false,
689 "errors": errors.iter().map(|error| {
690 serde_json::json!({
691 "manifest_path": &error.manifest_path,
692 "field_path": &error.field_path,
693 "message": &error.message,
694 })
695 }).collect::<Vec<_>>(),
696 });
697 println!(
698 "{}",
699 serde_json::to_string_pretty(&payload).unwrap_or_else(|error| {
700 fatal(&format!(
701 "failed to serialize persona validation errors: {error}"
702 ))
703 })
704 );
705}
706
707fn discoverable_persona_to_json(discovered: &package::DiscoverablePersona) -> serde_json::Value {
708 let persona = &discovered.persona;
709 let installed = discovered.installed_provenance();
710 let source_kind = if installed.is_some() {
711 "installed_package"
712 } else {
713 "root"
714 };
715 serde_json::json!({
716 "id": &discovered.id,
717 "name": persona.name.as_deref().unwrap_or_default(),
718 "version": persona.version.as_deref(),
719 "description": persona.description.as_deref().unwrap_or_default(),
720 "entry_workflow": persona.entry_workflow.as_deref().unwrap_or_default(),
721 "tools": &persona.tools,
722 "capabilities": &persona.capabilities,
723 "autonomy_tier": persona.autonomy_tier.map(|tier| tier.as_str()).unwrap_or_default(),
724 "receipt_policy": persona.receipt_policy.map(|policy| policy.as_str()).unwrap_or_default(),
725 "triggers": &persona.triggers,
726 "schedules": &persona.schedules,
727 "model_policy": {
728 "default_model": persona.model_policy.default_model.as_deref(),
729 "escalation_model": persona.model_policy.escalation_model.as_deref(),
730 "fallback_models": &persona.model_policy.fallback_models,
731 "effort": persona.model_policy.effort.as_deref(),
732 },
733 "budget": {
734 "daily_usd": persona.budget.daily_usd,
735 "hourly_usd": persona.budget.hourly_usd,
736 "run_usd": persona.budget.run_usd,
737 "frontier_escalations": persona.budget.frontier_escalations,
738 "max_tokens": persona.budget.max_tokens,
739 "max_runtime_seconds": persona.budget.max_runtime_seconds,
740 },
741 "handoffs": &persona.handoffs,
742 "context_packs": &persona.context_packs,
743 "evals": &persona.evals,
744 "steps": &persona.steps,
745 "stages": &persona.stages,
746 "owner": persona.owner.as_deref(),
747 "package_source": {
748 "package": persona.package_source.package.as_deref(),
749 "path": persona.package_source.path.as_deref(),
750 "git": persona.package_source.git.as_deref(),
751 "rev": persona.package_source.rev.as_deref(),
752 },
753 "rollout_policy": {
754 "mode": persona.rollout_policy.mode.as_deref(),
755 "percentage": persona.rollout_policy.percentage,
756 "cohorts": &persona.rollout_policy.cohorts,
757 },
758 "source": {
759 "kind": source_kind,
760 "manifest_path": &discovered.manifest_path,
761 "manifest_dir": &discovered.manifest_dir,
762 "package_alias": installed.map(|value| value.package_alias.as_str()),
763 "package_version": installed.and_then(|value| value.package_version.as_deref()),
764 "content_hash": installed.and_then(|value| value.content_hash.as_deref()),
765 "integrity": installed.map(|value| value.integrity.as_str()),
766 "package_source": installed.map(|value| value.source.as_str()),
767 },
768 })
769}
770
771fn comma_or_dash(values: &[String]) -> String {
772 if values.is_empty() {
773 "-".to_string()
774 } else {
775 values.join(", ")
776 }
777}
778
779fn fatal(message: &str) -> ! {
780 eprintln!("error: {message}");
781 process::exit(1);
782}