From 9d43e6aef9617bad58e0c77e9986ebef5cecb265 Mon Sep 17 00:00:00 2001 From: Alex Holmberg <113964069+Alex793x@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:19:19 +0200 Subject: [PATCH 1/2] Feature/condense overview with new representation (#29) * chore: release v0.3.0 * feat: Optimized Analysis Dashboard Overview with new default matrix option --- CHANGELOG.md | 12 + Cargo.lock | 106 ++- Cargo.toml | 4 +- README.md | 17 + docs/cli-display-modes.md | 136 ++++ docs/command-overview.md | 251 ++++++ src/analyzer/display.rs | 1322 +++++++++++++++++++++++++++++++ src/analyzer/docker_analyzer.rs | 2 +- src/analyzer/mod.rs | 5 +- src/cli.rs | 18 +- src/main.rs | 531 +------------ 11 files changed, 1900 insertions(+), 504 deletions(-) create mode 100644 docs/cli-display-modes.md create mode 100644 docs/command-overview.md create mode 100644 src/analyzer/display.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd892d3..66457d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [0.3.0](https://github.com/syncable-dev/syncable-cli/compare/v0.2.1...v0.3.0) - 2025-06-06 + +### Added + +- Added tool install verifier with cli calls ([#14](https://github.com/syncable-dev/syncable-cli/pull/14)) + +### Other + +- Feature/extendsive docker compose and docker scan ([#25](https://github.com/syncable-dev/syncable-cli/pull/25)) +- Feature/add automatic cli update ([#22](https://github.com/syncable-dev/syncable-cli/pull/22)) +- Feature/update dependabot ([#11](https://github.com/syncable-dev/syncable-cli/pull/11)) + ## [0.2.1](https://github.com/syncable-dev/syncable-cli/compare/v0.2.0...v0.2.1) - 2025-06-06 ### Other diff --git a/Cargo.lock b/Cargo.lock index 8792ab13..a612cfe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.15.11" @@ -373,7 +383,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -487,6 +497,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "cvss" version = "2.0.0" @@ -537,6 +568,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -545,10 +586,21 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.0", "windows-sys 0.59.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1969,7 +2021,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "web-time", ] @@ -2554,6 +2606,20 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettytable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width 0.1.14", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2762,6 +2828,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -3262,11 +3339,12 @@ dependencies = [ [[package]] name = "syncable-cli" -version = "0.2.1" +version = "0.3.0" dependencies = [ "assert_cmd", "chrono", "clap", + "colored", "dirs", "env_logger", "glob", @@ -3274,6 +3352,7 @@ dependencies = [ "log", "once_cell", "predicates", + "prettytable", "proptest", "rayon", "regex", @@ -3384,6 +3463,17 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3407,7 +3497,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -3827,6 +3917,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 31fa8951..d7eb9f8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syncable-cli" -version = "0.2.1" +version = "0.3.0" edition = "2024" authors = ["Syncable Team"] description = "A Rust-based CLI that analyzes code repositories and generates Infrastructure as Code configurations" @@ -31,6 +31,8 @@ once_cell = "1" rayon = "1.7" termcolor = "1" chrono = { version = "0.4", features = ["serde"] } +colored = "2" +prettytable = "0.10" # Vulnerability checking dependencies rustsec = "0.29" diff --git a/README.md b/README.md index d4014f54..8f5b81bb 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,25 @@ sync-ctl analyze /path/to/your/project # Get JSON output sync-ctl analyze --json > analysis.json + +# Use different display modes (NEW!) +sync-ctl analyze --display matrix # Modern dashboard view (default) +sync-ctl analyze --display summary # Brief summary only +sync-ctl analyze --display detailed # Legacy verbose output +sync-ctl analyze -d # Shorthand for detailed ``` +### πŸ“Š Display Modes (NEW!) + +The analyze command now offers multiple display formats: + +- **Matrix View** (default): A modern, compact dashboard with side-by-side project comparison +- **Summary View**: Brief overview perfect for CI/CD pipelines +- **Detailed View**: Traditional verbose output with all project details +- **JSON**: Machine-readable format for integration with other tools + +See the [Display Modes Documentation](docs/cli-display-modes.md) for visual examples and more details. + ### Check for Vulnerabilities ```bash diff --git a/docs/cli-display-modes.md b/docs/cli-display-modes.md new file mode 100644 index 00000000..96a6f01a --- /dev/null +++ b/docs/cli-display-modes.md @@ -0,0 +1,136 @@ +# CLI Display Modes + +The `sync-ctl analyze` command now offers multiple display modes to present analysis results in different formats optimized for various use cases. + +## Display Options + +### 1. Matrix View (Default) - `--display matrix` + +The matrix view provides a modern, compact dashboard that's easy to scan and compare projects side-by-side. This is the new default display mode. + +```bash +sync-ctl analyze . --display matrix +# or simply +sync-ctl analyze . +``` + +**Example Output:** +``` +═══════════════════════════════════════════════════════════════════════════════════════════════════ +πŸ“Š PROJECT ANALYSIS DASHBOARD +═══════════════════════════════════════════════════════════════════════════════════════════════════ + +β”Œβ”€ Architecture Overview ────────────────────────────────────────────────────────────────────────┐ +β”‚ Type: Monorepo (3 projects) β”‚ +β”‚ Pattern: Fullstack β”‚ +β”‚ Full-stack app with frontend/backend separation β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€ Technology Stack ─────────────────────────────────────────────────────────────────────────────┐ +β”‚ Languages: TypeScript β”‚ +β”‚ Frameworks: Encore, Tanstack Start β”‚ +β”‚ Databases: Drizzle ORM β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€ Projects Matrix ──────────────────────────────────────────────────────────────────────────────┐ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Project β”‚ Type β”‚ Languages β”‚ Main Tech β”‚ Ports β”‚ Docker β”‚ Deps β”‚ β”‚ +β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€ β”‚ +β”‚ β”‚ βš™οΈ backend β”‚ Backend β”‚ TypeScriptβ”‚ Encore β”‚ 4000 β”‚ βœ“ β”‚ 32 β”‚ β”‚ +β”‚ β”‚ πŸ—οΈ devops-agent β”‚ Infrastructureβ”‚ TypeScriptβ”‚ - β”‚ - β”‚ βœ— β”‚ 5 β”‚ β”‚ +β”‚ β”‚ 🌐 frontend β”‚ Frontend β”‚ TypeScriptβ”‚ Tanstack Start β”‚ 3000 β”‚ βœ“ β”‚ 123 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€ Docker Infrastructure ────────────────────────────────────────────────────────────────────────┐ +β”‚ Dockerfiles: 2 β”‚ +β”‚ Compose Files: 2 β”‚ +β”‚ Total Services: 5 β”‚ +β”‚ Orchestration Patterns: Microservices β”‚ +β”‚ ───────────────────────────────────────────────────────────────────────────────────────────── β”‚ +β”‚ Service Connectivity: β”‚ +β”‚ encore-postgres: 5431:5432 β”‚ +β”‚ encore: 4000:8080 β†’ encore-postgres β”‚ +β”‚ intellitask-app: 3000:3000 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€ Analysis Metrics ─────────────────────────────────────────────────────────────────────────────┐ +β”‚ ⏱️ Duration: 57ms πŸ“ Files: 294 🎯 Score: 87% πŸ”– Version: 0.3.0 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +═══════════════════════════════════════════════════════════════════════════════════════════════════ +``` + +### 2. Summary View - `--display summary` + +A brief overview of the analysis results, perfect for quick checks or CI/CD pipelines. + +```bash +sync-ctl analyze . --display summary +``` + +**Example Output:** +``` +β–Ά PROJECT ANALYSIS SUMMARY +────────────────────────────────────────────────── +β”‚ Architecture: Monorepo (3 projects) +β”‚ Pattern: Fullstack +β”‚ Stack: TypeScript +β”‚ Frameworks: Encore, Tanstack Start +β”‚ Analysis Time: 57ms +β”‚ Confidence: 87% +────────────────────────────────────────────────── +``` + +### 3. Detailed View (Legacy) - `--display detailed` or `-d` + +The traditional verbose output with all details in a vertical layout. Useful when you need to see everything about each project. + +```bash +sync-ctl analyze . --display detailed +# or for backward compatibility +sync-ctl analyze . -d +``` + +This produces the traditional long-form output with all details about each project. + +### 4. JSON Output - `--json` + +Machine-readable JSON output for integration with other tools or programmatic processing. + +```bash +sync-ctl analyze . --json +``` + +## Choosing the Right Display Mode + +- **Matrix View**: Best for daily use, comparing multiple projects, and getting a quick overview with key metrics +- **Summary View**: Ideal for CI/CD pipelines, scripts, or when you just need basic information +- **Detailed View**: Use when you need to see every detail about the analysis, including all dependencies, scripts, and configurations +- **JSON**: Perfect for integration with other tools, creating reports, or feeding data to dashboards + +## Benefits of the New Matrix View + +1. **Reduced Scrolling**: All important information fits on one screen +2. **Easy Comparison**: Projects are displayed side-by-side in a table +3. **Visual Hierarchy**: Box-drawing characters and colors create clear sections +4. **Key Metrics Focus**: Shows only the most important information by default +5. **Modern Appearance**: Clean, professional look with proper alignment +6. **LLM-Friendly**: The structured format is easy for AI assistants to parse and understand + +## Color Coding + +The matrix view uses colors strategically: +- **Blue**: Headers and structural elements +- **Yellow**: Important values and counts +- **Green**: Success indicators and positive metrics +- **Magenta**: Frameworks and technologies +- **Cyan**: Interactive elements and services +- **Red**: Error states or missing components + +## Tips + +- The matrix view automatically adjusts based on terminal width +- Use `--no-color` to disable colors if needed +- Pipe to `less` for scrolling in detailed view: `sync-ctl analyze . -d | less -R` +- Combine with `jq` for JSON processing: `sync-ctl analyze . --json | jq '.projects[].name'` \ No newline at end of file diff --git a/docs/command-overview.md b/docs/command-overview.md new file mode 100644 index 00000000..b406a3c2 --- /dev/null +++ b/docs/command-overview.md @@ -0,0 +1,251 @@ +# πŸš€ Syncable CLI - Complete Command Overview + +This document provides a comprehensive overview of all available commands and their different display modes. + +## πŸ“Š Analysis Commands + +### 1. Basic Project Analysis + +```bash +# Modern matrix view (default) - compact dashboard +sync-ctl analyze . + +# Detailed view with full Docker analysis +sync-ctl analyze . --display detailed +# Or use the legacy flag +sync-ctl analyze . -d + +# Summary view for CI/CD pipelines +sync-ctl analyze . --display summary + +# JSON output for scripts +sync-ctl analyze . --json +``` + +### 2. Display Mode Comparison + +#### Matrix View (Default) πŸ†• +- **Best for**: Quick overview, comparing multiple projects +- **Features**: Modern dashboard with box-drawing characters, side-by-side project comparison, key metrics +- **Docker Info**: Overview with service counts and orchestration patterns +- **Note**: Box alignment improvements in progress for better visual consistency + +#### Detailed View +- **Best for**: In-depth analysis, debugging, comprehensive reports +- **Features**: Full Docker analysis, complete technology breakdown, all metadata +- **Docker Info**: Complete Docker infrastructure analysis including: + - Dockerfile analysis with base images, ports, stages + - Docker Compose services with dependencies and networking + - Orchestration patterns and service discovery + - Port mappings and volume configurations +- **Usage**: Use this view when you need complete information about your project + +## πŸ” Security & Vulnerability Commands + +### 3. Security Analysis + +```bash +# Comprehensive security scan +sync-ctl security . + +# Include low-severity findings +sync-ctl security . --include-low + +# Skip specific checks +sync-ctl security . --no-secrets --no-code-patterns + +# Export security report +sync-ctl security . --output security-report.json --format json + +# Fail CI/CD on security findings +sync-ctl security . --fail-on-findings +``` + +### 4. Vulnerability Scanning + +```bash +# Scan all dependencies for vulnerabilities +sync-ctl vulnerabilities . + +# Filter by severity +sync-ctl vulnerabilities . --severity high + +# Export vulnerability report +sync-ctl vulnerabilities . --format json --output vulns.json +``` + +### 5. Dependency Analysis + +```bash +# Analyze dependencies with licenses +sync-ctl dependencies . --licenses + +# Include vulnerability checking +sync-ctl dependencies . --vulnerabilities + +# Production dependencies only +sync-ctl dependencies . --prod-only + +# JSON output +sync-ctl dependencies . --format json +``` + +## πŸ› οΈ Tool Management Commands + +### 6. Vulnerability Scanning Tools + +```bash +# Check tool installation status +sync-ctl tools status + +# Install missing tools +sync-ctl tools install + +# Install for specific languages +sync-ctl tools install --languages rust,python + +# Verify tool functionality +sync-ctl tools verify + +# Get installation guide +sync-ctl tools guide +``` + +## πŸ—οΈ Generation Commands + +### 7. IaC Generation + +```bash +# Generate all IaC files +sync-ctl generate . + +# Generate specific types +sync-ctl generate . --dockerfile --compose +sync-ctl generate . --terraform + +# Dry run (preview only) +sync-ctl generate . --dry-run + +# Custom output directory +sync-ctl generate . --output ./infrastructure/ +``` + +## πŸ”„ Validation Commands + +### 8. IaC Validation (Coming Soon) + +```bash +# Validate generated IaC files +sync-ctl validate . + +# Validate specific types +sync-ctl validate . --types dockerfile,compose + +# Auto-fix issues +sync-ctl validate . --fix +``` + +## πŸ“‹ Information Commands + +### 9. Support Information + +```bash +# Show supported languages +sync-ctl support --languages + +# Show supported frameworks +sync-ctl support --frameworks + +# Show all supported technologies +sync-ctl support +``` + +## 🎯 Advanced Usage Examples + +### Complete Project Analysis Workflow + +```bash +# 1. Quick overview +sync-ctl analyze . + +# 2. Detailed analysis with Docker +sync-ctl analyze . --display detailed + +# 3. Security scan +sync-ctl security . + +# 4. Vulnerability check +sync-ctl vulnerabilities . --severity medium + +# 5. Generate IaC +sync-ctl generate . --all +``` + +### CI/CD Integration + +```bash +# Quick check for CI/CD +sync-ctl analyze . --display summary + +# Security scan that fails on findings +sync-ctl security . --fail-on-findings + +# Vulnerability scan with threshold +sync-ctl vulnerabilities . --severity high + +# JSON reports for processing +sync-ctl dependencies . --vulnerabilities --format json > deps.json +``` + +### Monorepo Analysis + +```bash +# Analyze entire monorepo +sync-ctl analyze . + +# Matrix view shows all projects side-by-side +sync-ctl analyze . --display matrix + +# Individual project analysis +cd frontend && sync-ctl analyze . --display detailed +cd ../backend && sync-ctl analyze . --display detailed +``` + +## πŸ”§ Configuration Options + +### Global Options +- `--config ` - Custom configuration file +- `--verbose` / `-v` - Verbose output +- `--json` - JSON output format + +### Analysis Options +- `--display ` - matrix (default), detailed, summary +- `--only ` - Analyze specific components only + +### Security Options +- `--include-low` - Include low-severity findings +- `--no-secrets` - Skip secret detection +- `--no-code-patterns` - Skip code pattern analysis +- `--frameworks ` - Check specific frameworks + +### Tool Options +- `--languages ` - Target specific languages +- `--dry-run` - Preview installation +- `--yes` - Skip confirmation prompts + +## πŸ’‘ Pro Tips + +1. **For Development**: Use `--display detailed` to see complete Docker analysis +2. **For CI/CD**: Use `--display summary` for quick checks +3. **For Security**: Run `sync-ctl security . --fail-on-findings` in CI/CD +4. **For Debugging**: Use `--verbose` for detailed logs +5. **For Automation**: Use `--json` output with other tools +6. **For Teams**: Share vulnerability reports with `--output` option + +## πŸš€ What's Coming Next + +- **Validation Commands**: Validate generated IaC files +- **Advanced Security**: Infrastructure security scanning +- **Cloud Integration**: Deploy directly to cloud platforms +- **Monitoring Setup**: Automated monitoring configuration +- **Performance Analysis**: Resource optimization recommendations \ No newline at end of file diff --git a/src/analyzer/display.rs b/src/analyzer/display.rs new file mode 100644 index 00000000..b6b02f26 --- /dev/null +++ b/src/analyzer/display.rs @@ -0,0 +1,1322 @@ +//! # Display Module +//! +//! Provides improved CLI output formatting with matrix/dashboard views for better readability +//! and easier parsing by both humans and LLMs. + +use crate::analyzer::{ + MonorepoAnalysis, ProjectCategory, ArchitecturePattern, + DetectedTechnology, TechnologyCategory, LibraryType, + DockerAnalysis, OrchestrationPattern, +}; +use colored::*; +use prettytable::{Table, Cell, Row, format}; + +/// Content line for measuring and drawing +#[derive(Debug, Clone)] +struct ContentLine { + label: String, + value: String, + label_colored: bool, +} + +impl ContentLine { + fn new(label: &str, value: &str, label_colored: bool) -> Self { + Self { + label: label.to_string(), + value: value.to_string(), + label_colored, + } + } + + fn empty() -> Self { + Self { + label: String::new(), + value: String::new(), + label_colored: false, + } + } + + fn separator() -> Self { + Self { + label: "SEPARATOR".to_string(), + value: String::new(), + label_colored: false, + } + } + + +} + +/// Box drawer that pre-calculates optimal dimensions +struct BoxDrawer { + title: String, + lines: Vec, + min_width: usize, + max_width: usize, +} + +impl BoxDrawer { + fn new(title: &str) -> Self { + Self { + title: title.to_string(), + lines: Vec::new(), + min_width: 60, + max_width: 150, // Increased to accommodate longer content + } + } + + fn add_line(&mut self, label: &str, value: &str, label_colored: bool) { + self.lines.push(ContentLine::new(label, value, label_colored)); + } + + fn add_value_only(&mut self, value: &str) { + self.lines.push(ContentLine::new("", value, false)); + } + + fn add_separator(&mut self) { + self.lines.push(ContentLine::separator()); + } + + fn add_empty(&mut self) { + self.lines.push(ContentLine::empty()); + } + + /// Calculate optimal box width based on content + fn calculate_optimal_width(&self) -> usize { + let title_width = visual_width(&self.title) + 6; // "β”Œβ”€ " + title + " " + extra padding + let mut max_content_width = 0; + + // Calculate the actual rendered width for each line + for line in &self.lines { + if line.label == "SEPARATOR" { + continue; + } + + let rendered_width = self.calculate_rendered_line_width(line); + max_content_width = max_content_width.max(rendered_width); + } + + // Use exact content width with minimal buffer for safety + let content_width_with_buffer = max_content_width + 2; // Minimal buffer for safety + + // Box needs padding: "β”‚ " + content + " β”‚" = content + 4 + let needed_width = content_width_with_buffer + 4; + + // Use the maximum of title width and content width, with a reasonable minimum + let min_reasonable_width = 50; + let optimal_width = title_width.max(needed_width).max(min_reasonable_width); + optimal_width.clamp(self.min_width, self.max_width) + } + + /// Calculate the actual rendered width of a line as it will appear + fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize { + // Calculate actual display widths without formatting + let label_display_width = visual_width(&line.label); + let mut value_display_width = visual_width(&line.value); + + // Be more conservative for values that could grow significantly + if !line.value.is_empty() { + // Add extra space for values that are likely numeric and could grow + if line.label.contains("Files") || line.label.contains("Duration") || + line.label.contains("Dependencies") || line.label.contains("Ports") || + line.label.contains("Services") || line.label.contains("Total") { + value_display_width = value_display_width.max(8); // Reserve space for larger numbers + } + } + + if !line.label.is_empty() && !line.value.is_empty() { + // Both label and value - they need space between them + // For colored labels, ensure minimum spacing but use actual width + let actual_label_width = if line.label_colored { + label_display_width.max(20) // At least 20, but can be longer + } else { + label_display_width + }; + actual_label_width + 1 + value_display_width + } else if !line.value.is_empty() { + // Value only + value_display_width + } else if !line.label.is_empty() { + // Label only + label_display_width + } else { + // Empty line + 0 + } + } + + /// Draw the complete box + fn draw(&self) -> String { + let box_width = self.calculate_optimal_width(); + let content_width = box_width - 4; // Available space for content + + let mut output = Vec::new(); + + // Top border + output.push(self.draw_top(box_width)); + + // Content lines + for line in &self.lines { + if line.label == "SEPARATOR" { + output.push(self.draw_separator(box_width)); + } else if line.label.is_empty() && line.value.is_empty() { + output.push(self.draw_empty_line(box_width)); + } else { + output.push(self.draw_content_line(line, content_width)); + } + } + + // Bottom border + output.push(self.draw_bottom(box_width)); + + output.join("\n") + } + + fn draw_top(&self, width: usize) -> String { + let title_colored = self.title.bright_cyan(); + let title_len = visual_width(&self.title); + + // "β”Œβ”€ " + title + " " + remaining dashes + "┐" + let prefix_len = 3; // "β”Œβ”€ " + let suffix_len = 1; // "┐" + let title_space = 1; // space after title + + let remaining_space = width - prefix_len - title_len - title_space - suffix_len; + + format!("β”Œβ”€ {} {}┐", + title_colored, + "─".repeat(remaining_space) + ) + } + + fn draw_bottom(&self, width: usize) -> String { + format!("β””{}β”˜", "─".repeat(width - 2)) + } + + fn draw_separator(&self, width: usize) -> String { + format!("β”‚ {} β”‚", "─".repeat(width - 4).dimmed()) + } + + fn draw_empty_line(&self, width: usize) -> String { + format!("β”‚ {} β”‚", " ".repeat(width - 4)) + } + + fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String { + // Format the label with color if needed, but calculate width dynamically + let formatted_label = if line.label_colored && !line.label.is_empty() { + line.label.bright_white().to_string() + } else { + line.label.clone() + }; + let formatted_value = line.value.clone(); + + // Calculate actual display widths + let label_display_width = visual_width(&line.label); // Use original label for width calculation + let value_display_width = visual_width(&formatted_value); + + // For colored labels, ensure minimum spacing but allow longer labels + let effective_label_width = if line.label_colored && !line.label.is_empty() { + label_display_width.max(20) // At least 20, but can be longer if needed + } else { + label_display_width + }; + + // Determine content layout + let content = if !line.label.is_empty() && !line.value.is_empty() { + // Both label and value - right-align the value + let available_space = content_width; + let min_space_between = 1; // Minimum space between label and value + + // Calculate how much space we need and have + let label_width = visual_width(&formatted_label); + let value_width = visual_width(&formatted_value); + let total_needed = label_width + min_space_between + value_width; + + if total_needed <= available_space { + // Everything fits - right-align the value + let padding_needed = available_space - label_width - value_width; + format!("{}{}{}", formatted_label, " ".repeat(padding_needed), formatted_value) + } else { + // Need to truncate value + let max_value_width = available_space.saturating_sub(label_width + min_space_between); + let truncated_value = truncate_to_width(&formatted_value, max_value_width); + let truncated_value_width = visual_width(&truncated_value); + let padding_needed = available_space - label_width - truncated_value_width; + format!("{}{}{}", formatted_label, " ".repeat(padding_needed), truncated_value) + } + } else if !line.value.is_empty() { + // Value only - left-align it (for descriptions, etc.) + let value_width = visual_width(&formatted_value); + if value_width <= content_width { + let padding_needed = content_width - value_width; + format!("{}{}", formatted_value, " ".repeat(padding_needed)) + } else { + // Truncate and ensure it fills exactly content_width + let truncated = truncate_to_width(&formatted_value, content_width); + let actual_width = visual_width(&truncated); + let padding_needed = content_width - actual_width; + format!("{}{}", truncated, " ".repeat(padding_needed)) + } + } else if !line.label.is_empty() { + // Label only - left-align it + let label_width = visual_width(&formatted_label); + if label_width <= content_width { + let padding_needed = content_width - label_width; + format!("{}{}", formatted_label, " ".repeat(padding_needed)) + } else { + // Truncate and ensure it fills exactly content_width + let truncated = truncate_to_width(&formatted_label, content_width); + let actual_width = visual_width(&truncated); + let padding_needed = content_width - actual_width; + format!("{}{}", truncated, " ".repeat(padding_needed)) + } + } else { + // Empty line + " ".repeat(content_width) + }; + + // Verify content is exactly the right width and fix if needed + let actual_content_width = visual_width(&content); + let final_content = if actual_content_width == content_width { + content + } else if actual_content_width < content_width { + // For table content (contains β”‚ or ─), don't add padding as it's already properly formatted + if content.contains("β”‚") || content.contains("─┼─") { + content + } else { + // Add padding to reach exact width for non-table content + let padding_needed = content_width - actual_content_width; + format!("{}{}", content, " ".repeat(padding_needed)) + } + } else { + // Truncate to exact width if somehow too long + truncate_to_width(&content, content_width) + }; + + format!("β”‚ {} β”‚", final_content) + } +} + +/// Calculate visual width of a string, handling ANSI color codes +fn visual_width(s: &str) -> usize { + let mut width = 0; + let mut chars = s.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch == '\x1b' { + // Skip ANSI escape sequence + if chars.peek() == Some(&'[') { + chars.next(); // consume '[' + while let Some(c) = chars.next() { + if c.is_ascii_alphabetic() { + break; // End of escape sequence + } + } + } + } else { + // Simple width calculation for common cases + // Most characters are width 1, some are width 0 or 2 + width += char_width(ch); + } + } + + width +} + +/// Simple character width calculation without external dependencies +fn char_width(ch: char) -> usize { + match ch { + // Control characters have width 0 + '\u{0000}'..='\u{001F}' | '\u{007F}' => 0, + // Combining marks have width 0 + '\u{0300}'..='\u{036F}' => 0, + // Emoji and symbols (width 2) + '\u{2600}'..='\u{26FF}' | // Miscellaneous Symbols + '\u{2700}'..='\u{27BF}' | // Dingbats + '\u{1F000}'..='\u{1F02F}' | // Mahjong Tiles + '\u{1F030}'..='\u{1F09F}' | // Domino Tiles + '\u{1F0A0}'..='\u{1F0FF}' | // Playing Cards + '\u{1F100}'..='\u{1F1FF}' | // Enclosed Alphanumeric Supplement + '\u{1F200}'..='\u{1F2FF}' | // Enclosed Ideographic Supplement + '\u{1F300}'..='\u{1F5FF}' | // Miscellaneous Symbols and Pictographs + '\u{1F600}'..='\u{1F64F}' | // Emoticons + '\u{1F650}'..='\u{1F67F}' | // Ornamental Dingbats + '\u{1F680}'..='\u{1F6FF}' | // Transport and Map Symbols + '\u{1F700}'..='\u{1F77F}' | // Alchemical Symbols + '\u{1F780}'..='\u{1F7FF}' | // Geometric Shapes Extended + '\u{1F800}'..='\u{1F8FF}' | // Supplemental Arrows-C + '\u{1F900}'..='\u{1F9FF}' | // Supplemental Symbols and Pictographs + // Full-width characters (common CJK ranges) + '\u{1100}'..='\u{115F}' | // Hangul Jamo + '\u{2E80}'..='\u{2EFF}' | // CJK Radicals + '\u{2F00}'..='\u{2FDF}' | // Kangxi Radicals + '\u{2FF0}'..='\u{2FFF}' | // Ideographic Description + '\u{3000}'..='\u{303E}' | // CJK Symbols and Punctuation + '\u{3041}'..='\u{3096}' | // Hiragana + '\u{30A1}'..='\u{30FA}' | // Katakana + '\u{3105}'..='\u{312D}' | // Bopomofo + '\u{3131}'..='\u{318E}' | // Hangul Compatibility Jamo + '\u{3190}'..='\u{31BA}' | // Kanbun + '\u{31C0}'..='\u{31E3}' | // CJK Strokes + '\u{31F0}'..='\u{31FF}' | // Katakana Phonetic Extensions + '\u{3200}'..='\u{32FF}' | // Enclosed CJK Letters and Months + '\u{3300}'..='\u{33FF}' | // CJK Compatibility + '\u{3400}'..='\u{4DBF}' | // CJK Extension A + '\u{4E00}'..='\u{9FFF}' | // CJK Unified Ideographs + '\u{A000}'..='\u{A48C}' | // Yi Syllables + '\u{A490}'..='\u{A4C6}' | // Yi Radicals + '\u{AC00}'..='\u{D7AF}' | // Hangul Syllables + '\u{F900}'..='\u{FAFF}' | // CJK Compatibility Ideographs + '\u{FE10}'..='\u{FE19}' | // Vertical Forms + '\u{FE30}'..='\u{FE6F}' | // CJK Compatibility Forms + '\u{FF00}'..='\u{FF60}' | // Fullwidth Forms + '\u{FFE0}'..='\u{FFE6}' => 2, + // Most other printable characters have width 1 + _ => 1, + } +} + +/// Truncate string to specified visual width, preserving color codes when possible +fn truncate_to_width(s: &str, max_width: usize) -> String { + if visual_width(s) <= max_width { + return s.to_string(); + } + + let mut result = String::new(); + let mut current_width = 0; + let mut chars = s.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch == '\x1b' { + // Preserve ANSI escape sequence + result.push(ch); + if chars.peek() == Some(&'[') { + result.push(chars.next().unwrap()); // consume '[' + while let Some(c) = chars.next() { + result.push(c); + if c.is_ascii_alphabetic() { + break; // End of escape sequence + } + } + } + } else { + let char_width = char_width(ch); + if current_width + char_width > max_width { + if max_width >= 3 { + result.push_str("..."); + } + break; + } + result.push(ch); + current_width += char_width; + } + } + + result +} + +/// Display mode for analysis output +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DisplayMode { + /// Compact matrix view (default) + Matrix, + /// Detailed vertical view (legacy) + Detailed, + /// Summary only + Summary, + /// JSON output + Json, +} + +/// Main display function that routes to appropriate formatter +pub fn display_analysis(analysis: &MonorepoAnalysis, mode: DisplayMode) { + match mode { + DisplayMode::Matrix => display_matrix_view(analysis), + DisplayMode::Detailed => display_detailed_view(analysis), + DisplayMode::Summary => display_summary_view(analysis), + DisplayMode::Json => display_json_view(analysis), + } +} + +/// Display analysis in a compact matrix/dashboard format +pub fn display_matrix_view(analysis: &MonorepoAnalysis) { + // Header + println!("\n{}", "═".repeat(100).bright_blue()); + println!("{}", "πŸ“Š PROJECT ANALYSIS DASHBOARD".bright_white().bold()); + println!("{}", "═".repeat(100).bright_blue()); + + // Architecture Overview Box + display_architecture_box(analysis); + + // Technology Stack Box + display_technology_stack_box(analysis); + + // Projects Matrix + if analysis.projects.len() > 1 { + display_projects_matrix(analysis); + } else { + display_single_project_matrix(analysis); + } + + // Docker Infrastructure Overview + if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) { + display_docker_overview_matrix(analysis); + } + + // Analysis Metrics Box + display_metrics_box(analysis); + + // Footer + println!("\n{}", "═".repeat(100).bright_blue()); +} + +/// Display architecture overview in a box +fn display_architecture_box(analysis: &MonorepoAnalysis) { + let mut box_drawer = BoxDrawer::new("Architecture Overview"); + + let arch_type = if analysis.is_monorepo { + format!("Monorepo ({} projects)", analysis.projects.len()) + } else { + "Single Project".to_string() + }; + + box_drawer.add_line("Type:", &arch_type.yellow(), true); + box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true); + + // Pattern description + let pattern_desc = match &analysis.technology_summary.architecture_pattern { + ArchitecturePattern::Monolithic => "Single, self-contained application", + ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation", + ArchitecturePattern::Microservices => "Multiple independent microservices", + ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces", + ArchitecturePattern::EventDriven => "Event-driven with decoupled components", + ArchitecturePattern::Mixed => "Mixed architecture patterns", + }; + box_drawer.add_value_only(&pattern_desc.dimmed()); + + println!("\n{}", box_drawer.draw()); +} + +/// Display technology stack overview +fn display_technology_stack_box(analysis: &MonorepoAnalysis) { + let mut box_drawer = BoxDrawer::new("Technology Stack"); + + let mut has_content = false; + + // Languages + if !analysis.technology_summary.languages.is_empty() { + let languages = analysis.technology_summary.languages.join(", "); + box_drawer.add_line("Languages:", &languages.blue(), true); + has_content = true; + } + + // Frameworks + if !analysis.technology_summary.frameworks.is_empty() { + let frameworks = analysis.technology_summary.frameworks.join(", "); + box_drawer.add_line("Frameworks:", &frameworks.magenta(), true); + has_content = true; + } + + // Databases + if !analysis.technology_summary.databases.is_empty() { + let databases = analysis.technology_summary.databases.join(", "); + box_drawer.add_line("Databases:", &databases.cyan(), true); + has_content = true; + } + + if !has_content { + box_drawer.add_value_only("No technologies detected"); + } + + println!("\n{}", box_drawer.draw()); +} + +/// Display projects in a matrix table format +fn display_projects_matrix(analysis: &MonorepoAnalysis) { + let mut box_drawer = BoxDrawer::new("Projects Matrix"); + + // Collect all data first to calculate optimal column widths + let mut project_data = Vec::new(); + for project in &analysis.projects { + let name = project.name.clone(); // Remove emoji to avoid width calculation issues + let proj_type = format_project_category(&project.project_category); + + let languages = project.analysis.languages.iter() + .map(|l| l.name.clone()) + .collect::>() + .join(", "); + + let main_tech = get_main_technologies(&project.analysis.technologies); + + let ports = if project.analysis.ports.is_empty() { + "-".to_string() + } else { + project.analysis.ports.iter() + .map(|p| p.number.to_string()) + .collect::>() + .join(", ") + }; + + let docker = if project.analysis.docker_analysis.is_some() { + "Yes" + } else { + "No" + }; + + let deps_count = project.analysis.dependencies.len().to_string(); + + project_data.push((name, proj_type.to_string(), languages, main_tech, ports, docker.to_string(), deps_count)); + } + + // Calculate column widths based on content + let headers = vec!["Project", "Type", "Languages", "Main Tech", "Ports", "Docker", "Deps"]; + let mut col_widths = headers.iter().map(|h| visual_width(h)).collect::>(); + + for (name, proj_type, languages, main_tech, ports, docker, deps_count) in &project_data { + col_widths[0] = col_widths[0].max(visual_width(name)); + col_widths[1] = col_widths[1].max(visual_width(proj_type)); + col_widths[2] = col_widths[2].max(visual_width(languages)); + col_widths[3] = col_widths[3].max(visual_width(main_tech)); + col_widths[4] = col_widths[4].max(visual_width(ports)); + col_widths[5] = col_widths[5].max(visual_width(docker)); + col_widths[6] = col_widths[6].max(visual_width(deps_count)); + } + + + // Create header row + let header_parts: Vec = headers.iter().zip(&col_widths) + .map(|(h, &w)| format!("{: = col_widths.iter() + .map(|&w| "─".repeat(w)) + .collect(); + let separator_line = separator_parts.join("─┼─"); + box_drawer.add_value_only(&separator_line); + + // Add data rows + for (name, proj_type, languages, main_tech, ports, docker, deps_count) in project_data { + let row_parts = vec![ + format!("{:>() + .join(", "); + box_drawer.add_line("Languages:", &lang_info.blue(), true); + } + + // Technologies by category + add_technologies_to_drawer(&project.analysis.technologies, &mut box_drawer); + + // Key metrics + box_drawer.add_separator(); + box_drawer.add_line("Key Metrics:", "", true); + + // Display metrics on two lines to fit properly + box_drawer.add_value_only(&format!("Entry Points: {} β”‚ Exposed Ports: {} β”‚ Env Variables: {}", + project.analysis.entry_points.len(), + project.analysis.ports.len(), + project.analysis.environment_variables.len() + ).cyan()); + + box_drawer.add_value_only(&format!("Build Scripts: {} β”‚ Dependencies: {}", + project.analysis.build_scripts.len(), + project.analysis.dependencies.len() + ).cyan()); + + // Confidence score with progress bar + add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer); + + println!("\n{}", box_drawer.draw()); + } +} + +/// Add technologies organized by category to the box drawer +fn add_technologies_to_drawer(technologies: &[DetectedTechnology], box_drawer: &mut BoxDrawer) { + let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new(); + + for tech in technologies { + by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech); + } + + // Display primary technology first + if let Some(primary) = technologies.iter().find(|t| t.is_primary) { + let primary_info = format!("{} {}", + primary.name.bright_yellow().bold(), + format!("({:.0}%)", primary.confidence * 100.0).dimmed() + ); + box_drawer.add_line("Primary Stack:", &primary_info, true); + } + + // Display other categories + let categories = [ + (TechnologyCategory::FrontendFramework, "Frameworks"), + (TechnologyCategory::BuildTool, "Build Tools"), + (TechnologyCategory::Database, "Databases"), + (TechnologyCategory::Testing, "Testing"), + ]; + + for (category, label) in &categories { + if let Some(techs) = by_category.get(category) { + let tech_names = techs.iter() + .map(|t| format!("{} ({:.0}%)", t.name, t.confidence * 100.0)) + .collect::>() + .join(", "); + + if !tech_names.is_empty() { + let label_with_colon = format!("{}:", label); + box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true); + } + } + } + + // Handle Library category separately since it's parameterized + for (cat, techs) in &by_category { + if matches!(cat, TechnologyCategory::Library(_)) { + let tech_names = techs.iter() + .map(|t| format!("{} ({:.0}%)", t.name, t.confidence * 100.0)) + .collect::>() + .join(", "); + + if !tech_names.is_empty() { + box_drawer.add_line("Libraries:", &tech_names.magenta(), true); + } + } + } +} + +/// Display Docker infrastructure overview in matrix format +fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) { + let mut box_drawer = BoxDrawer::new("Docker Infrastructure"); + + let mut total_dockerfiles = 0; + let mut total_compose_files = 0; + let mut total_services = 0; + let mut orchestration_patterns = std::collections::HashSet::new(); + + for project in &analysis.projects { + if let Some(docker) = &project.analysis.docker_analysis { + total_dockerfiles += docker.dockerfiles.len(); + total_compose_files += docker.compose_files.len(); + total_services += docker.services.len(); + orchestration_patterns.insert(&docker.orchestration_pattern); + } + } + + box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true); + box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true); + box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true); + + let patterns = orchestration_patterns.iter() + .map(|p| format!("{:?}", p)) + .collect::>() + .join(", "); + box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true); + + // Service connectivity summary + let mut has_services = false; + for project in &analysis.projects { + if let Some(docker) = &project.analysis.docker_analysis { + for service in &docker.services { + if !service.ports.is_empty() || !service.depends_on.is_empty() { + has_services = true; + break; + } + } + } + } + + if has_services { + box_drawer.add_separator(); + box_drawer.add_line("Service Connectivity:", "", true); + + for project in &analysis.projects { + if let Some(docker) = &project.analysis.docker_analysis { + for service in &docker.services { + if !service.ports.is_empty() || !service.depends_on.is_empty() { + let port_info = service.ports.iter() + .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port))) + .collect::>() + .join(", "); + + let deps_info = if service.depends_on.is_empty() { + String::new() + } else { + format!(" β†’ {}", service.depends_on.join(", ")) + }; + + let info = format!(" {}: {}{}", service.name, port_info, deps_info); + box_drawer.add_value_only(&info.cyan()); + } + } + } + } + } + + println!("\n{}", box_drawer.draw()); +} + +/// Display analysis metrics +fn display_metrics_box(analysis: &MonorepoAnalysis) { + let mut box_drawer = BoxDrawer::new("Analysis Metrics"); + + // Performance metrics + let duration_ms = analysis.metadata.analysis_duration_ms; + let duration_str = if duration_ms < 1000 { + format!("{}ms", duration_ms) + } else { + format!("{:.1}s", duration_ms as f64 / 1000.0) + }; + + // Create metrics line without emojis first to avoid width calculation issues + let metrics_line = format!( + "Duration: {} | Files: {} | Score: {}% | Version: {}", + duration_str, + analysis.metadata.files_analyzed, + format!("{:.0}", analysis.metadata.confidence_score * 100.0), + analysis.metadata.analyzer_version + ); + + // Apply single color to the entire line for consistency + let colored_metrics = metrics_line.cyan(); + box_drawer.add_value_only(&colored_metrics.to_string()); + + println!("\n{}", box_drawer.draw()); +} + +/// Add confidence score as a progress bar to the box drawer +fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) { + let percentage = (score * 100.0) as u8; + let bar_width = 20; + let filled = ((score * bar_width as f32) as usize).min(bar_width); + + let bar = format!("{}{}", + "β–ˆ".repeat(filled).green(), + "β–‘".repeat(bar_width - filled).dimmed() + ); + + let color = if percentage >= 80 { + "green" + } else if percentage >= 60 { + "yellow" + } else { + "red" + }; + + let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color)); + box_drawer.add_line("Confidence:", &confidence_info, true); +} + +/// Get main technologies for display +fn get_main_technologies(technologies: &[DetectedTechnology]) -> String { + let primary = technologies.iter().find(|t| t.is_primary); + let frameworks: Vec<_> = technologies.iter() + .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework)) + .take(2) + .collect(); + + let mut result = Vec::new(); + + if let Some(p) = primary { + result.push(p.name.clone()); + } + + for f in frameworks { + if Some(&f.name) != primary.map(|p| &p.name) { + result.push(f.name.clone()); + } + } + + if result.is_empty() { + "-".to_string() + } else { + result.join(", ") + } +} + +/// Display in detailed vertical format (legacy) +pub fn display_detailed_view(analysis: &MonorepoAnalysis) { + // Use the legacy detailed display format + println!("{}", "=".repeat(80)); + println!("\nπŸ“Š PROJECT ANALYSIS RESULTS"); + println!("{}", "=".repeat(80)); + + // Overall project information + if analysis.is_monorepo { + println!("\nπŸ—οΈ Architecture: Monorepo with {} projects", analysis.projects.len()); + println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern); + + display_architecture_description(&analysis.technology_summary.architecture_pattern); + } else { + println!("\nπŸ—οΈ Architecture: Single Project"); + } + + // Technology Summary + println!("\n🌐 Technology Summary:"); + if !analysis.technology_summary.languages.is_empty() { + println!(" Languages: {}", analysis.technology_summary.languages.join(", ")); + } + if !analysis.technology_summary.frameworks.is_empty() { + println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", ")); + } + if !analysis.technology_summary.databases.is_empty() { + println!(" Databases: {}", analysis.technology_summary.databases.join(", ")); + } + + // Individual project details + println!("\nπŸ“ Project Details:"); + println!("{}", "=".repeat(80)); + + for (i, project) in analysis.projects.iter().enumerate() { + println!("\n{} {}. {} ({})", + get_category_emoji(&project.project_category), + i + 1, + project.name, + format_project_category(&project.project_category) + ); + + if analysis.is_monorepo { + println!(" πŸ“‚ Path: {}", project.path.display()); + } + + // Languages for this project + if !project.analysis.languages.is_empty() { + println!(" 🌐 Languages:"); + for lang in &project.analysis.languages { + print!(" β€’ {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0); + if let Some(version) = &lang.version { + print!(" - Version: {}", version); + } + println!(); + } + } + + // Technologies for this project + if !project.analysis.technologies.is_empty() { + println!(" πŸš€ Technologies:"); + display_technologies_detailed_legacy(&project.analysis.technologies); + } + + // Entry Points + if !project.analysis.entry_points.is_empty() { + println!(" πŸ“ Entry Points ({}):", project.analysis.entry_points.len()); + for (j, entry) in project.analysis.entry_points.iter().enumerate() { + println!(" {}. File: {}", j + 1, entry.file.display()); + if let Some(func) = &entry.function { + println!(" Function: {}", func); + } + if let Some(cmd) = &entry.command { + println!(" Command: {}", cmd); + } + } + } + + // Ports + if !project.analysis.ports.is_empty() { + println!(" πŸ”Œ Exposed Ports ({}):", project.analysis.ports.len()); + for port in &project.analysis.ports { + println!(" β€’ Port {}: {:?}", port.number, port.protocol); + if let Some(desc) = &port.description { + println!(" {}", desc); + } + } + } + + // Environment Variables + if !project.analysis.environment_variables.is_empty() { + println!(" πŸ” Environment Variables ({}):", project.analysis.environment_variables.len()); + let required_vars: Vec<_> = project.analysis.environment_variables.iter() + .filter(|ev| ev.required) + .collect(); + let optional_vars: Vec<_> = project.analysis.environment_variables.iter() + .filter(|ev| !ev.required) + .collect(); + + if !required_vars.is_empty() { + println!(" Required:"); + for var in required_vars { + println!(" β€’ {} {}", + var.name, + if let Some(desc) = &var.description { + format!("({})", desc) + } else { + String::new() + } + ); + } + } + + if !optional_vars.is_empty() { + println!(" Optional:"); + for var in optional_vars { + println!(" β€’ {} = {:?}", + var.name, + var.default_value.as_deref().unwrap_or("no default") + ); + } + } + } + + // Build Scripts + if !project.analysis.build_scripts.is_empty() { + println!(" πŸ”¨ Build Scripts ({}):", project.analysis.build_scripts.len()); + let default_scripts: Vec<_> = project.analysis.build_scripts.iter() + .filter(|bs| bs.is_default) + .collect(); + let other_scripts: Vec<_> = project.analysis.build_scripts.iter() + .filter(|bs| !bs.is_default) + .collect(); + + if !default_scripts.is_empty() { + println!(" Default scripts:"); + for script in default_scripts { + println!(" β€’ {}: {}", script.name, script.command); + if let Some(desc) = &script.description { + println!(" {}", desc); + } + } + } + + if !other_scripts.is_empty() { + println!(" Other scripts:"); + for script in other_scripts { + println!(" β€’ {}: {}", script.name, script.command); + if let Some(desc) = &script.description { + println!(" {}", desc); + } + } + } + } + + // Dependencies (sample) + if !project.analysis.dependencies.is_empty() { + println!(" πŸ“¦ Dependencies ({}):", project.analysis.dependencies.len()); + if project.analysis.dependencies.len() <= 5 { + for (name, version) in &project.analysis.dependencies { + println!(" β€’ {} v{}", name, version); + } + } else { + // Show first 5 + for (name, version) in project.analysis.dependencies.iter().take(5) { + println!(" β€’ {} v{}", name, version); + } + println!(" ... and {} more", project.analysis.dependencies.len() - 5); + } + } + + // Docker Infrastructure Analysis + if let Some(docker_analysis) = &project.analysis.docker_analysis { + display_docker_analysis_detailed_legacy(docker_analysis); + } + + // Project type + println!(" 🎯 Project Type: {:?}", project.analysis.project_type); + + if i < analysis.projects.len() - 1 { + println!("{}", "-".repeat(40)); + } + } + + // Summary + println!("\nπŸ“‹ ANALYSIS SUMMARY"); + println!("{}", "=".repeat(80)); + println!("βœ… Project Analysis Complete!"); + + if analysis.is_monorepo { + println!("\nπŸ—οΈ Monorepo Architecture:"); + println!(" β€’ Total projects: {}", analysis.projects.len()); + println!(" β€’ Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern); + + let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count(); + let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count(); + let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count(); + let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count(); + + if frontend_count > 0 { println!(" β€’ Frontend projects: {}", frontend_count); } + if backend_count > 0 { println!(" β€’ Backend/API projects: {}", backend_count); } + if service_count > 0 { println!(" β€’ Service projects: {}", service_count); } + if lib_count > 0 { println!(" β€’ Library projects: {}", lib_count); } + } + + println!("\nπŸ“ˆ Analysis Metadata:"); + println!(" β€’ Duration: {}ms", analysis.metadata.analysis_duration_ms); + println!(" β€’ Files analyzed: {}", analysis.metadata.files_analyzed); + println!(" β€’ Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0); + println!(" β€’ Analyzer version: {}", analysis.metadata.analyzer_version); +} + +/// Helper function for legacy detailed technology display +fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) { + // Group technologies by category + let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new(); + + for tech in technologies { + by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech); + } + + // Find and display primary technology + if let Some(primary) = technologies.iter().find(|t| t.is_primary) { + println!("\nπŸ› οΈ Technology Stack:"); + println!(" 🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0); + println!(" Architecture driver for this project"); + } + + // Display categories in order + let categories = [ + (TechnologyCategory::MetaFramework, "πŸ—οΈ Meta-Frameworks"), + (TechnologyCategory::BackendFramework, "πŸ–₯️ Backend Frameworks"), + (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"), + (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"), + (TechnologyCategory::Library(LibraryType::Utility), "πŸ“š Core Libraries"), + (TechnologyCategory::BuildTool, "πŸ”¨ Build Tools"), + (TechnologyCategory::PackageManager, "πŸ“¦ Package Managers"), + (TechnologyCategory::Database, "πŸ—ƒοΈ Database & ORM"), + (TechnologyCategory::Runtime, "⚑ Runtimes"), + (TechnologyCategory::Testing, "πŸ§ͺ Testing"), + ]; + + for (category, label) in &categories { + if let Some(techs) = by_category.get(category) { + if !techs.is_empty() { + println!("\n {}:", label); + for tech in techs { + println!(" β€’ {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0); + if let Some(version) = &tech.version { + println!(" Version: {}", version); + } + } + } + } + } + + // Handle other Library types separately + for (cat, techs) in &by_category { + match cat { + TechnologyCategory::Library(lib_type) => { + let label = match lib_type { + LibraryType::StateManagement => "πŸ”„ State Management", + LibraryType::DataFetching => "πŸ”ƒ Data Fetching", + LibraryType::Routing => "πŸ—ΊοΈ Routing", + LibraryType::Styling => "🎨 Styling", + LibraryType::HttpClient => "🌐 HTTP Clients", + LibraryType::Authentication => "πŸ” Authentication", + LibraryType::Other(_) => "πŸ“¦ Other Libraries", + _ => continue, // Skip already handled UI and Utility + }; + + // Only print if not already handled above + if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() { + println!("\n {}:", label); + for tech in techs { + println!(" β€’ {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0); + if let Some(version) = &tech.version { + println!(" Version: {}", version); + } + } + } + } + _ => {} // Other categories already handled in the array + } + } +} + +/// Helper function for legacy Docker analysis display +fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) { + println!("\n 🐳 Docker Infrastructure Analysis:"); + + // Dockerfiles + if !docker_analysis.dockerfiles.is_empty() { + println!(" πŸ“„ Dockerfiles ({}):", docker_analysis.dockerfiles.len()); + for dockerfile in &docker_analysis.dockerfiles { + println!(" β€’ {}", dockerfile.path.display()); + if let Some(env) = &dockerfile.environment { + println!(" Environment: {}", env); + } + if let Some(base_image) = &dockerfile.base_image { + println!(" Base image: {}", base_image); + } + if !dockerfile.exposed_ports.is_empty() { + println!(" Exposed ports: {}", + dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::>().join(", ")); + } + if dockerfile.is_multistage { + println!(" Multi-stage build: {} stages", dockerfile.build_stages.len()); + } + println!(" Instructions: {}", dockerfile.instruction_count); + } + } + + // Compose files + if !docker_analysis.compose_files.is_empty() { + println!(" πŸ“‹ Compose Files ({}):", docker_analysis.compose_files.len()); + for compose_file in &docker_analysis.compose_files { + println!(" β€’ {}", compose_file.path.display()); + if let Some(env) = &compose_file.environment { + println!(" Environment: {}", env); + } + if let Some(version) = &compose_file.version { + println!(" Version: {}", version); + } + if !compose_file.service_names.is_empty() { + println!(" Services: {}", compose_file.service_names.join(", ")); + } + if !compose_file.networks.is_empty() { + println!(" Networks: {}", compose_file.networks.join(", ")); + } + if !compose_file.volumes.is_empty() { + println!(" Volumes: {}", compose_file.volumes.join(", ")); + } + } + } + + // Rest of the detailed Docker display... + println!(" πŸ—οΈ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern); + match docker_analysis.orchestration_pattern { + OrchestrationPattern::SingleContainer => { + println!(" Simple containerized application"); + } + OrchestrationPattern::DockerCompose => { + println!(" Multi-service Docker Compose setup"); + } + OrchestrationPattern::Microservices => { + println!(" Microservices architecture with service discovery"); + } + OrchestrationPattern::EventDriven => { + println!(" Event-driven architecture with message queues"); + } + OrchestrationPattern::ServiceMesh => { + println!(" Service mesh for advanced service communication"); + } + OrchestrationPattern::Mixed => { + println!(" Mixed/complex orchestration pattern"); + } + } +} + +/// Display architecture description +fn display_architecture_description(pattern: &ArchitecturePattern) { + match pattern { + ArchitecturePattern::Monolithic => { + println!(" πŸ“¦ This is a single, self-contained application"); + } + ArchitecturePattern::Fullstack => { + println!(" 🌐 This is a full-stack application with separate frontend and backend"); + } + ArchitecturePattern::Microservices => { + println!(" πŸ”— This is a microservices architecture with multiple independent services"); + } + ArchitecturePattern::ApiFirst => { + println!(" πŸ”Œ This is an API-first architecture focused on service interfaces"); + } + ArchitecturePattern::EventDriven => { + println!(" πŸ“‘ This is an event-driven architecture with decoupled components"); + } + ArchitecturePattern::Mixed => { + println!(" πŸ”€ This is a mixed architecture combining multiple patterns"); + } + } +} + +/// Display summary view only +pub fn display_summary_view(analysis: &MonorepoAnalysis) { + println!("\n{} {}", "β–Ά".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold()); + println!("{}", "─".repeat(50).dimmed()); + + println!("{} Architecture: {}", "β”‚".dimmed(), + if analysis.is_monorepo { + format!("Monorepo ({} projects)", analysis.projects.len()).yellow() + } else { + "Single Project".to_string().yellow() + } + ); + + println!("{} Pattern: {}", "β”‚".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green()); + println!("{} Stack: {}", "β”‚".dimmed(), analysis.technology_summary.languages.join(", ").blue()); + + if !analysis.technology_summary.frameworks.is_empty() { + println!("{} Frameworks: {}", "β”‚".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta()); + } + + println!("{} Analysis Time: {}ms", "β”‚".dimmed(), analysis.metadata.analysis_duration_ms); + println!("{} Confidence: {:.0}%", "β”‚".dimmed(), analysis.metadata.confidence_score * 100.0); + + println!("{}", "─".repeat(50).dimmed()); +} + +/// Display JSON output +pub fn display_json_view(analysis: &MonorepoAnalysis) { + match serde_json::to_string_pretty(analysis) { + Ok(json) => println!("{}", json), + Err(e) => eprintln!("Error serializing to JSON: {}", e), + } +} + +/// Get emoji for project category +fn get_category_emoji(category: &ProjectCategory) -> &'static str { + match category { + ProjectCategory::Frontend => "🌐", + ProjectCategory::Backend => "βš™οΈ", + ProjectCategory::Api => "πŸ”Œ", + ProjectCategory::Service => "πŸš€", + ProjectCategory::Library => "πŸ“š", + ProjectCategory::Tool => "πŸ”§", + ProjectCategory::Documentation => "πŸ“–", + ProjectCategory::Infrastructure => "πŸ—οΈ", + ProjectCategory::Unknown => "❓", + } +} + +/// Format project category name +fn format_project_category(category: &ProjectCategory) -> &'static str { + match category { + ProjectCategory::Frontend => "Frontend", + ProjectCategory::Backend => "Backend", + ProjectCategory::Api => "API", + ProjectCategory::Service => "Service", + ProjectCategory::Library => "Library", + ProjectCategory::Tool => "Tool", + ProjectCategory::Documentation => "Documentation", + ProjectCategory::Infrastructure => "Infrastructure", + ProjectCategory::Unknown => "Unknown", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_modes() { + // Test that display modes are properly defined + assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix); + assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed); + } +} \ No newline at end of file diff --git a/src/analyzer/docker_analyzer.rs b/src/analyzer/docker_analyzer.rs index 6a6063d4..5218b204 100644 --- a/src/analyzer/docker_analyzer.rs +++ b/src/analyzer/docker_analyzer.rs @@ -76,7 +76,7 @@ pub struct ComposeFileInfo { } /// Container orchestration patterns -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum OrchestrationPattern { /// Single container application SingleContainer, diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 8b935e8e..d59e8f73 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -21,6 +21,7 @@ pub mod security_analyzer; pub mod tool_installer; pub mod monorepo_detector; pub mod docker_analyzer; +pub mod display; // Re-export dependency analysis types pub use dependency_parser::{ @@ -59,7 +60,7 @@ pub struct DetectedLanguage { } /// Categories of detected technologies with proper classification -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum TechnologyCategory { /// Full-stack meta-frameworks that provide complete application structure MetaFramework, @@ -82,7 +83,7 @@ pub enum TechnologyCategory { } /// Specific types of libraries for better classification -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum LibraryType { /// UI libraries (React, Vue, Preact) UI, diff --git a/src/cli.rs b/src/cli.rs index cf88c552..ce23eafd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -39,10 +39,14 @@ pub enum Commands { #[arg(short, long)] json: bool, - /// Show detailed analysis information - #[arg(short, long)] + /// Show detailed analysis information (legacy format) + #[arg(short, long, conflicts_with = "display")] detailed: bool, + /// Display format for analysis results + #[arg(long, value_enum, default_value = "matrix")] + display: Option, + /// Only analyze specific aspects (languages, frameworks, dependencies) #[arg(long, value_delimiter = ',')] only: Option>, @@ -270,6 +274,16 @@ pub enum OutputFormat { Json, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum DisplayFormat { + /// Compact matrix/dashboard view (modern, easy to scan) + Matrix, + /// Detailed vertical view (legacy format with all details) + Detailed, + /// Brief summary only + Summary, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum SeverityThreshold { Low, diff --git a/src/main.rs b/src/main.rs index 9e27bf03..8c4a1f8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,18 @@ use clap::Parser; use syncable_cli::{ analyzer::{ self, vulnerability_checker::VulnerabilitySeverity, DetectedTechnology, TechnologyCategory, LibraryType, - analyze_monorepo, MonorepoAnalysis, ProjectCategory, ArchitecturePattern, + analyze_monorepo, analyze_monorepo_with_config, MonorepoAnalysis, ProjectCategory, ArchitecturePattern, DockerAnalysis, DockerfileInfo, ComposeFileInfo, DockerService, OrchestrationPattern, - NetworkingConfig, DockerEnvironment + NetworkingConfig, DockerEnvironment, + SecurityAnalyzer, SecurityAnalysisConfig, SecuritySeverity, + DependencyAnalysis, VulnerabilitySeverity as VulnSeverity, + vulnerability_checker::VulnerabilityChecker }, - cli::{Cli, Commands, ToolsCommand, OutputFormat, SeverityThreshold}, + cli::{Cli, Commands, ToolsCommand, OutputFormat, SeverityThreshold, DisplayFormat}, config, generator, }; +use syncable_cli::analyzer::display::{display_analysis, DisplayMode}; use std::process; use std::collections::HashMap; use std::fs; @@ -43,8 +47,8 @@ async fn run() -> syncable_cli::Result<()> { // Execute command let result = match cli.command { - Commands::Analyze { path, json, detailed, only } => { - handle_analyze(path, json, detailed, only) + Commands::Analyze { path, json, detailed, display, only } => { + handle_analyze(path, json, detailed, display, only) } Commands::Generate { path, @@ -154,6 +158,7 @@ fn handle_analyze( path: std::path::PathBuf, json: bool, detailed: bool, + display: Option, _only: Option>, ) -> syncable_cli::Result<()> { println!("πŸ” Analyzing project: {}", path.display()); @@ -161,499 +166,24 @@ fn handle_analyze( let monorepo_analysis = analyze_monorepo(&path)?; if json { - println!("{}", serde_json::to_string_pretty(&monorepo_analysis)?); - } else if detailed { - display_detailed_monorepo_analysis(&monorepo_analysis); + display_analysis(&monorepo_analysis, DisplayMode::Json); } else { - display_summary_monorepo_analysis(&monorepo_analysis); - } - - Ok(()) -} - -fn display_detailed_monorepo_analysis(analysis: &MonorepoAnalysis) { - println!("{}", "=".repeat(80)); - println!("\nπŸ“Š PROJECT ANALYSIS RESULTS"); - println!("{}", "=".repeat(80)); - - // Overall project information - if analysis.is_monorepo { - println!("\nπŸ—οΈ Architecture: Monorepo with {} projects", analysis.projects.len()); - println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern); - - display_architecture_description(&analysis.technology_summary.architecture_pattern); - } else { - println!("\nπŸ—οΈ Architecture: Single Project"); - } - - // Technology Summary - println!("\n🌐 Technology Summary:"); - if !analysis.technology_summary.languages.is_empty() { - println!(" Languages: {}", analysis.technology_summary.languages.join(", ")); - } - if !analysis.technology_summary.frameworks.is_empty() { - println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", ")); - } - if !analysis.technology_summary.databases.is_empty() { - println!(" Databases: {}", analysis.technology_summary.databases.join(", ")); - } - - // Individual project details - println!("\nπŸ“ Project Details:"); - println!("{}", "=".repeat(80)); - - for (i, project) in analysis.projects.iter().enumerate() { - println!("\n{} {}. {} ({})", - get_category_emoji(&project.project_category), - i + 1, - project.name, - format_project_category(&project.project_category) - ); - - if analysis.is_monorepo { - println!(" πŸ“‚ Path: {}", project.path.display()); - } - - // Languages for this project - if !project.analysis.languages.is_empty() { - println!(" 🌐 Languages:"); - for lang in &project.analysis.languages { - print!(" β€’ {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0); - if let Some(version) = &lang.version { - print!(" - Version: {}", version); - } - println!(); - } - } - - // Technologies for this project - if !project.analysis.technologies.is_empty() { - println!(" πŸš€ Technologies:"); - display_technologies_detailed(&project.analysis.technologies); - } - - // Entry Points - if !project.analysis.entry_points.is_empty() { - println!(" πŸ“ Entry Points ({}):", project.analysis.entry_points.len()); - for (j, entry) in project.analysis.entry_points.iter().enumerate() { - println!(" {}. File: {}", j + 1, entry.file.display()); - if let Some(func) = &entry.function { - println!(" Function: {}", func); - } - if let Some(cmd) = &entry.command { - println!(" Command: {}", cmd); - } - } - } - - // Ports - if !project.analysis.ports.is_empty() { - println!(" πŸ”Œ Exposed Ports ({}):", project.analysis.ports.len()); - for port in &project.analysis.ports { - println!(" β€’ Port {}: {:?}", port.number, port.protocol); - if let Some(desc) = &port.description { - println!(" {}", desc); - } - } - } - - // Environment Variables - if !project.analysis.environment_variables.is_empty() { - println!(" πŸ” Environment Variables ({}):", project.analysis.environment_variables.len()); - let required_vars: Vec<_> = project.analysis.environment_variables.iter() - .filter(|ev| ev.required) - .collect(); - let optional_vars: Vec<_> = project.analysis.environment_variables.iter() - .filter(|ev| !ev.required) - .collect(); - - if !required_vars.is_empty() { - println!(" Required:"); - for var in required_vars { - println!(" β€’ {} {}", - var.name, - if let Some(desc) = &var.description { - format!("({})", desc) - } else { - String::new() - } - ); - } - } - - if !optional_vars.is_empty() { - println!(" Optional:"); - for var in optional_vars { - println!(" β€’ {} = {:?}", - var.name, - var.default_value.as_deref().unwrap_or("no default") - ); - } - } - } - - // Build Scripts - if !project.analysis.build_scripts.is_empty() { - println!(" πŸ”¨ Build Scripts ({}):", project.analysis.build_scripts.len()); - let default_scripts: Vec<_> = project.analysis.build_scripts.iter() - .filter(|bs| bs.is_default) - .collect(); - let other_scripts: Vec<_> = project.analysis.build_scripts.iter() - .filter(|bs| !bs.is_default) - .collect(); - - if !default_scripts.is_empty() { - println!(" Default scripts:"); - for script in default_scripts { - println!(" β€’ {}: {}", script.name, script.command); - if let Some(desc) = &script.description { - println!(" {}", desc); - } - } - } - - if !other_scripts.is_empty() { - println!(" Other scripts:"); - for script in other_scripts { - println!(" β€’ {}: {}", script.name, script.command); - if let Some(desc) = &script.description { - println!(" {}", desc); - } - } - } - } - - // Dependencies (sample) - if !project.analysis.dependencies.is_empty() { - println!(" πŸ“¦ Dependencies ({}):", project.analysis.dependencies.len()); - if project.analysis.dependencies.len() <= 5 { - for (name, version) in &project.analysis.dependencies { - println!(" β€’ {} v{}", name, version); - } - } else { - // Show first 5 - for (name, version) in project.analysis.dependencies.iter().take(5) { - println!(" β€’ {} v{}", name, version); - } - println!(" ... and {} more", project.analysis.dependencies.len() - 5); - } - } - - // Docker Infrastructure Analysis - if let Some(docker_analysis) = &project.analysis.docker_analysis { - display_docker_analysis_detailed(docker_analysis); - } - - // Project type - println!(" 🎯 Project Type: {:?}", project.analysis.project_type); - - if i < analysis.projects.len() - 1 { - println!("{}", "-".repeat(40)); - } - } - - // Summary - println!("\nπŸ“‹ ANALYSIS SUMMARY"); - println!("{}", "=".repeat(80)); - println!("βœ… Project Analysis Complete!"); - - if analysis.is_monorepo { - println!("\nπŸ—οΈ Monorepo Architecture:"); - println!(" β€’ Total projects: {}", analysis.projects.len()); - println!(" β€’ Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern); - - let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count(); - let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count(); - let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count(); - let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count(); - - if frontend_count > 0 { println!(" β€’ Frontend projects: {}", frontend_count); } - if backend_count > 0 { println!(" β€’ Backend/API projects: {}", backend_count); } - if service_count > 0 { println!(" β€’ Service projects: {}", service_count); } - if lib_count > 0 { println!(" β€’ Library projects: {}", lib_count); } - } - - println!("\nπŸ“ˆ Analysis Metadata:"); - println!(" β€’ Duration: {}ms", analysis.metadata.analysis_duration_ms); - println!(" β€’ Files analyzed: {}", analysis.metadata.files_analyzed); - println!(" β€’ Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0); - println!(" β€’ Analyzer version: {}", analysis.metadata.analyzer_version); -} - -fn display_docker_analysis_detailed(docker_analysis: &DockerAnalysis) { - println!("\n 🐳 Docker Infrastructure Analysis:"); - - // Dockerfiles - if !docker_analysis.dockerfiles.is_empty() { - println!(" πŸ“„ Dockerfiles ({}):", docker_analysis.dockerfiles.len()); - for dockerfile in &docker_analysis.dockerfiles { - println!(" β€’ {}", dockerfile.path.display()); - if let Some(env) = &dockerfile.environment { - println!(" Environment: {}", env); - } - if let Some(base_image) = &dockerfile.base_image { - println!(" Base image: {}", base_image); - } - if !dockerfile.exposed_ports.is_empty() { - println!(" Exposed ports: {}", - dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::>().join(", ")); - } - if dockerfile.is_multistage { - println!(" Multi-stage build: {} stages", dockerfile.build_stages.len()); - } - println!(" Instructions: {}", dockerfile.instruction_count); - } - } - - // Compose files - if !docker_analysis.compose_files.is_empty() { - println!(" πŸ“‹ Compose Files ({}):", docker_analysis.compose_files.len()); - for compose_file in &docker_analysis.compose_files { - println!(" β€’ {}", compose_file.path.display()); - if let Some(env) = &compose_file.environment { - println!(" Environment: {}", env); - } - if let Some(version) = &compose_file.version { - println!(" Version: {}", version); - } - if !compose_file.service_names.is_empty() { - println!(" Services: {}", compose_file.service_names.join(", ")); - } - if !compose_file.networks.is_empty() { - println!(" Networks: {}", compose_file.networks.join(", ")); - } - if !compose_file.volumes.is_empty() { - println!(" Volumes: {}", compose_file.volumes.join(", ")); - } - } - } - - // Services - if !docker_analysis.services.is_empty() { - println!(" πŸš€ Services ({}):", docker_analysis.services.len()); - for service in &docker_analysis.services { - println!(" β€’ {} ({})", service.name, - match &service.image_or_build { - syncable_cli::analyzer::docker_analyzer::ImageOrBuild::Image(img) => format!("image: {}", img), - syncable_cli::analyzer::docker_analyzer::ImageOrBuild::Build { context, .. } => format!("build: {}", context), - } - ); - - // Port mappings - if !service.ports.is_empty() { - println!(" Ports:"); - for port in &service.ports { - if let Some(host_port) = port.host_port { - println!(" - {}:{} ({})", host_port, port.container_port, port.protocol); - } else { - println!(" - {} ({})", port.container_port, port.protocol); - } - } - } - - // Dependencies - if !service.depends_on.is_empty() { - println!(" Depends on: {}", service.depends_on.join(", ")); - } - - // Networks - if !service.networks.is_empty() { - println!(" Networks: {}", service.networks.join(", ")); - } - - // Environment variables (show count if many) - if !service.environment.is_empty() { - if service.environment.len() <= 3 { - println!(" Environment:"); - for (key, value) in &service.environment { - let display_value = if value.is_empty() { "(set)" } else { value }; - println!(" - {}={}", key, display_value); - } - } else { - println!(" Environment variables: {} defined", service.environment.len()); - } + // Determine display mode + let mode = if detailed { + // Legacy flag for backward compatibility + DisplayMode::Detailed + } else { + match display { + Some(DisplayFormat::Matrix) | None => DisplayMode::Matrix, + Some(DisplayFormat::Detailed) => DisplayMode::Detailed, + Some(DisplayFormat::Summary) => DisplayMode::Summary, } - } - } - - // Networking configuration - println!(" 🌐 Networking:"); - if docker_analysis.networking.service_discovery.internal_dns { - println!(" β€’ Internal DNS enabled"); - } - if !docker_analysis.networking.service_discovery.external_tools.is_empty() { - println!(" β€’ Service discovery tools: {}", - docker_analysis.networking.service_discovery.external_tools.join(", ")); - } - if docker_analysis.networking.service_discovery.service_mesh { - println!(" β€’ Service mesh detected"); - } - - // Load balancing - if !docker_analysis.networking.load_balancing.is_empty() { - println!(" β€’ Load balancers:"); - for lb in &docker_analysis.networking.load_balancing { - println!(" - {} ({}): {} backends", - lb.service, lb.lb_type, lb.backends.len()); - } - } - - // External connectivity - if !docker_analysis.networking.external_connectivity.exposed_services.is_empty() { - println!(" β€’ External services:"); - for exposed in &docker_analysis.networking.external_connectivity.exposed_services { - let ssl_indicator = if exposed.ssl_enabled { " (SSL)" } else { "" }; - println!(" - {}: ports {}{}", - exposed.service, - exposed.external_ports.iter().map(|p| p.to_string()).collect::>().join(", "), - ssl_indicator - ); - } - } - - if !docker_analysis.networking.external_connectivity.ingress_patterns.is_empty() { - println!(" β€’ Ingress patterns: {}", - docker_analysis.networking.external_connectivity.ingress_patterns.join(", ")); - } - - if !docker_analysis.networking.external_connectivity.api_gateways.is_empty() { - println!(" β€’ API gateways: {}", - docker_analysis.networking.external_connectivity.api_gateways.join(", ")); - } - - // Orchestration pattern - println!(" πŸ—οΈ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern); - - match docker_analysis.orchestration_pattern { - OrchestrationPattern::SingleContainer => { - println!(" Simple containerized application"); - } - OrchestrationPattern::DockerCompose => { - println!(" Multi-service Docker Compose setup"); - } - OrchestrationPattern::Microservices => { - println!(" Microservices architecture with service discovery"); - } - OrchestrationPattern::EventDriven => { - println!(" Event-driven architecture with message queues"); - } - OrchestrationPattern::ServiceMesh => { - println!(" Service mesh for advanced service communication"); - } - OrchestrationPattern::Mixed => { - println!(" Mixed/complex orchestration pattern"); - } - } - - // Environments - if !docker_analysis.environments.is_empty() { - println!(" πŸ”„ Environments ({}):", docker_analysis.environments.len()); - for env in &docker_analysis.environments { - println!(" β€’ {}: {} Dockerfiles, {} Compose files", - env.name, env.dockerfiles.len(), env.compose_files.len()); - } - } -} - -fn display_summary_monorepo_analysis(analysis: &MonorepoAnalysis) { - println!("\nπŸ“Š Analysis Results:"); - println!("β”œβ”€β”€ Root: {}", analysis.root_path.display()); - - if analysis.is_monorepo { - println!("β”œβ”€β”€ Architecture: Monorepo ({} projects)", analysis.projects.len()); - println!("β”œβ”€β”€ Pattern: {:?}", analysis.technology_summary.architecture_pattern); - } else { - println!("β”œβ”€β”€ Architecture: Single Project"); - } - - println!("β”œβ”€β”€ Languages: {}", analysis.technology_summary.languages.join(", ")); - if !analysis.technology_summary.frameworks.is_empty() { - println!("β”œβ”€β”€ Frameworks: {}", analysis.technology_summary.frameworks.join(", ")); - } - if !analysis.technology_summary.databases.is_empty() { - println!("β”œβ”€β”€ Databases: {}", analysis.technology_summary.databases.join(", ")); - } - - println!("└── Projects:"); - - for (i, project) in analysis.projects.iter().enumerate() { - let connector = if i == analysis.projects.len() - 1 { "└──" } else { "β”œβ”€β”€" }; - - println!(" {} {} {} ({})", - connector, - get_category_emoji(&project.project_category), - project.name, - format_project_category(&project.project_category) - ); + }; - if analysis.is_monorepo { - let sub_connector = if i == analysis.projects.len() - 1 { " " } else { "β”‚ " }; - println!(" {} πŸ“‚ Path: {}", sub_connector, project.path.display()); - println!(" {} 🌐 Languages: {}", sub_connector, - project.analysis.languages.iter().map(|l| l.name.clone()).collect::>().join(", ")); - - if project.analysis.ports.len() > 0 { - println!(" {} πŸ”Œ Ports: {}", sub_connector, - project.analysis.ports.iter().map(|p| p.number.to_string()).collect::>().join(", ")); - } - } + display_analysis(&monorepo_analysis, mode); } - println!("\nπŸ“ˆ Analysis metadata:"); - println!("β”œβ”€β”€ Duration: {}ms", analysis.metadata.analysis_duration_ms); - println!("β”œβ”€β”€ Files analyzed: {}", analysis.metadata.files_analyzed); - println!("└── Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0); -} - -fn get_category_emoji(category: &ProjectCategory) -> &'static str { - match category { - ProjectCategory::Frontend => "🌐", - ProjectCategory::Backend => "βš™οΈ", - ProjectCategory::Api => "πŸ”Œ", - ProjectCategory::Service => "πŸš€", - ProjectCategory::Library => "πŸ“š", - ProjectCategory::Tool => "πŸ”§", - ProjectCategory::Documentation => "πŸ“–", - ProjectCategory::Infrastructure => "πŸ—οΈ", - ProjectCategory::Unknown => "❓", - } -} - -fn format_project_category(category: &ProjectCategory) -> &'static str { - match category { - ProjectCategory::Frontend => "Frontend", - ProjectCategory::Backend => "Backend", - ProjectCategory::Api => "API", - ProjectCategory::Service => "Service", - ProjectCategory::Library => "Library", - ProjectCategory::Tool => "Tool", - ProjectCategory::Documentation => "Documentation", - ProjectCategory::Infrastructure => "Infrastructure", - ProjectCategory::Unknown => "Unknown", - } -} - -fn display_architecture_description(pattern: &ArchitecturePattern) { - match pattern { - ArchitecturePattern::Monolithic => { - println!(" πŸ“¦ This is a single, self-contained application"); - } - ArchitecturePattern::Fullstack => { - println!(" 🌐 This is a full-stack application with separate frontend and backend"); - } - ArchitecturePattern::Microservices => { - println!(" πŸ”— This is a microservices architecture with multiple independent services"); - } - ArchitecturePattern::ApiFirst => { - println!(" πŸ”Œ This is an API-first architecture focused on service interfaces"); - } - ArchitecturePattern::EventDriven => { - println!(" πŸ“‘ This is an event-driven architecture with decoupled components"); - } - ArchitecturePattern::Mixed => { - println!(" πŸ”€ This is a mixed architecture combining multiple patterns"); - } - } + Ok(()) } fn handle_generate( @@ -1978,3 +1508,18 @@ async fn handle_tools(command: ToolsCommand) -> syncable_cli::Result<()> { Ok(()) } + +/// Format project category for display +fn format_project_category(category: &ProjectCategory) -> &'static str { + match category { + ProjectCategory::Frontend => "Frontend", + ProjectCategory::Backend => "Backend", + ProjectCategory::Api => "API", + ProjectCategory::Service => "Service", + ProjectCategory::Library => "Library", + ProjectCategory::Tool => "Tool", + ProjectCategory::Documentation => "Documentation", + ProjectCategory::Infrastructure => "Infrastructure", + ProjectCategory::Unknown => "Unknown", + } +} From 84fdffdab7cc8d4ef9983de6a292fb93ac1bf4ce Mon Sep 17 00:00:00 2001 From: Alex Holmberg <113964069+Alex793x@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:32:31 +0200 Subject: [PATCH 2/2] Feature/fix auto update (#32) * chore: release v0.3.0 * Feature/condense overview with new representation (#29) * chore: release v0.3.0 * feat: Optimized Analysis Dashboard Overview with new default matrix option * chore: release v0.4.0 * feat: fixed update version --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 16 ++++++---------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66457d6e..32115ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [0.4.0](https://github.com/syncable-dev/syncable-cli/compare/v0.3.0...v0.4.0) - 2025-06-06 + +### Other + +- Feature/condense overview with new representation ([#29](https://github.com/syncable-dev/syncable-cli/pull/29)) + ## [0.3.0](https://github.com/syncable-dev/syncable-cli/compare/v0.2.1...v0.3.0) - 2025-06-06 ### Added diff --git a/Cargo.lock b/Cargo.lock index a612cfe3..cbdbbdbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3339,7 +3339,7 @@ dependencies = [ [[package]] name = "syncable-cli" -version = "0.3.0" +version = "0.4.0" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index d7eb9f8e..2ed96025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syncable-cli" -version = "0.3.0" +version = "0.4.0" edition = "2024" authors = ["Syncable Team"] description = "A Rust-based CLI that analyzes code repositories and generates Infrastructure as Code configurations" diff --git a/src/main.rs b/src/main.rs index 8c4a1f8a..53c07b11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,7 @@ use clap::Parser; use syncable_cli::{ analyzer::{ self, vulnerability_checker::VulnerabilitySeverity, DetectedTechnology, TechnologyCategory, LibraryType, - analyze_monorepo, analyze_monorepo_with_config, MonorepoAnalysis, ProjectCategory, ArchitecturePattern, - DockerAnalysis, DockerfileInfo, ComposeFileInfo, DockerService, OrchestrationPattern, - NetworkingConfig, DockerEnvironment, - SecurityAnalyzer, SecurityAnalysisConfig, SecuritySeverity, - DependencyAnalysis, VulnerabilitySeverity as VulnSeverity, - vulnerability_checker::VulnerabilityChecker + analyze_monorepo, ProjectCategory, }, cli::{Cli, Commands, ToolsCommand, OutputFormat, SeverityThreshold, DisplayFormat}, config, @@ -127,23 +122,24 @@ fn check_for_update() { } } - // Query crates.io with proper User-Agent header + // Query GitHub releases API instead of crates.io let client = reqwest::blocking::Client::builder() .user_agent(format!("syncable-cli/{} ({})", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_REPOSITORY"))) .build(); if let Ok(client) = client { let resp = client - .get("https://crates.io/api/v1/crates/syncable-cli") + .get("https://api.github.com/repos/syncable-dev/syncable-cli/releases/latest") .send() .and_then(|r| r.json::()); if let Ok(json) = resp { - let latest = json["crate"]["max_version"].as_str().unwrap_or(""); + let latest = json["tag_name"].as_str().unwrap_or("") + .trim_start_matches('v'); // Remove 'v' prefix if present let current = env!("CARGO_PKG_VERSION"); if latest != "" && latest != current { println!( - "\x1b[33mπŸ”” A new version of sync-ctl is available: {latest} (current: {current})\nRun `cargo install syncable-cli --force` to update.\x1b[0m" + "\x1b[33mπŸ”” A new version of sync-ctl is available: {latest} (current: {current})\nRun `cargo install --git https://github.com/syncable-dev/syncable-cli --tag v{latest}` to update.\x1b[0m" ); } }