1use std::fmt;
2use std::path::PathBuf;
3
4use clap::ValueEnum;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
8#[serde(rename_all = "kebab-case")]
9pub enum Context {
10 Startup,
11 SkillDev,
12 TaskTools,
13 ProjectDev,
14}
15
16impl Context {
17 pub const fn as_str(self) -> &'static str {
18 match self {
19 Self::Startup => "startup",
20 Self::SkillDev => "skill-dev",
21 Self::TaskTools => "task-tools",
22 Self::ProjectDev => "project-dev",
23 }
24 }
25
26 pub const fn supported_values() -> &'static [&'static str] {
27 &["startup", "skill-dev", "task-tools", "project-dev"]
28 }
29
30 pub fn from_config_value(value: &str) -> Option<Self> {
31 match value {
32 "startup" => Some(Self::Startup),
33 "skill-dev" => Some(Self::SkillDev),
34 "task-tools" => Some(Self::TaskTools),
35 "project-dev" => Some(Self::ProjectDev),
36 _ => None,
37 }
38 }
39}
40
41impl fmt::Display for Context {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.write_str(self.as_str())
44 }
45}
46
47pub const SUPPORTED_CONTEXTS: [Context; 4] = [
48 Context::Startup,
49 Context::SkillDev,
50 Context::TaskTools,
51 Context::ProjectDev,
52];
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
55#[serde(rename_all = "kebab-case")]
56pub enum Scope {
57 Home,
58 Project,
59}
60
61impl Scope {
62 pub const fn as_str(self) -> &'static str {
63 match self {
64 Self::Home => "home",
65 Self::Project => "project",
66 }
67 }
68
69 pub const fn supported_values() -> &'static [&'static str] {
70 &["home", "project"]
71 }
72
73 pub fn from_config_value(value: &str) -> Option<Self> {
74 match value {
75 "home" => Some(Self::Home),
76 "project" => Some(Self::Project),
77 _ => None,
78 }
79 }
80}
81
82impl fmt::Display for Scope {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.write_str(self.as_str())
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
89#[serde(rename_all = "kebab-case")]
90pub enum OutputFormat {
91 #[default]
92 Text,
93 Json,
94}
95
96impl OutputFormat {
97 pub const fn as_str(self) -> &'static str {
98 match self {
99 Self::Text => "text",
100 Self::Json => "json",
101 }
102 }
103}
104
105impl fmt::Display for OutputFormat {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.write_str(self.as_str())
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
112#[serde(rename_all = "kebab-case")]
113pub enum FallbackMode {
114 #[default]
115 Auto,
116 LocalOnly,
117}
118
119impl FallbackMode {
120 pub const fn as_str(self) -> &'static str {
121 match self {
122 Self::Auto => "auto",
123 Self::LocalOnly => "local-only",
124 }
125 }
126}
127
128impl fmt::Display for FallbackMode {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.write_str(self.as_str())
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
135#[serde(rename_all = "kebab-case")]
136pub enum ResolveFormat {
137 #[default]
138 Text,
139 Json,
140 Checklist,
141}
142
143impl ResolveFormat {
144 pub const fn as_str(self) -> &'static str {
145 match self {
146 Self::Text => "text",
147 Self::Json => "json",
148 Self::Checklist => "checklist",
149 }
150 }
151}
152
153impl fmt::Display for ResolveFormat {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str(self.as_str())
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum, Default)]
160#[serde(rename_all = "kebab-case")]
161pub enum BaselineTarget {
162 Home,
163 Project,
164 #[default]
165 All,
166}
167
168impl BaselineTarget {
169 pub const fn as_str(self) -> &'static str {
170 match self {
171 Self::Home => "home",
172 Self::Project => "project",
173 Self::All => "all",
174 }
175 }
176}
177
178impl fmt::Display for BaselineTarget {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str(self.as_str())
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum DocumentStatus {
187 Present,
188 Missing,
189}
190
191impl DocumentStatus {
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Self::Present => "present",
195 Self::Missing => "missing",
196 }
197 }
198}
199
200impl fmt::Display for DocumentStatus {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.write_str(self.as_str())
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
207#[serde(rename_all = "kebab-case")]
208pub enum DocumentSource {
209 Builtin,
210 BuiltinFallback,
211 ExtensionHome,
212 ExtensionProject,
213}
214
215impl DocumentSource {
216 pub const fn as_str(self) -> &'static str {
217 match self {
218 Self::Builtin => "builtin",
219 Self::BuiltinFallback => "builtin-fallback",
220 Self::ExtensionHome => "extension-home",
221 Self::ExtensionProject => "extension-project",
222 }
223 }
224}
225
226impl fmt::Display for DocumentSource {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 f.write_str(self.as_str())
229 }
230}
231
232#[derive(Debug, Clone, Serialize)]
233pub struct ResolvedDocument {
234 pub context: Context,
235 pub scope: Scope,
236 pub path: PathBuf,
237 pub required: bool,
238 pub status: DocumentStatus,
239 pub source: DocumentSource,
240 pub why: String,
241}
242
243#[derive(Debug, Clone, Serialize)]
244pub struct ResolveSummary {
245 pub required_total: usize,
246 pub present_required: usize,
247 pub missing_required: usize,
248}
249
250impl ResolveSummary {
251 pub fn from_documents(documents: &[ResolvedDocument]) -> Self {
252 let required_total = documents.iter().filter(|doc| doc.required).count();
253 let present_required = documents
254 .iter()
255 .filter(|doc| doc.required && doc.status == DocumentStatus::Present)
256 .count();
257 let missing_required = required_total.saturating_sub(present_required);
258
259 Self {
260 required_total,
261 present_required,
262 missing_required,
263 }
264 }
265}
266
267#[derive(Debug, Clone, Serialize)]
268pub struct ResolveReport {
269 pub context: Context,
270 pub strict: bool,
271 pub agent_home: PathBuf,
272 pub project_path: PathBuf,
273 pub is_linked_worktree: bool,
274 pub git_common_dir: Option<PathBuf>,
275 pub primary_worktree_path: Option<PathBuf>,
276 pub documents: Vec<ResolvedDocument>,
277 pub summary: ResolveSummary,
278}
279
280impl ResolveReport {
281 pub fn has_missing_required(&self) -> bool {
282 self.summary.missing_required > 0
283 }
284}
285
286#[derive(Debug, Clone, Serialize)]
287pub struct BaselineCheckItem {
288 pub scope: Scope,
289 pub context: Context,
290 pub label: String,
291 pub path: PathBuf,
292 pub required: bool,
293 pub status: DocumentStatus,
294 pub source: DocumentSource,
295 pub why: String,
296}
297
298#[derive(Debug, Clone, Serialize)]
299pub struct BaselineCheckReport {
300 pub target: BaselineTarget,
301 pub strict: bool,
302 pub agent_home: PathBuf,
303 pub project_path: PathBuf,
304 pub items: Vec<BaselineCheckItem>,
305 pub missing_required: usize,
306 pub missing_optional: usize,
307 pub suggested_actions: Vec<String>,
308}
309
310impl BaselineCheckReport {
311 pub fn from_items(
312 target: BaselineTarget,
313 strict: bool,
314 agent_home: PathBuf,
315 project_path: PathBuf,
316 items: Vec<BaselineCheckItem>,
317 suggested_actions: Vec<String>,
318 ) -> Self {
319 let missing_required = items
320 .iter()
321 .filter(|item| item.required && item.status == DocumentStatus::Missing)
322 .count();
323 let missing_optional = items
324 .iter()
325 .filter(|item| !item.required && item.status == DocumentStatus::Missing)
326 .count();
327
328 Self {
329 target,
330 strict,
331 agent_home,
332 project_path,
333 items,
334 missing_required,
335 missing_optional,
336 suggested_actions,
337 }
338 }
339
340 pub fn has_missing_required(&self) -> bool {
341 self.missing_required > 0
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
346#[serde(rename_all = "kebab-case")]
347pub enum DocumentWhen {
348 Always,
349}
350
351impl DocumentWhen {
352 pub const fn as_str(self) -> &'static str {
353 match self {
354 Self::Always => "always",
355 }
356 }
357
358 pub const fn supported_values() -> &'static [&'static str] {
359 &["always"]
360 }
361
362 pub fn from_config_value(value: &str) -> Option<Self> {
363 match value {
364 "always" => Some(Self::Always),
365 _ => None,
366 }
367 }
368}
369
370impl fmt::Display for DocumentWhen {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 f.write_str(self.as_str())
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
377pub struct ConfigDocumentEntry {
378 pub context: Context,
379 pub scope: Scope,
380 pub path: PathBuf,
381 pub required: bool,
382 pub when: DocumentWhen,
383 pub notes: Option<String>,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
387pub struct ConfigScopeFile {
388 pub source_scope: Scope,
389 pub root: PathBuf,
390 pub file_path: PathBuf,
391 pub documents: Vec<ConfigDocumentEntry>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
395pub struct LoadedConfigs {
396 pub home: Option<ConfigScopeFile>,
397 pub project: Option<ConfigScopeFile>,
398}
399
400impl LoadedConfigs {
401 pub fn in_load_order(&self) -> Vec<&ConfigScopeFile> {
402 let mut ordered = Vec::new();
403 if let Some(home) = self.home.as_ref() {
404 ordered.push(home);
405 }
406 if let Some(project) = self.project.as_ref() {
407 ordered.push(project);
408 }
409 ordered
410 }
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
414#[serde(rename_all = "kebab-case")]
415pub enum ConfigErrorKind {
416 Io,
417 Parse,
418 Validation,
419}
420
421impl ConfigErrorKind {
422 pub const fn as_str(self) -> &'static str {
423 match self {
424 Self::Io => "io",
425 Self::Parse => "parse",
426 Self::Validation => "validation",
427 }
428 }
429}
430
431impl fmt::Display for ConfigErrorKind {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 f.write_str(self.as_str())
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
438pub struct ConfigErrorLocation {
439 pub line: usize,
440 pub column: usize,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
444pub struct ConfigLoadError {
445 pub kind: ConfigErrorKind,
446 pub file_path: PathBuf,
447 pub document_index: Option<usize>,
448 pub field: Option<String>,
449 pub location: Option<ConfigErrorLocation>,
450 pub message: String,
451}
452
453impl ConfigLoadError {
454 pub fn io(file_path: PathBuf, message: impl Into<String>) -> Self {
455 Self {
456 kind: ConfigErrorKind::Io,
457 file_path,
458 document_index: None,
459 field: None,
460 location: None,
461 message: message.into(),
462 }
463 }
464
465 pub fn parse(
466 file_path: PathBuf,
467 message: impl Into<String>,
468 location: Option<ConfigErrorLocation>,
469 ) -> Self {
470 Self {
471 kind: ConfigErrorKind::Parse,
472 file_path,
473 document_index: None,
474 field: None,
475 location,
476 message: message.into(),
477 }
478 }
479
480 pub fn validation(
481 file_path: PathBuf,
482 document_index: usize,
483 field: impl Into<String>,
484 message: impl Into<String>,
485 ) -> Self {
486 Self {
487 kind: ConfigErrorKind::Validation,
488 file_path,
489 document_index: Some(document_index),
490 field: Some(field.into()),
491 location: None,
492 message: message.into(),
493 }
494 }
495
496 pub fn validation_root(
497 file_path: PathBuf,
498 field: impl Into<String>,
499 message: impl Into<String>,
500 ) -> Self {
501 Self {
502 kind: ConfigErrorKind::Validation,
503 file_path,
504 document_index: None,
505 field: Some(field.into()),
506 location: None,
507 message: message.into(),
508 }
509 }
510}
511
512impl fmt::Display for ConfigLoadError {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 match (self.document_index, self.field.as_deref(), self.location) {
515 (Some(index), Some(field), Some(location)) => write!(
516 f,
517 "{}:{}:{} [{}] document[{index}].{field}: {}",
518 self.file_path.display(),
519 location.line,
520 location.column,
521 self.kind,
522 self.message
523 ),
524 (Some(index), Some(field), None) => write!(
525 f,
526 "{} [{}] document[{index}].{field}: {}",
527 self.file_path.display(),
528 self.kind,
529 self.message
530 ),
531 (None, None, Some(location)) => write!(
532 f,
533 "{}:{}:{} [{}]: {}",
534 self.file_path.display(),
535 location.line,
536 location.column,
537 self.kind,
538 self.message
539 ),
540 (None, Some(field), Some(location)) => write!(
541 f,
542 "{}:{}:{} [{}] {field}: {}",
543 self.file_path.display(),
544 location.line,
545 location.column,
546 self.kind,
547 self.message
548 ),
549 (None, Some(field), None) => write!(
550 f,
551 "{} [{}] {field}: {}",
552 self.file_path.display(),
553 self.kind,
554 self.message
555 ),
556 _ => write!(
557 f,
558 "{} [{}]: {}",
559 self.file_path.display(),
560 self.kind,
561 self.message
562 ),
563 }
564 }
565}
566
567impl std::error::Error for ConfigLoadError {}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
570#[serde(rename_all = "kebab-case")]
571pub enum AddDocumentAction {
572 Inserted,
573 Updated,
574}
575
576impl AddDocumentAction {
577 pub const fn as_str(self) -> &'static str {
578 match self {
579 Self::Inserted => "inserted",
580 Self::Updated => "updated",
581 }
582 }
583}
584
585impl fmt::Display for AddDocumentAction {
586 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587 f.write_str(self.as_str())
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
592pub struct AddDocumentReport {
593 pub target: Scope,
594 pub target_root: PathBuf,
595 pub config_path: PathBuf,
596 pub created_config: bool,
597 pub action: AddDocumentAction,
598 pub entry: ConfigDocumentEntry,
599 pub document_count: usize,
600}
601
602#[derive(Debug, Clone, Serialize)]
603pub struct StubReport {
604 pub command: String,
605 pub implemented: bool,
606 pub message: String,
607}