1use serde::Deserialize;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
5pub struct FileConfig {
6 pub format: Option<String>,
7 pub baseline: Option<PathBuf>,
8 pub md_out: Option<PathBuf>,
9 pub agent_pack: Option<PathBuf>,
10 pub agent_pack_format: Option<String>,
11 pub profile: Option<String>,
12 pub fail_on: Option<String>,
13 pub timings: Option<String>,
14 pub include: Option<Vec<String>>,
15 pub exclude: Option<Vec<String>>,
16 pub init: Option<InitDefaults>,
17 pub doctor: Option<DoctorDefaults>,
18 pub ci: Option<CiDefaults>,
19}
20
21#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
22pub struct InitDefaults {
23 pub output_dir: Option<PathBuf>,
24 pub path: Option<PathBuf>,
25 pub agent_pack_dir: Option<PathBuf>,
26 pub write_commands: Option<bool>,
27 pub shell_script: Option<bool>,
28 pub fix_prompt: Option<bool>,
29 pub profile: Option<String>,
30}
31
32#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
33pub struct DoctorDefaults {
34 pub output_dir: Option<PathBuf>,
35 pub agents: Option<PathBuf>,
36 pub format: Option<String>,
37 pub fix: Option<bool>,
38 pub repair: Option<Vec<String>>,
39 pub freshness_against: Option<PathBuf>,
40 pub plan_out: Option<PathBuf>,
41 pub profile: Option<String>,
42 pub open_pr_brief: Option<bool>,
43 pub open_pr_comment: Option<bool>,
44}
45
46#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
47pub struct CiDefaults {
48 pub doctor_repair: Option<Vec<String>>,
49 pub comment_mode: Option<String>,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct CliOverrides {
54 pub format: Option<String>,
55 pub baseline: Option<PathBuf>,
56 pub md_out: Option<PathBuf>,
57 pub agent_pack: Option<PathBuf>,
58 pub agent_pack_format: Option<String>,
59 pub profile: Option<String>,
60 pub fail_on: Option<String>,
61 pub timings: Option<String>,
62 pub include: Vec<String>,
63 pub exclude: Vec<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct RuntimeConfig {
68 pub format: String,
69 pub baseline: Option<PathBuf>,
70 pub md_out: Option<PathBuf>,
71 pub agent_pack: Option<PathBuf>,
72 pub agent_pack_format: String,
73 pub profile: String,
74 pub fail_on: String,
75 pub timings: String,
76 pub include: Vec<String>,
77 pub exclude: Vec<String>,
78}
79
80pub fn load_file_config(config_path: Option<&Path>) -> Result<FileConfig, miette::Report> {
81 let Some(path) = resolve_config_path(config_path) else {
82 return Ok(FileConfig::default());
83 };
84
85 let raw = std::fs::read_to_string(&path)
86 .map_err(|err| miette::miette!("Failed to read config file {}: {}", path.display(), err))?;
87 toml::from_str(&raw)
88 .map_err(|err| miette::miette!("Failed to parse config file {}: {}", path.display(), err))
89}
90
91pub fn resolve_runtime_config(file: FileConfig, cli: CliOverrides) -> RuntimeConfig {
92 RuntimeConfig {
93 format: cli
94 .format
95 .or(file.format)
96 .unwrap_or_else(|| "table".to_string()),
97 baseline: cli.baseline.or(file.baseline),
98 md_out: cli.md_out.or(file.md_out),
99 agent_pack: cli.agent_pack.or(file.agent_pack),
100 agent_pack_format: cli
101 .agent_pack_format
102 .or(file.agent_pack_format)
103 .unwrap_or_else(|| "json".to_string()),
104 profile: cli
105 .profile
106 .or(file.profile)
107 .unwrap_or_else(|| "full".to_string()),
108 fail_on: cli
109 .fail_on
110 .or(file.fail_on)
111 .unwrap_or_else(|| "error".to_string()),
112 timings: cli
113 .timings
114 .or(file.timings)
115 .unwrap_or_else(|| "off".to_string()),
116 include: if cli.include.is_empty() {
117 file.include.unwrap_or_default()
118 } else {
119 cli.include
120 },
121 exclude: if cli.exclude.is_empty() {
122 file.exclude.unwrap_or_default()
123 } else {
124 cli.exclude
125 },
126 }
127}
128
129fn resolve_config_path(config_path: Option<&Path>) -> Option<PathBuf> {
130 if let Some(path) = config_path {
131 return Some(path.to_path_buf());
132 }
133
134 let default = PathBuf::from("verifyos.toml");
135 if default.exists() {
136 Some(default)
137 } else {
138 None
139 }
140}