diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1b621d7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Publish to PyPI + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + build-and-publish: + name: Build & Publish Python Package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: python -m pip install --upgrade pip build twine + + - name: Build distributions + run: python -m build + + - name: Verify distribution archives + run: python -m twine check dist/* + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 0000000..24cb4ce --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,51 @@ +name: Release Standalone Binaries + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-binaries: + name: Build Standalone Binary (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + artifact_name: devsetup-windows-amd64.exe + binary_path: dist/devsetup.exe + - os: ubuntu-latest + artifact_name: devsetup-linux-amd64 + binary_path: dist/devsetup + - os: macos-latest + artifact_name: devsetup-macos-universal + binary_path: dist/devsetup + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies & PyInstaller + run: | + python -m pip install --upgrade pip pyinstaller + python -m pip install . + + - name: Compile standalone binary + run: | + pyinstaller --onefile --name devsetup --clean src/devsetup/cli/main.py + + - name: Upload Binary to Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'release' + with: + files: ${{ matrix.binary_path }} + diff --git a/.gitignore b/.gitignore index e8e862f..5bbddc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Python bytecode __pycache__/ *.py[cod] +*$py.class *.pyd *.so @@ -14,8 +15,12 @@ ENV/ build/ dist/ site/ +_build/ *.egg-info/ *.egg +*.whl +*.tar.gz +*.spec # Test and coverage artifacts .pytest_cache/ @@ -26,12 +31,19 @@ site/ coverage.xml htmlcov/ -# Tooling state +# Tooling and lock files .pre-commit-config.yaml.lock +# Environment variables +.env +.env.* +!.env.example + # OS files .DS_Store Thumbs.db +ehthumbs.db +[Dd]esktop.ini # Editor settings .idea/ @@ -41,8 +53,7 @@ Thumbs.db # Logs *.log -# Temporary files +# Temporary and backup files *.tmp *.swp - -docs/ +*.bak diff --git a/README.md b/README.md index 0b36da1..232a6ed 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,85 @@ -# DevSetup +# DevSetup 🐾 + + +DevSetup is a universal, cross-platform CLI for bootstrapping GitHub projects with one command. [![CI](https://github.com/DEMOCODE675/CloneCode/actions/workflows/ci.yml/badge.svg)](https://github.com/DEMOCODE675/CloneCode/actions/workflows/ci.yml) [![Python](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) **DevSetup** is a cross-platform command-line tool for getting GitHub projects ready to run. Give it a repository URL or a local project directory and it will identify the technology stack, install dependencies, and report project health with clear Rich-powered output. + +```text +# ------------------------------------------------------------------------ +# ........................................................................ +# ......................... /\_____/\ .......................... +# ........................ / /\ /\ \ ......................... +# ...................... ( (o) (o) ) '....................... +# ................... \ / .. ( (*) . (*) ) .. \ /................... +# .................. === ... ( .w. ) ... === ................... +# .................. / \ .... \ / .... / \ ................... +# ........................ / | | \ ......................... +# ........................ | | | | ......................... +# ........................ | ( | | ) | ......................... +# ........................ (____( | | )____) ......................... +# ........................................................................ +# ............[ DevSetup Universal Environment Bootstrapper ]............. +# ------------------------------------------------------------------------ +``` + +--- + +## 🚀 Quick Run (No Clone or Setup Needed!) + +You can run `devsetup` immediately in your terminal without cloning or saving any files: + +### Run ephemerally (like `npx`): +```bash +# Using uv (fastest): +uvx --from git+https://github.com/DEMOCODE675/CloneCode.git@v2 devsetup + +# Or using pipx: +pipx run --spec git+https://github.com/DEMOCODE675/CloneCode.git@v2 devsetup +``` + +--- + +## 📦 One-Line Global Install + +Install `devsetup` permanently to your system with a single command: + +### Windows (PowerShell): +```powershell +irm https://raw.githubusercontent.com/DEMOCODE675/CloneCode/v2/install.ps1 | iex +``` + +### macOS & Linux: +```bash + +curl -fsSL https://raw.githubusercontent.com/DEMOCODE675/CloneCode/v2/install.sh | bash +``` + +### Via `pipx` or `pip`: +```bash +pipx install git+https://github.com/DEMOCODE675/CloneCode.git@v2 +# or +pip install git+https://github.com/DEMOCODE675/CloneCode.git@v2 +``` + +Once installed, just type: +```bash +devsetup +``` + +--- + +## 💻 Usage + ```bash +devsetup setup https://github.com/user/project + devsetup https://github.com/owner/project + ``` ## Why DevSetup? @@ -27,6 +100,22 @@ Starting an unfamiliar repository usually means figuring out its language, frame ## Supported ecosystems + +### Clone only +```bash +devsetup clone https://github.com/user/project +``` + +### Scan a project +```bash +devsetup scan . +``` + +### Run diagnostics +```bash +devsetup doctor . +``` + DevSetup currently includes plugins for: **Angular · Astro · Django · Express · FastAPI · Flutter · Go · Java · Laravel · NestJS · Next.js · Node.js · PHP · Python · Rails · React · Remix · Ruby · Rust · Spring · Svelte · Vue** @@ -37,7 +126,35 @@ Detection is based on project metadata such as manifests, lock files, source lay DevSetup is currently installed from a checkout: + +### Show mascot cat banner +```bash + +devsetup cat +# Or modern card style: +devsetup banner --style card +``` + +### List installed plugins +```bash +devsetup plugins +``` + +### Clear cache +```bash +devsetup cache clear +``` + +### Clean generated files +```bash +devsetup clean +``` + +### Show version ```bash +devsetup version +``` + git clone https://github.com/DEMOCODE675/CloneCode.git cd CloneCode python -m pip install -e . @@ -69,7 +186,35 @@ devsetup setup https://github.com/owner/project --destination ~/src/project Passing a repository URL directly performs the same bootstrap flow: + +--- + +## ✨ Highlights + +- Python 3.12+ +- Typer CLI with Rich output & cute mascot cat terminal splash screen +- Universal one-line installer for Windows, macOS, and Linux +- Plugin-driven architecture +- Automatic package manager and framework detection + +--- + +## 🛠️ Development + +To develop locally: + ```bash +git clone https://github.com/DEMOCODE675/CloneCode.git +cd CloneCode +pip install -e .[dev] +``` + +Run checks: +```bash + +ruff check src tests +pytest + devsetup https://github.com/owner/project ``` @@ -126,3 +271,4 @@ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for local setu ## License No license file is currently included in this repository. Add a license before distributing DevSetup or accepting external contributions under explicit reuse terms. + diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..c770b5a --- /dev/null +++ b/install.ps1 @@ -0,0 +1,78 @@ +# DevSetup Windows One-Line Installer +# Usage: irm https://raw.githubusercontent.com/DEMOCODE675/CloneCode/v2/install.ps1 | iex + +$ErrorActionPreference = "Stop" + +Write-Host "🐾 Installing DevSetup CLI..." -ForegroundColor Cyan + +$RepoUrl = "git+https://github.com/DEMOCODE675/CloneCode.git@v2" +$InstallBase = "$HOME\.devsetup" +$BinDir = "$InstallBase\bin" + +# 1. Try pipx if available +if (Get-Command pipx -ErrorAction SilentlyContinue) { + Write-Host "Found pipx, installing globally in isolated environment..." -ForegroundColor Green + pipx install --force $RepoUrl + Write-Host "DevSetup installed successfully via pipx!" -ForegroundColor Green + devsetup cat + exit 0 +} + +# 2. Try uv if available +if (Get-Command uv -ErrorAction SilentlyContinue) { + Write-Host "Found uv, installing tool..." -ForegroundColor Green + uv tool install --force $RepoUrl + Write-Host "DevSetup installed successfully via uv!" -ForegroundColor Green + devsetup cat + exit 0 +} + +# 3. Use standard Python +$pythonCmd = $null +if (Get-Command python -ErrorAction SilentlyContinue) { + $pythonCmd = "python" +} elseif (Get-Command py -ErrorAction SilentlyContinue) { + $pythonCmd = "py" +} + +if (-not $pythonCmd) { + Write-Host "Error: Python 3.12+ was not found on your system." -ForegroundColor Red + Write-Host "Please install Python from https://www.python.org/downloads/ or via Microsoft Store." -ForegroundColor Yellow + exit 1 +} + +Write-Host "Setting up isolated DevSetup environment in $InstallBase..." -ForegroundColor Cyan +if (-not (Test-Path $InstallBase)) { + New-Item -ItemType Directory -Path $InstallBase -Force | Out-Null +} + +$VenvDir = "$InstallBase\venv" +& $pythonCmd -m venv $VenvDir + +$VenvPython = "$VenvDir\Scripts\python.exe" +$VenvDevSetup = "$VenvDir\Scripts\devsetup.exe" + +Write-Host "Installing latest DevSetup CLI from repository..." -ForegroundColor Cyan +& $VenvPython -m pip install --quiet --upgrade pip +& $VenvPython -m pip install --quiet --upgrade $RepoUrl + +if (-not (Test-Path $BinDir)) { + New-Item -ItemType Directory -Path $BinDir -Force | Out-Null +} + +# Create wrapper script in bin dir +$ShimCmd = "$BinDir\devsetup.cmd" +"@echo off`r`n`"$VenvDevSetup`" %*" | Set-Content -Path $ShimCmd -Encoding Ascii + +# Ensure BinDir is in User PATH +$UserPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) +if ($UserPath -notlike "*$BinDir*") { + Write-Host "Adding $BinDir to User PATH..." -ForegroundColor Cyan + $NewPath = "$BinDir;$UserPath" + [Environment]::SetEnvironmentVariable("Path", $NewPath, [EnvironmentVariableTarget]::User) + $env:Path = "$BinDir;$env:Path" +} + +Write-Host "`n DevSetup is installed and ready to use!`n" -ForegroundColor Green +& $VenvDevSetup cat + diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..08e1750 --- /dev/null +++ b/install.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# DevSetup macOS & Linux One-Line Installer +# Usage: curl -fsSL https://raw.githubusercontent.com/DEMOCODE675/CloneCode/v2/install.sh | bash + +set -e + +REPO_URL="git+https://github.com/DEMOCODE675/CloneCode.git@v2" +INSTALL_BASE="${HOME}/.devsetup" +BIN_DIR="${HOME}/.local/bin" + +echo -e "\033[36m🐾 Installing DevSetup CLI...\033[0m" + +# 1. Try pipx if available +if command -v pipx >/dev/null 2>&1; then + echo -e "\033[32mFound pipx, installing globally in isolated environment...\033[0m" + pipx install --force "${REPO_URL}" + echo -e "\033[32mDevSetup installed successfully via pipx!\033[0m" + devsetup cat || true + exit 0 +fi + +# 2. Try uv if available +if command -v uv >/dev/null 2>&1; then + echo -e "\033[32mFound uv, installing tool...\033[0m" + uv tool install --force "${REPO_URL}" + echo -e "\033[32mDevSetup installed successfully via uv!\033[0m" + devsetup cat || true + exit 0 +fi + +# 3. Check for python3 +PYTHON_CMD="" +if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD="python3" +elif command -v python >/dev/null 2>&1; then + PYTHON_CMD="python" +fi + +if [ -z "${PYTHON_CMD}" ]; then + echo -e "\033[31mError: Python 3.12+ was not found on your system.\033[0m" + echo "Please install Python 3 from https://www.python.org/ or via your system package manager." + exit 1 +fi + +echo -e "\033[36mSetting up isolated DevSetup environment in ${INSTALL_BASE}...\033[0m" +mkdir -p "${INSTALL_BASE}" +VENV_DIR="${INSTALL_BASE}/venv" +${PYTHON_CMD} -m venv "${VENV_DIR}" + +VENV_PYTHON="${VENV_DIR}/bin/python" +VENV_DEVSETUP="${VENV_DIR}/bin/devsetup" + +echo -e "\033[36mInstalling latest DevSetup CLI from repository...\033[0m" +"${VENV_PYTHON}" -m pip install --quiet --upgrade pip +"${VENV_PYTHON}" -m pip install --quiet --upgrade "${REPO_URL}" + +mkdir -p "${BIN_DIR}" +ln -sf "${VENV_DEVSETUP}" "${BIN_DIR}/devsetup" + +# Check if ~/.local/bin is in PATH +if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then + echo -e "\033[33mNote: ${BIN_DIR} is not in your current PATH.\033[0m" + echo "Add the following line to your ~/.bashrc or ~/.zshrc:" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" +fi + +echo -e "\n\033[32m✨ DevSetup is installed and ready to use!\033[0m\n" +"${VENV_DEVSETUP}" cat || true + diff --git a/src/devsetup/cli/commands/banner.py b/src/devsetup/cli/commands/banner.py new file mode 100644 index 0000000..4a00c01 --- /dev/null +++ b/src/devsetup/cli/commands/banner.py @@ -0,0 +1,41 @@ +"""Banner and cat commands.""" + +from __future__ import annotations + +from typing import Literal + +import typer + +from devsetup.cli.ui.console import console, render_banner + +BannerStyle = Literal["matrix", "card"] + + +def register(app: typer.Typer) -> None: + """Register banner and cat commands on the supplied Typer app.""" + + @app.command() + def banner( + style: str = typer.Option( + "matrix", + "--style", + "-s", + help="Banner style: 'matrix' (reference dotted art) or 'card' (modern CLI card).", + ), + ) -> None: + """Display the DevSetup cat banner.""" + chosen_style: BannerStyle = "card" if style.lower() == "card" else "matrix" + console.print(render_banner(style=chosen_style)) + + @app.command() + def cat( + style: str = typer.Option( + "matrix", + "--style", + "-s", + help="Banner style: 'matrix' (reference dotted art) or 'card' (modern CLI card).", + ), + ) -> None: + """Show the mascot cat.""" + chosen_style: BannerStyle = "card" if style.lower() == "card" else "matrix" + console.print(render_banner(style=chosen_style)) diff --git a/src/devsetup/cli/main.py b/src/devsetup/cli/main.py index 613a6ce..4e7fd23 100644 --- a/src/devsetup/cli/main.py +++ b/src/devsetup/cli/main.py @@ -4,6 +4,7 @@ import typer +from devsetup.cli.commands.banner import register as register_banner from devsetup.cli.commands.cache import register as register_cache from devsetup.cli.commands.clean import register as register_clean from devsetup.cli.commands.clone import register as register_clone @@ -15,7 +16,11 @@ from devsetup.cli.ui.console import console, render_banner from devsetup.core.orchestrator import build_services, repository_destination -app = typer.Typer(add_completion=False, help="Universal development environment bootstrapper.", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) +app = typer.Typer( + add_completion=False, + help="Universal development environment bootstrapper.", + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) @app.callback(invoke_without_command=True) @@ -30,7 +35,12 @@ def main(ctx: typer.Context) -> None: repository_url = ctx.args[0] services = build_services() target = repository_destination(repository_url, services.config) - services.cloner.clone(repository_url, target, branch=services.config.clone.branch, depth=services.config.clone.depth) + services.cloner.clone( + repository_url, + target, + branch=services.config.clone.branch, + depth=services.config.clone.depth, + ) snapshot = services.scanner.scan(target) detected = services.detector.detect_snapshot(snapshot) results = services.installer.install(detected) @@ -51,7 +61,8 @@ def main(ctx: typer.Context) -> None: register_clean(app) register_cache(app) register_version(app) +register_banner(app) if __name__ == "__main__": - app() \ No newline at end of file + app() diff --git a/src/devsetup/cli/ui/cat_art.py b/src/devsetup/cli/ui/cat_art.py new file mode 100644 index 0000000..1ea2c98 --- /dev/null +++ b/src/devsetup/cli/ui/cat_art.py @@ -0,0 +1,178 @@ +"""ASCII and ANSI styled cat banners for DevSetup CLI.""" + +from __future__ import annotations + +from typing import Literal + +from rich.console import RenderableType +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from devsetup.version import __version__ + +TOTAL_BANNER_WIDTH = 72 + + +def _make_matrix_row( + row_segments: list[tuple[str, str]], width: int = TOTAL_BANNER_WIDTH +) -> Text: + """Center a list of styled text segments within a dotted background line.""" + total_len = sum(len(txt) for txt, _ in row_segments) + rem = max(0, width - total_len) + left_dots = rem // 2 + right_dots = rem - left_dots + + row_text = Text() + row_text.append("# " + "." * left_dots, style="dim cyan") + for txt, style in row_segments: + row_text.append(txt, style=style) + row_text.append("." * right_dots + "\n", style="dim cyan") + return row_text + + +def build_matrix_cat_banner( + width: int = TOTAL_BANNER_WIDTH, + title: str = "DevSetup", + subtitle: str = "Universal Environment Bootstrapper", +) -> Text: + """Build the Image 2 styled dotted matrix terminal banner featuring the cartoon cat.""" + banner = Text() + dash_line = "# " + "-" * width + "\n" + dots_line = "# " + "." * width + "\n" + + banner.append(dash_line, style="dim cyan") + banner.append(dots_line, style="dim cyan") + + # Geometry for the cute cartoon cat (matching Image 1) + cat_rows: list[list[tuple[str, str]]] = [ + [(" /\\_____/\\ ", "bold bright_white")], + [ + (" / ", "bold bright_white"), + ("/\\", "bold bright_magenta"), + (" ", "bold bright_white"), + ("/\\", "bold bright_magenta"), + (" \\ ", "bold bright_white"), + ], + [ + (" ( ", "bold bright_white"), + ("(o) (o)", "bold bright_cyan"), + (" ) ", "bold bright_white"), + (" '", "dim white"), + ], + [ + (" \\ / ", "bold white"), + (".. ", "dim cyan"), + ("( ", "bold bright_white"), + ("(*)", "bold bright_magenta"), + (" . ", "bold white"), + ("(*)", "bold bright_magenta"), + (" ) ", "bold bright_white"), + ("..", "dim cyan"), + (" \\ /", "bold white"), + ], + [ + (" === ", "bold white"), + ("... ", "dim cyan"), + ("( ", "bold bright_white"), + (".w.", "bold yellow"), + (" ) ", "bold bright_white"), + ("...", "dim cyan"), + (" === ", "bold white"), + ], + [ + (" / \\ ", "bold white"), + (".... ", "dim cyan"), + ("\\ / ", "bold bright_white"), + ("....", "dim cyan"), + (" / \\ ", "bold white"), + ], + [(" / | | \\ ", "bold bright_white")], + [(" | | | | ", "bold bright_white")], + [(" | ( | | ) | ", "bold bright_white")], + [(" (____( | | )____) ", "bold bright_white")], + ] + + for row in cat_rows: + banner.append_text(_make_matrix_row(row, width=width)) + + banner.append(dots_line, style="dim cyan") + + # Footer segment + footer_plain = f"[ {title} {subtitle} ]" + rem_footer = max(0, width - len(footer_plain)) + left_f = rem_footer // 2 + right_f = rem_footer - left_f + + banner.append("# " + "." * left_f, style="dim cyan") + banner.append("[", style="dim white") + banner.append(f" {title} ", style="bold bright_cyan") + banner.append(f"{subtitle} ", style="white") + banner.append("]", style="dim white") + banner.append("." * right_f + "\n", style="dim cyan") + + banner.append(dash_line, style="dim cyan") + return banner + + +def build_card_cat_banner( + title: str = "DevSetup", + version: str = __version__, +) -> Panel: + """Build a modern Claude CLI style two-column card banner.""" + cat = Text() + cat.append(" /\\_____/\\ \n", style="bold bright_white") + cat.append(" / ", style="bold bright_white") + cat.append("/\\", style="bold bright_magenta") + cat.append(" ", style="bold bright_white") + cat.append("/\\", style="bold bright_magenta") + cat.append(" \\ \n", style="bold bright_white") + cat.append(" ( ", style="bold bright_white") + cat.append("(o) (o)", style="bold bright_cyan") + cat.append(" ) '\n", style="bold bright_white") + cat.append(" \\ / ( ", style="bold white") + cat.append("(*)", style="bold bright_magenta") + cat.append(" . ", style="bold white") + cat.append("(*)", style="bold bright_magenta") + cat.append(" ) \\ /\n", style="bold white") + cat.append(" === ( ", style="bold white") + cat.append(".w.", style="bold yellow") + cat.append(" ) ===\n", style="bold white") + cat.append(" / \\ \\ / / \\\n", style="bold white") + cat.append(" / | | \\ \n", style="bold bright_white") + cat.append(" | | | | \n", style="bold bright_white") + cat.append(" (____( | | )____) \n", style="bold bright_white") + + info = Text() + info.append(f"{title} ", style="bold bright_cyan") + info.append(f"v{version}\n", style="dim white") + info.append("Universal development environment bootstrapper\n\n", style="italic white") + info.append("Quick Start:\n", style="bold yellow") + info.append(" devsetup ", style="bright_white") + info.append("Clone and bootstrap repository\n", style="dim white") + info.append(" devsetup doctor ", style="bright_white") + info.append("Check system prerequisites\n", style="dim white") + info.append(" devsetup scan ", style="bright_white") + info.append("Detect frameworks and tools\n", style="dim white") + info.append(" devsetup banner ", style="bright_white") + info.append("Display this cat banner\n", style="dim white") + info.append(" devsetup --help ", style="bright_white") + info.append("Show command documentation\n", style="dim white") + + grid = Table.grid(padding=(0, 3)) + grid.add_column(justify="center") + grid.add_column(justify="left") + grid.add_row(cat, info) + + return Panel(grid, title=f"[bold cyan]{title}[/bold cyan]", border_style="cyan", expand=False) + + +def render_cat_banner( + style: Literal["matrix", "card"] = "matrix", + width: int = TOTAL_BANNER_WIDTH, +) -> RenderableType: + """Render the requested cat banner style.""" + if style == "card": + return build_card_cat_banner() + return build_matrix_cat_banner(width=width) + diff --git a/src/devsetup/cli/ui/console.py b/src/devsetup/cli/ui/console.py index 02abd7f..78b5541 100644 --- a/src/devsetup/cli/ui/console.py +++ b/src/devsetup/cli/ui/console.py @@ -2,19 +2,21 @@ from __future__ import annotations -from rich.console import Console -from rich.panel import Panel +from typing import Literal + +from rich.console import Console, RenderableType from rich.table import Table +from devsetup.cli.ui.cat_art import render_cat_banner from devsetup.core.models import HealthCheckResult console = Console() -def render_banner() -> Panel: - """Return the DevSetup banner panel.""" +def render_banner(style: Literal["matrix", "card"] = "matrix") -> RenderableType: + """Return the DevSetup cat banner.""" - return Panel.fit("[bold cyan]DevSetup[/bold cyan]\nUniversal development environment bootstrapper", border_style="cyan") + return render_cat_banner(style=style) def render_health_table(results: tuple[HealthCheckResult, ...]) -> Table: @@ -27,4 +29,4 @@ def render_health_table(results: tuple[HealthCheckResult, ...]) -> Table: for result in results: status = "[green]PASS[/green]" if result.success else "[red]FAIL[/red]" table.add_row(result.name, status, result.message) - return table \ No newline at end of file + return table diff --git a/tests/test_banner.py b/tests/test_banner.py new file mode 100644 index 0000000..be0a03f --- /dev/null +++ b/tests/test_banner.py @@ -0,0 +1,80 @@ +"""Tests for cat banners and banner commands.""" + +from __future__ import annotations + +from typer.testing import CliRunner + +from devsetup.cli.main import app +from devsetup.cli.ui.cat_art import ( + TOTAL_BANNER_WIDTH, + build_card_cat_banner, + build_matrix_cat_banner, + render_cat_banner, +) +from devsetup.cli.ui.console import render_banner + +runner = CliRunner() + + +def test_matrix_cat_banner_dimensions() -> None: + """Verify that every line of the matrix banner matches the exact required width.""" + banner = build_matrix_cat_banner(width=TOTAL_BANNER_WIDTH) + plain_text = banner.plain + lines = [line for line in plain_text.splitlines() if line] + + assert len(lines) > 5 + for line in lines: + # Every line starts with '# ' followed by exactly TOTAL_BANNER_WIDTH characters + assert line.startswith("# ") + assert len(line) == TOTAL_BANNER_WIDTH + 2 # '# ' + 72 characters + + +def test_card_cat_banner_contains_cat_and_title() -> None: + """Verify the card banner builds properly as a Panel.""" + panel = build_card_cat_banner() + assert panel.title == "[bold cyan]DevSetup[/bold cyan]" + + +def test_render_cat_banner_options() -> None: + """Verify render_cat_banner supports both styles.""" + matrix_art = render_cat_banner("matrix") + card_art = render_cat_banner("card") + assert "/\\_____/\\" in str(matrix_art) + assert card_art is not None + + +def test_render_banner_defaults_to_matrix() -> None: + """Verify render_banner() returns matrix banner by default.""" + banner = render_banner() + assert "/\\_____/\\" in str(banner) + assert "DevSetup" in str(banner) + + +def test_cli_bare_shows_cat_banner() -> None: + """Running devsetup with no arguments should output the cat banner and exit 0.""" + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "/\\_____/\\" in result.stdout + assert "DevSetup" in result.stdout + + +def test_cli_banner_command() -> None: + """Running 'devsetup banner' should output the cat banner.""" + result = runner.invoke(app, ["banner"]) + assert result.exit_code == 0 + assert "/\\_____/\\" in result.stdout + + +def test_cli_cat_command() -> None: + """Running 'devsetup cat' should output the mascot cat.""" + result = runner.invoke(app, ["cat"]) + assert result.exit_code == 0 + assert "/\\_____/\\" in result.stdout + + +def test_cli_banner_card_style() -> None: + """Running 'devsetup banner --style card' should render the card layout.""" + result = runner.invoke(app, ["banner", "--style", "card"]) + assert result.exit_code == 0 + assert "DevSetup" in result.stdout + assert "Quick Start:" in result.stdout