use clap::{Parser, Subcommand}; use anyhow::Result; use archdoc_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer}; use std::path::Path; /// CLI interface for ArchDoc #[derive(Parser)] #[command(name = "archdoc")] #[command(about = "Generate architecture documentation for Python projects")] #[command(version = "0.1.0")] pub struct Cli { #[command(subcommand)] command: Commands, /// Verbose output #[arg(short, long, global = true)] verbose: bool, } #[derive(Subcommand)] enum Commands { /// Initialize archdoc in the project Init { /// Project root directory #[arg(short, long, default_value = ".")] root: String, /// Output directory for documentation #[arg(short, long, default_value = "docs/architecture")] out: String, }, /// Generate or update documentation Generate { /// Project root directory #[arg(short, long, default_value = ".")] root: String, /// Output directory for documentation #[arg(short, long, default_value = "docs/architecture")] out: String, /// Configuration file path #[arg(short, long, default_value = "archdoc.toml")] config: String, }, /// Check if documentation is up to date Check { /// Project root directory #[arg(short, long, default_value = ".")] root: String, /// Configuration file path #[arg(short, long, default_value = "archdoc.toml")] config: String, }, } fn main() -> Result<()> { let cli = Cli::parse(); // Setup logging based on verbose flag setup_logging(cli.verbose)?; match &cli.command { Commands::Init { root, out } => { init_project(root, out)?; } Commands::Generate { root, out, config } => { let config = load_config(config)?; let model = analyze_project(root, &config)?; generate_docs(&model, out)?; } Commands::Check { root, config } => { let config = load_config(config)?; check_docs_consistency(root, &config)?; } } Ok(()) } fn setup_logging(verbose: bool) -> Result<()> { // TODO: Implement logging setup println!("Setting up logging with verbose={}", verbose); Ok(()) } fn init_project(root: &str, out: &str) -> Result<()> { // TODO: Implement project initialization println!("Initializing project at {} with output to {}", root, out); // Create output directory let out_path = std::path::Path::new(out); std::fs::create_dir_all(out_path) .map_err(|e| anyhow::anyhow!("Failed to create output directory: {}", e))?; // Create docs/architecture directory structure let docs_arch_path = out_path.join("docs").join("architecture"); std::fs::create_dir_all(&docs_arch_path) .map_err(|e| anyhow::anyhow!("Failed to create docs/architecture directory: {}", e))?; // Create modules and files directories std::fs::create_dir_all(docs_arch_path.join("modules")) .map_err(|e| anyhow::anyhow!("Failed to create modules directory: {}", e))?; std::fs::create_dir_all(docs_arch_path.join("files")) .map_err(|e| anyhow::anyhow!("Failed to create files directory: {}", e))?; // Create default ARCHITECTURE.md template let architecture_md_content = r#"# ARCHITECTURE — New Project ## Project summary **Name:** New Project **Description:** ## Key decisions (manual) - ## Non-goals (manual) - --- ## Document metadata - **Created:** 2026-01-25 - **Updated:** 2026-01-25 - **Generated by:** archdoc (cli) v0.1 --- ## Rails / Tooling > Generated. Do not edit inside this block. --- ## Repository layout (top-level) > Generated. Do not edit inside this block. --- ## Modules index > Generated. Do not edit inside this block. --- ## Critical dependency points > Generated. Do not edit inside this block. --- ## Change notes (manual) - "#; let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md"); std::fs::write(&architecture_md_path, architecture_md_content) .map_err(|e| anyhow::anyhow!("Failed to create ARCHITECTURE.md: {}", e))?; // Create default archdoc.toml config let config_toml_content = r#"[project] root = "." out_dir = "docs/architecture" entry_file = "ARCHITECTURE.md" language = "python" [scan] include = ["src", "app", "tests"] exclude = [ ".venv", "venv", "__pycache__", ".git", "dist", "build", ".mypy_cache", ".ruff_cache", ".pytest_cache", "*.egg-info" ] follow_symlinks = false max_file_size = "10MB" [python] src_roots = ["src", "."] include_tests = true parse_docstrings = true max_parse_errors = 10 [analysis] resolve_calls = true resolve_inheritance = false detect_integrations = true integration_patterns = [ { type = "http", patterns = ["requests", "httpx", "aiohttp"] }, { type = "db", patterns = ["sqlalchemy", "psycopg", "mysql", "sqlite3"] }, { type = "queue", patterns = ["celery", "kafka", "pika", "redis"] } ] [output] single_file = false per_file_docs = true create_directories = true overwrite_manual_sections = false [diff] update_timestamp_on_change_only = true hash_algorithm = "sha256" preserve_manual_content = true [thresholds] critical_fan_in = 20 critical_fan_out = 20 high_complexity = 50 [rendering] template_engine = "handlebars" max_table_rows = 100 truncate_long_descriptions = true description_max_length = 200 [logging] level = "info" file = "archdoc.log" format = "compact" [caching] enabled = true cache_dir = ".archdoc/cache" max_cache_age = "24h" "#; let config_toml_path = std::path::Path::new(root).join("archdoc.toml"); if !config_toml_path.exists() { std::fs::write(&config_toml_path, config_toml_content) .map_err(|e| anyhow::anyhow!("Failed to create archdoc.toml: {}", e))?; } println!("Project initialized successfully!"); println!("Created:"); println!(" - {}", architecture_md_path.display()); println!(" - {}", config_toml_path.display()); println!(" - {} (directory structure)", docs_arch_path.display()); Ok(()) } fn load_config(config_path: &str) -> Result { // TODO: Implement config loading println!("Loading config from {}", config_path); Config::load_from_file(Path::new(config_path)) .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e)) } fn analyze_project(root: &str, config: &Config) -> Result { // TODO: Implement project analysis println!("Analyzing project at {} with config", root); // Initialize scanner let scanner = FileScanner::new(config.clone()); // Scan for Python files let python_files = scanner.scan_python_files(std::path::Path::new(root))?; // Initialize Python analyzer let analyzer = PythonAnalyzer::new(config.clone()); // Parse each Python file let mut parsed_modules = Vec::new(); for file_path in python_files { match analyzer.parse_module(&file_path) { Ok(module) => parsed_modules.push(module), Err(e) => { eprintln!("Warning: Failed to parse {}: {}", file_path.display(), e); // Continue with other files } } } // Resolve symbols and build project model analyzer.resolve_symbols(&parsed_modules) .map_err(|e| anyhow::anyhow!("Failed to resolve symbols: {}", e)) } fn generate_docs(model: &ProjectModel, out: &str) -> Result<()> { // TODO: Implement documentation generation println!("Generating docs to {}", out); // Initialize renderer let renderer = archdoc_core::renderer::Renderer::new(); // Initialize writer let writer = archdoc_core::writer::DiffAwareWriter::new(); // Write to file - ARCHITECTURE.md should be in the project root, not output directory // The out parameter is for the docs/architecture directory structure let output_path = std::path::Path::new(".").join("ARCHITECTURE.md"); // Render and update each section individually // Update integrations section match renderer.render_integrations_section(model) { Ok(content) => { if let Err(e) = writer.update_file_with_markers(&output_path, &content, "integrations") { eprintln!("Warning: Failed to update integrations section: {}", e); } } Err(e) => { eprintln!("Warning: Failed to render integrations section: {}", e); } } // Update rails section match renderer.render_rails_section(model) { Ok(content) => { if let Err(e) = writer.update_file_with_markers(&output_path, &content, "rails") { eprintln!("Warning: Failed to update rails section: {}", e); } } Err(e) => { eprintln!("Warning: Failed to render rails section: {}", e); } } // Update layout section match renderer.render_layout_section(model) { Ok(content) => { if let Err(e) = writer.update_file_with_markers(&output_path, &content, "layout") { eprintln!("Warning: Failed to update layout section: {}", e); } } Err(e) => { eprintln!("Warning: Failed to render layout section: {}", e); } } // Update modules index section match renderer.render_modules_index_section(model) { Ok(content) => { if let Err(e) = writer.update_file_with_markers(&output_path, &content, "modules_index") { eprintln!("Warning: Failed to update modules_index section: {}", e); } } Err(e) => { eprintln!("Warning: Failed to render modules_index section: {}", e); } } // Update critical points section match renderer.render_critical_points_section(model) { Ok(content) => { if let Err(e) = writer.update_file_with_markers(&output_path, &content, "critical_points") { eprintln!("Warning: Failed to update critical_points section: {}", e); } } Err(e) => { eprintln!("Warning: Failed to render critical_points section: {}", e); } } Ok(()) } fn check_docs_consistency(root: &str, config: &Config) -> Result<()> { // TODO: Implement consistency checking println!("Checking docs consistency for project at {} with config", root); // Analyze project let model = analyze_project(root, config)?; // Generate documentation content - if this succeeds, the analysis is working let renderer = archdoc_core::renderer::Renderer::new(); let generated_architecture_md = renderer.render_architecture_md(&model)?; // Read existing documentation let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file); if !architecture_md_path.exists() { return Err(anyhow::anyhow!("Documentation file {} does not exist", architecture_md_path.display())); } let existing_architecture_md = std::fs::read_to_string(&architecture_md_path) .map_err(|e| anyhow::anyhow!("Failed to read {}: {}", architecture_md_path.display(), e))?; // For V1, we'll just check that we can generate content without errors // A full implementation would compare only the generated sections println!("Documentation analysis successful - project can be documented"); println!("Generated content length: {}", generated_architecture_md.len()); println!("Existing content length: {}", existing_architecture_md.len()); Ok(()) }