use clap::{Args, Subcommand};
use serde::Serialize;
use std::path::Path;
use crate::docs;
use homeboy::code_audit::docs_audit::AuditResult;
use homeboy::codebase_map;
use homeboy::component;
use super::CmdResult;
#[derive(Args)]
pub struct DocsArgs {
#[command(subcommand)]
pub command: Option<DocsCommand>,
#[arg(value_name = "TOPIC")]
pub topic: Option<String>,
}
#[derive(Subcommand)]
pub enum DocsCommand {
Map {
component_id: String,
#[arg(long, value_delimiter = ',')]
source_dirs: Option<Vec<String>>,
#[arg(long)]
include_private: bool,
#[arg(long)]
write: bool,
#[arg(long, default_value = "docs")]
output_dir: String,
},
Generate {
spec: Option<String>,
#[arg(long, value_name = "JSON")]
json: Option<String>,
#[arg(long, value_name = "AUDIT_JSON")]
from_audit: Option<String>,
#[arg(long)]
dry_run: bool,
},
}
#[derive(Serialize)]
#[serde(tag = "command")]
pub enum DocsOutput {
#[serde(rename = "docs.map")]
Map(codebase_map::CodebaseMap),
#[serde(rename = "docs.generate")]
Generate {
files_created: Vec<String>,
files_updated: Vec<String>,
hints: Vec<String>,
},
}
pub(crate) fn is_json_mode(args: &DocsArgs) -> bool {
matches!(
args.command,
Some(DocsCommand::Map { .. }) | Some(DocsCommand::Generate { .. })
)
}
pub fn run_markdown(args: DocsArgs) -> CmdResult<String> {
let topic = args.topic.as_deref().unwrap_or("index");
if topic == "list" {
let topics = docs::available_topics();
return Ok((topics.join("\n"), 0));
}
let topic_vec = vec![topic.to_string()];
let resolved = docs::resolve(&topic_vec)?;
Ok((resolved.content, 0))
}
pub fn run(args: DocsArgs, _global: &super::GlobalArgs) -> CmdResult<DocsOutput> {
match args.command {
Some(DocsCommand::Map {
component_id,
source_dirs,
include_private,
write,
output_dir,
}) => run_map(&component_id, source_dirs, include_private, write, &output_dir),
Some(DocsCommand::Generate {
spec,
json,
from_audit,
dry_run,
}) => {
if let Some(ref audit_source) = from_audit {
run_generate_from_audit(audit_source, dry_run)
} else {
let json_spec = json.as_deref().or(spec.as_deref());
run_generate(json_spec)
}
}
None => Err(homeboy::Error::validation_invalid_argument(
"command",
"JSON output requires map or generate subcommand. Use `homeboy docs <topic>` for topic display.",
None,
Some(vec![
"homeboy docs map <component-id>".to_string(),
"homeboy docs generate --json '<spec>'".to_string(),
"homeboy docs generate --from-audit @audit.json".to_string(),
"homeboy docs commands/deploy".to_string(),
]),
)),
}
}
fn run_map(
component_id: &str,
source_dirs: Option<Vec<String>>,
include_private: bool,
write: bool,
output_dir: &str,
) -> CmdResult<DocsOutput> {
let config = codebase_map::MapConfig {
component_id,
source_dirs,
include_private,
};
let map = codebase_map::build_map(&config)?;
if write {
let comp = component::load(component_id)?;
let base = Path::new(&comp.local_path).join(output_dir);
let files = codebase_map::render_map_to_markdown(&map, &base)?;
return Ok((
DocsOutput::Generate {
files_created: files,
files_updated: vec![],
hints: vec![format!(
"Generated docs from {} classes across {} modules",
map.total_classes,
map.modules.len()
)],
},
0,
));
}
Ok((DocsOutput::Map(map), 0))
}
fn run_generate(json_spec: Option<&str>) -> CmdResult<DocsOutput> {
let spec_str = json_spec.ok_or_else(|| {
homeboy::Error::validation_invalid_argument(
"json",
"Generate requires a JSON spec. Use --json or provide as positional argument.",
None,
Some(vec![
r#"homeboy docs generate --json '{"output_dir":"docs","files":[{"path":"test.md","title":"Test"}]}'"#.to_string(),
]),
)
})?;
let json_content = super::merge_json_sources(Some(spec_str), &[])?;
let spec: homeboy::docs::GenerateSpec = serde_json::from_value(json_content).map_err(|e| {
homeboy::Error::validation_invalid_json(e, Some("parse generate spec".to_string()), None)
})?;
let result = homeboy::docs::generate_from_spec(&spec)?;
Ok((
DocsOutput::Generate {
files_created: result.files_created,
files_updated: result.files_updated,
hints: result.hints,
},
0,
))
}
fn run_generate_from_audit(source: &str, dry_run: bool) -> CmdResult<DocsOutput> {
let effective_source = if !source.starts_with('{')
&& !source.starts_with('[')
&& source != "-"
&& !source.starts_with('@')
&& std::path::Path::new(source).exists()
{
format!("@{}", source)
} else {
source.to_string()
};
let json_content = super::merge_json_sources(Some(&effective_source), &[])?;
let audit: AuditResult = if let Some(data) = json_content.get("data") {
serde_json::from_value(data.clone())
} else {
serde_json::from_value(json_content)
}
.map_err(|e| {
homeboy::Error::validation_invalid_json(e, Some("parse audit result".to_string()), None)
})?;
let result = homeboy::docs::generate_from_audit(&audit, dry_run)?;
Ok((
DocsOutput::Generate {
files_created: result.files_created,
files_updated: result.files_updated,
hints: result.hints,
},
0,
))
}