Skip to main content

spec_driven_docs/commands/
status.rs

1//! `status` subcommand: runtime-shape.
2//!
3//! Resolves the target, asks the status service, and renders text or JSON.
4//! Status is a report, not a gate: drift never turns into a failing exit.
5
6use 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
13/// Report an instance's state.
14///
15/// # Errors
16///
17/// [`AppError::ManifestInvalid`] when a manifest exists but cannot be
18/// trusted; manifest absence reports instead of failing.
19pub 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
58/// One entry line: what it is, where it resolved, and what decided it.
59fn 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
77/// Print every path the report carries, under one heading.
78///
79/// The text form is for a person reading a terminal. A skill reads
80/// `--json`, where every field is named and nothing is abbreviated.
81fn 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}