1use std::path::PathBuf;
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::config::schema::{
12 AptMirrorDef, CheckSpec, ConfigDocument, EnvironmentMutation, InstallSpec, OriginMap,
13};
14use crate::util::valid_config_id;
15
16mod output;
17
18pub use crate::model::output::{
19 InstallOutcome, InstallPreview, InstallReport, SkillStatus, ToolInstallOutcome,
20 ToolInstallResult, ToolStatus,
21};
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub enum Profile {
26 Minimal,
28 #[default]
30 Standard,
31 Advanced,
33 Custom(String),
35}
36
37impl Profile {
38 pub fn parse(value: &str) -> Option<Self> {
40 match value {
41 "minimal" => Some(Self::Minimal),
42 "standard" => Some(Self::Standard),
43 "advanced" => Some(Self::Advanced),
44 value if valid_config_id(value) => Some(Self::Custom(value.to_string())),
45 _ => None,
46 }
47 }
48
49 pub fn as_str(&self) -> &str {
51 match self {
52 Self::Minimal => "minimal",
53 Self::Standard => "standard",
54 Self::Advanced => "advanced",
55 Self::Custom(value) => value,
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use crate::model::Profile;
63
64 #[test]
65 fn cli_profiles_use_the_config_id_contract() {
66 assert!(Profile::parse("ci-agent-2").is_some());
67 for invalid in ["CI", "ci_agent", "-ci", "ci-", "ci--agent"] {
68 assert!(Profile::parse(invalid).is_none(), "accepted {invalid}");
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74#[serde(rename_all = "lowercase")]
75pub enum Agent {
77 Claude,
79 OpenCode,
81}
82
83impl Agent {
84 pub fn as_str(self) -> &'static str {
86 match self {
87 Self::Claude => "claude",
88 Self::OpenCode => "opencode",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
94#[serde(rename_all = "snake_case")]
95pub enum InstallKind {
97 Tool,
99 Skill,
101}
102
103impl InstallKind {
104 pub fn as_str(self) -> &'static str {
106 match self {
107 Self::Tool => "tool",
108 Self::Skill => "skill",
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114#[serde(rename_all = "lowercase")]
115pub enum BackendKind {
117 Apt,
119 Brew,
121 Cargo,
123 Rustup,
125 Npm,
127 Pip,
129 #[serde(rename = "uv-tool")]
131 UvTool,
132 Winget,
134 Archive,
136 Git,
138 Shell,
140}
141
142impl BackendKind {
143 pub fn as_str(self) -> &'static str {
145 match self {
146 Self::Apt => "apt",
147 Self::Brew => "brew",
148 Self::Cargo => "cargo",
149 Self::Rustup => "rustup",
150 Self::Npm => "npm",
151 Self::Pip => "pip",
152 Self::UvTool => "uv-tool",
153 Self::Winget => "winget",
154 Self::Archive => "archive",
155 Self::Git => "git",
156 Self::Shell => "shell",
157 }
158 }
159}
160
161#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(default, deny_unknown_fields)]
163pub(crate) struct ToolDef {
165 pub name: String,
166 pub display_name: Option<String>,
167 pub version: Option<String>,
168 pub optional: bool,
169 pub allow_insecure_hosts: Vec<String>,
170 pub detect: Option<CheckSpec>,
171 pub install: Option<InstallSpec>,
172 pub verify: Option<CheckSpec>,
173}
174
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
176#[serde(rename_all = "snake_case")]
177pub enum ArchiveFormat {
179 #[default]
181 File,
182 TarGz,
184 TarXz,
186 Zip,
188}
189
190impl ToolDef {
191 pub(crate) fn backend(&self) -> Option<BackendKind> {
192 match self.install.as_ref()? {
193 InstallSpec::Apt(_) => Some(BackendKind::Apt),
194 InstallSpec::Brew(_) => Some(BackendKind::Brew),
195 InstallSpec::Cargo(_) => Some(BackendKind::Cargo),
196 InstallSpec::Rustup(_) => Some(BackendKind::Rustup),
197 InstallSpec::Npm(_) => Some(BackendKind::Npm),
198 InstallSpec::Pip(_) => Some(BackendKind::Pip),
199 InstallSpec::UvTool(_) => Some(BackendKind::UvTool),
200 InstallSpec::Winget(_) => Some(BackendKind::Winget),
201 InstallSpec::Archive(_) => Some(BackendKind::Archive),
202 InstallSpec::Git(_) => Some(BackendKind::Git),
203 InstallSpec::Shell(_) => Some(BackendKind::Shell),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(default, deny_unknown_fields)]
210pub(crate) struct SkillDef {
212 pub name: String,
213 pub display_name: Option<String>,
214 pub optional: bool,
215 pub source: String,
216 pub agents: Vec<Agent>,
217 pub revision: Option<String>,
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(default, deny_unknown_fields)]
222pub(crate) struct InstallConfig {
224 pub environment: EnvironmentDef,
225 pub apt_mirror: Option<AptMirrorDef>,
226 pub tools: Vec<ToolDef>,
227 pub skills: Vec<SkillDef>,
228}
229
230#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(default, deny_unknown_fields)]
232pub(crate) struct EnvironmentDef {
234 pub mutations: Vec<EnvironmentMutation>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct LoadedConfig {
240 pub document: ConfigDocument,
242 pub origins: OriginMap,
244 pub path: Option<PathBuf>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
249#[serde(deny_unknown_fields)]
250pub struct RegistryEntry {
252 pub name: String,
254 pub kind: InstallKind,
256 pub source: String,
258 pub profile: String,
260 pub targets: Vec<RegistryTarget>,
262 pub installed_at: u64,
264 #[serde(deserialize_with = "deserialize_explicit_option")]
265 #[schemars(schema_with = "nullable_string_schema", required)]
266 pub artifact_id: Option<String>,
268 #[serde(deserialize_with = "deserialize_explicit_option")]
269 #[schemars(schema_with = "nullable_string_schema", required)]
270 pub previous_artifact_id: Option<String>,
272 #[serde(deserialize_with = "deserialize_explicit_option")]
273 #[schemars(schema_with = "nullable_string_schema", required)]
274 pub config_hash: Option<String>,
276 #[serde(deserialize_with = "deserialize_explicit_option")]
277 #[schemars(schema_with = "nullable_string_schema", required)]
278 pub plan_hash: Option<String>,
280 #[serde(deserialize_with = "deserialize_explicit_option")]
281 #[schemars(schema_with = "nullable_string_schema", required)]
282 pub source_revision: Option<String>,
284 #[serde(deserialize_with = "deserialize_explicit_option")]
285 #[schemars(schema_with = "nullable_backend_schema", required)]
286 pub backend: Option<BackendKind>,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
291#[serde(deny_unknown_fields)]
292pub struct RegistryTarget {
294 pub path: PathBuf,
296 #[serde(deserialize_with = "deserialize_explicit_option")]
297 #[schemars(schema_with = "nullable_string_schema", required)]
298 pub binary: Option<String>,
300}
301
302fn nullable_string_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
303 generator.subschema_for::<Option<String>>()
304}
305
306fn nullable_backend_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
307 generator.subschema_for::<Option<BackendKind>>()
308}
309
310fn deserialize_explicit_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
311where
312 D: serde::Deserializer<'de>,
313 T: Deserialize<'de>,
314{
315 Option::<T>::deserialize(deserializer)
316}
317
318impl RegistryEntry {
319 pub fn stable_id(&self) -> String {
321 format!("{}:{}", self.kind.as_str(), self.name)
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct InstallOptions {
328 pub profile: Profile,
330 pub config_path: Option<PathBuf>,
332 pub overlay_paths: Vec<PathBuf>,
334 pub status_bar: bool,
336 pub yes: bool,
338 pub only: Vec<String>,
340 pub exclude: Vec<String>,
342}
343
344impl Default for InstallOptions {
345 fn default() -> Self {
346 Self {
347 profile: Profile::Standard,
348 config_path: None,
349 overlay_paths: Vec::new(),
350 status_bar: true,
351 yes: false,
352 only: Vec::new(),
353 exclude: Vec::new(),
354 }
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
359pub(crate) struct DiagnosticCheck {
361 pub(crate) id: String,
363 pub(crate) severity: String,
365 pub(crate) summary: String,
367 pub(crate) suggestion: Option<String>,
369 pub(crate) duration_ms: u128,
371}