1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use greentic_runner::desktop::{
6 HttpMock, HttpMockMode, MocksConfig, OtlpHook, Runner, SigningPolicy, ToolsMock,
7};
8use serde_json::{Value as JsonValue, json};
9
10#[derive(Debug, Clone)]
11pub struct PackRunConfig<'a> {
12 pub pack_path: &'a Path,
13 pub entry: Option<String>,
14 pub input: Option<String>,
15 pub policy: RunPolicy,
16 pub otlp: Option<String>,
17 pub allow_hosts: Option<Vec<String>>,
18 pub mocks: MockSetting,
19 pub artifacts_dir: Option<&'a Path>,
20}
21
22#[derive(Debug, Clone, Copy)]
23pub enum RunPolicy {
24 Strict,
25 DevOk,
26}
27
28#[derive(Debug, Clone, Copy)]
29pub enum MockSetting {
30 On,
31 Off,
32}
33
34pub fn run(config: PackRunConfig<'_>) -> Result<()> {
35 let input_value = parse_input(config.input)?;
36 let otlp_hook = config.otlp.map(|endpoint| OtlpHook {
37 endpoint,
38 headers: Vec::new(),
39 sample_all: true,
40 });
41 let allow_hosts = config.allow_hosts.unwrap_or_default();
42 let mocks_config = build_mocks_config(config.mocks, allow_hosts)?;
43
44 let artifacts_override = config.artifacts_dir.map(|dir| dir.to_path_buf());
45 if let Some(dir) = &artifacts_override {
46 fs::create_dir_all(dir)
47 .with_context(|| format!("failed to create artifacts directory {}", dir.display()))?;
48 }
49
50 let runner = Runner::new();
51 let run_result = runner
52 .run_pack_with(config.pack_path, |opts| {
53 opts.entry_flow = config.entry.clone();
54 opts.input = input_value.clone();
55 opts.signing = signing_policy(config.policy);
56 if let Some(hook) = otlp_hook.clone() {
57 opts.otlp = Some(hook);
58 }
59 opts.mocks = mocks_config.clone();
60 opts.artifacts_dir = artifacts_override.clone();
61 })
62 .context("pack execution failed")?;
63
64 let rendered =
65 serde_json::to_string_pretty(&run_result).context("failed to render run result JSON")?;
66 println!("{rendered}");
67
68 Ok(())
69}
70
71fn parse_input(input: Option<String>) -> Result<JsonValue> {
72 if let Some(raw) = input {
73 if raw.trim().is_empty() {
74 return Ok(json!({}));
75 }
76 serde_json::from_str(&raw).context("failed to parse --input JSON")
77 } else {
78 Ok(json!({}))
79 }
80}
81
82fn build_mocks_config(setting: MockSetting, allow_hosts: Vec<String>) -> Result<MocksConfig> {
83 let mut config = MocksConfig {
84 net_allowlist: allow_hosts
85 .into_iter()
86 .map(|host| host.trim().to_ascii_lowercase())
87 .filter(|host| !host.is_empty())
88 .collect(),
89 ..MocksConfig::default()
90 };
91
92 if matches!(setting, MockSetting::On) {
93 config.http = Some(HttpMock {
94 record_replay_dir: None,
95 mode: HttpMockMode::RecordReplay,
96 rewrites: Vec::new(),
97 });
98
99 let tools_dir = PathBuf::from(".greentic").join("mocks").join("tools");
100 fs::create_dir_all(&tools_dir)
101 .with_context(|| format!("failed to create {}", tools_dir.display()))?;
102 config.mcp_tools = Some(ToolsMock {
103 directory: None,
104 script_dir: Some(tools_dir),
105 short_circuit: true,
106 });
107 }
108
109 Ok(config)
110}
111
112fn signing_policy(policy: RunPolicy) -> SigningPolicy {
113 match policy {
114 RunPolicy::Strict => SigningPolicy::Strict,
115 RunPolicy::DevOk => SigningPolicy::DevOk,
116 }
117}