use anyhow::Result;
use colored::Colorize;
use std::path::{Path, PathBuf};
use crate::charter::{self, CharterStatus};
use crate::config::StrayMarkConfig;
use crate::manifest::DistManifest;
use crate::utils::{self, pad_right_visual, visual_width};
const EXPECTED_DIRS: &[&str] = &[
"00-governance",
"01-requirements",
"02-design/decisions",
"03-implementation",
"04-testing",
"05-operations/incidents",
"05-operations/runbooks",
"06-evolution/technical-debt",
"07-ai-audit/agent-logs",
"07-ai-audit/decisions",
"07-ai-audit/ethical-reviews",
"08-security",
"09-ai-models",
"templates",
];
const EXPECTED_FILES: &[(&str, &str)] = &[
(".straymark/config.yml", "config.yml"),
(".straymark/dist-manifest.yml", "dist-manifest.yml"),
("STRAYMARK.md", "STRAYMARK.md"),
];
const DOC_TYPES: &[(&str, &str)] = &[
("ADR", "Architecture Decisions"),
("AIDEC", "AI Decisions"),
("AILOG", "AI Action Logs"),
("ETH", "Ethical Reviews"),
("INC", "Incident Post-mortems"),
("REQ", "Requirements"),
("TDE", "Technical Debt"),
("TES", "Test Plans"),
("SEC", "Security"),
("MCARD", "Model Cards"),
("SBOM", "Software Bill of Materials"),
("DPIA", "Data Protection Impact"),
];
pub fn run(path: &str) -> Result<()> {
let resolved = match utils::resolve_project_root(path) {
Some(r) => r,
None => {
let target = PathBuf::from(path)
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(path));
utils::info(&format!(
"StrayMark is not installed in {}",
target.display()
));
utils::info("Run 'straymark init' to initialize StrayMark in this directory.");
return Ok(());
}
};
if resolved.is_fallback {
utils::info(&format!(
"Using StrayMark installation at repo root: {}",
resolved.path.display()
));
}
let target = resolved.path;
let straymark_dir = target.join(".straymark");
let version = load_version(&target);
let language = load_language(&target);
let cli_version = env!("CARGO_PKG_VERSION");
println!();
println!(" {}", "StrayMark Status".bold().cyan());
println!();
println!(" {}", "Project".bold());
let project_rows: Vec<(&str, String)> = vec![
("Path", target.display().to_string()),
("Framework", format!("fw-{}", version)),
("CLI", format!("cli-{}", cli_version)),
("Language", language.clone()),
];
let label_w = project_rows
.iter()
.map(|(l, _)| visual_width(l))
.max()
.unwrap_or(5);
let value_w = project_rows
.iter()
.map(|(_, v)| visual_width(v))
.max()
.unwrap_or(10);
print_border(" ┌", label_w, "┬", value_w, "┐");
for (label, value) in &project_rows {
println!(
" │ {} │ {} │",
pad_right_visual(label, label_w).dimmed(),
pad_right_visual(value, value_w),
);
}
print_border(" └", label_w, "┴", value_w, "┘");
println!();
println!(" {}", "Structure".bold());
let mut struct_items: Vec<(String, bool)> = Vec::new();
for dir in EXPECTED_DIRS {
let dir_path = straymark_dir.join(dir);
struct_items.push((format!("{dir}/"), dir_path.exists()));
}
for &(rel_path, label) in EXPECTED_FILES {
let file_path = target.join(rel_path);
struct_items.push((label.to_string(), file_path.exists()));
}
let total_items = struct_items.len();
let total_ok = struct_items.iter().filter(|(_, ok)| *ok).count();
let total_missing = total_items - total_ok;
if total_missing == 0 {
println!(
" {} All {} items present",
"✓".green().bold(),
total_items
);
} else {
println!(
" {} {}/{} items present ({} missing)",
"!".yellow().bold(),
total_ok,
total_items,
total_missing
);
}
let name_w = struct_items
.iter()
.map(|(name, _)| visual_width(name))
.max()
.unwrap_or(10)
.max(visual_width("Directory / File"));
let status_w = 6;
println!();
println!(
" {} {} {}",
pad_right_visual("Directory / File", name_w).dimmed(),
"│".dimmed(),
pad_right_visual("Status", status_w).dimmed(),
);
println!(
" {}",
format!("{}─┼─{}", "─".repeat(name_w), "─".repeat(status_w)).dimmed()
);
for (name, exists) in &struct_items {
let status_text = if *exists { "✓ OK" } else { "✗ --" };
let name_cell = pad_right_visual(name, name_w);
let status_cell = pad_right_visual(status_text, status_w);
if *exists {
println!(" {} │ {}", name_cell, status_cell.green());
} else {
println!(" {} │ {}", name_cell.yellow(), status_cell.yellow());
}
}
let counts = count_documents(&straymark_dir);
let total: usize = counts.iter().map(|(_, _, c)| c).sum();
println!();
println!(" {}", "Documentation".bold());
let type_w = DOC_TYPES
.iter()
.map(|(p, l)| visual_width(&format!("{p:<6}{l}")))
.max()
.unwrap_or(20)
.max(visual_width("Type"));
let count_w = 5;
println!();
println!(
" {} {} {}",
pad_right_visual("Type", type_w).dimmed(),
"│".dimmed(),
pad_right_visual("Count", count_w).dimmed(),
);
println!(
" {}",
format!("{}─┼─{}", "─".repeat(type_w), "─".repeat(count_w)).dimmed()
);
for (prefix, label, count) in &counts {
let display = format!("{prefix:<6}{label}");
let count_str = format!("{count:>count_w$}");
let padded = pad_right_visual(&display, type_w);
if *count > 0 {
println!(" {} │ {}", padded, count_str.green().bold());
} else {
println!(" {} │ {}", padded.dimmed(), count_str.dimmed());
}
}
let total_str = format!("{total:>count_w$}");
println!(
" {} │ {}",
pad_right_visual("TOTAL", type_w).bold(),
total_str.cyan().bold(),
);
println!();
let charter_counts = count_charters(&target);
print_charters_block(&charter_counts);
if total_missing > 0 {
println!(
" {} Run {} to restore missing directories and files",
"→".blue().bold(),
"straymark repair".cyan().bold()
);
}
if total > 0 {
println!(
" {} Run {} to browse documentation interactively",
"→".blue().bold(),
"straymark explore".cyan().bold()
);
}
if total_missing > 0 || total > 0 {
println!();
}
Ok(())
}
fn print_border(prefix: &str, w1: usize, mid: &str, w2: usize, suffix: &str) {
println!(
"{}",
format!(
"{}{}{}{}{}",
prefix,
"─".repeat(w1 + 2),
mid,
"─".repeat(w2 + 2),
suffix
)
.dimmed()
);
}
fn load_version(project_root: &std::path::Path) -> String {
let manifest_path = project_root.join(".straymark/dist-manifest.yml");
match DistManifest::load(&manifest_path) {
Ok(m) => m.version,
Err(_) => {
utils::warn("Could not read dist-manifest.yml");
"unknown".to_string()
}
}
}
fn load_language(project_root: &std::path::Path) -> String {
StrayMarkConfig::resolve_language(project_root)
}
fn count_documents(straymark_dir: &std::path::Path) -> Vec<(&'static str, &'static str, usize)> {
let files = walk_files(straymark_dir);
DOC_TYPES
.iter()
.map(|&(doc_type, label)| {
let prefix = format!("{}-", doc_type);
let count = files
.iter()
.filter(|p| {
utils::is_user_document(p)
&& p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with(&prefix))
.unwrap_or(false)
})
.count();
(doc_type, label, count)
})
.collect()
}
fn walk_files(dir: &std::path::Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(walk_files(&path));
} else {
files.push(path);
}
}
}
files
}
struct CharterCounts {
declared: usize,
in_progress: usize,
closed: usize,
unparseable: usize,
total: usize,
}
fn count_charters(project_root: &Path) -> CharterCounts {
let (charters, errors) = charter::discover_and_parse(project_root);
let mut declared = 0;
let mut in_progress = 0;
let mut closed = 0;
for c in &charters {
match c.frontmatter.status {
CharterStatus::Declared => declared += 1,
CharterStatus::InProgress => in_progress += 1,
CharterStatus::Closed => closed += 1,
}
}
CharterCounts {
declared,
in_progress,
closed,
unparseable: errors.len(),
total: charters.len(),
}
}
fn print_charters_block(c: &CharterCounts) {
println!(" {}", "Charters".bold());
if c.total == 0 && c.unparseable == 0 {
println!(
" {} {}",
"·".dimmed(),
"No Charters yet — run `straymark charter new` to declare one (see STRAYMARK.md §15).".dimmed(),
);
println!();
return;
}
let rows: Vec<(&str, usize, colored::Color)> = vec![
("declared", c.declared, colored::Color::White),
("in-progress", c.in_progress, colored::Color::Yellow),
("closed", c.closed, colored::Color::Green),
];
let label_w = rows
.iter()
.map(|(l, _, _)| visual_width(l))
.max()
.unwrap_or(11);
let count_w = 5;
println!();
println!(
" {} {} {}",
pad_right_visual("Status", label_w).dimmed(),
"│".dimmed(),
pad_right_visual("Count", count_w).dimmed(),
);
println!(
" {}",
format!("{}─┼─{}", "─".repeat(label_w), "─".repeat(count_w)).dimmed()
);
for (label, count, color) in &rows {
let count_str = format!("{count:>count_w$}");
let padded = pad_right_visual(label, label_w);
if *count > 0 {
println!(" {} │ {}", padded, count_str.color(*color).bold());
} else {
println!(" {} │ {}", padded.dimmed(), count_str.dimmed());
}
}
let total_str = format!("{:>count_w$}", c.total);
println!(
" {} │ {}",
pad_right_visual("TOTAL", label_w).bold(),
total_str.cyan().bold(),
);
if c.unparseable > 0 {
println!(
" {} {} unparseable Charter file{} — run `straymark charter list` to see the warning detail.",
"!".yellow().bold(),
c.unparseable,
if c.unparseable == 1 { "" } else { "s" },
);
}
println!();
}