spec_driven_docs/commands/
status.rs1use crate::cli::status::StatusArgs;
7use crate::context::AppContext;
8use crate::domain::paths::{PathEntry, PathSource, Paths};
9use crate::error::AppError;
10use crate::output;
11use crate::services::status::status;
12
13pub fn run(ctx: &AppContext, args: StatusArgs) -> Result<(), AppError> {
20 let target = if args.target.is_absolute() {
21 args.target
22 } else if args.target == "." {
23 ctx.cwd.clone()
24 } else {
25 return Err(AppError::Usage("target must be absolute or .".to_string()));
26 };
27 let report = status(&target)?;
28 if args.json {
29 return output::json(&report);
30 }
31 if !report.instance {
32 output::line(format!("no instance at {target}"));
33 render_paths(&report.paths);
34 return Ok(());
35 }
36 let profile = report.profile.map_or("unknown", |profile| profile.as_str());
37 if let Some(version) = report.canon_version {
38 output::line(format!(
39 "spec-driven-docs {version} ({profile}) at {target}"
40 ));
41 }
42 if let Some(alignment) = report.alignment {
43 let word = match alignment {
44 crate::services::status::Alignment::Aligned => "aligned",
45 crate::services::status::Alignment::BinaryNewer => "binary newer; run 'sdd upgrade'",
46 crate::services::status::Alignment::InstanceNewer => "instance newer; upgrade sdd",
47 };
48 output::line(format!("alignment: {word}"));
49 }
50 output::line(format!(
51 "managed drift: {}; adopted drift: {}; failures: {}",
52 report.managed_drift, report.adopted_drift, report.failures
53 ));
54 render_paths(&report.paths);
55 Ok(())
56}
57
58fn entry(label: &str, path: &PathEntry) {
60 output::line(format!(
61 " {label}: {} ({})",
62 path.path,
63 source_word(path.source)
64 ));
65}
66
67const fn source_word(source: PathSource) -> &'static str {
68 match source {
69 PathSource::Recorded => "recorded",
70 PathSource::Default => "default",
71 PathSource::Env => "env",
72 PathSource::Profile => "profile",
73 PathSource::Proposal => "proposal",
74 }
75}
76
77fn render_paths(paths: &Paths) {
82 output::line("paths:");
83 if let Some(user) = paths.user.as_ref() {
84 entry("state root", &user.state_root);
85 entry("cache root", &user.cache_root);
86 entry("skill receipt", &user.skill_receipt);
87 for root in &user.agent_roots {
88 let moved = root
89 .variable
90 .map_or_else(|| source_word(root.source).to_string(), str::to_string);
91 output::line(format!(" agent root {}: {} ({moved})", root.id, root.path));
92 }
93 }
94 let Some(active) = paths.active.as_ref() else {
95 for (name, candidate) in &paths.candidates {
96 output::line(format!(
97 " candidate {name}: {}",
98 candidate.destinations.docs_root.path
99 ));
100 }
101 return;
102 };
103 let held = &active.destinations;
104 entry("instance directory", &held.instance_dir);
105 entry("declaration", &held.declaration);
106 entry("debt", &held.debt);
107 entry("hook configuration", &held.hooks_config);
108 entry("agent digest", &held.agents_digest);
109 entry("documentation root", &held.docs_root);
110}