From 8897738c6039f9f396f1de605775272ec7147827 Mon Sep 17 00:00:00 2001 From: DEMOCODE675 Date: Sun, 2 Aug 2026 23:40:39 +0530 Subject: [PATCH 1/3] readme.md changes --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ac538c..5ba4a41 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,59 @@ DevSetup is a cross-platform CLI for bootstrapping GitHub projects with one command. + +## Usage + + ```bash -devsetup https://github.com/user/project +devsetup setup https://github.com/user/project ``` It scans the repository, detects the language, framework, and package manager, then runs the install and health-check flow with Rich progress output. + +### Clone only + +```bash +devsetup clone https://github.com/user/project +``` + +### Scan a project + +```bash +devsetup scan . +``` + +### Run diagnostics + +```bash +devsetup doctor . +``` + +### List installed plugins + +```bash +devsetup plugins +``` + +### Clear cache + +```bash +devsetup cache clear +``` + +### Clean generated files + +```bash +devsetup clean +``` + +### Show version + +```bash +devsetup version +``` + ## Highlights - Python 3.12+ @@ -24,7 +71,7 @@ pip install -e .[dev] ## Commands -- `devsetup ` or `devsetup setup ` +- `devsetup setup ` - `devsetup clone ` - `devsetup scan [path]` - `devsetup doctor [path]` From 8c4d457bd970e82368e312a49575ed517f730ba1 Mon Sep 17 00:00:00 2001 From: DEMOCODE675 Date: Mon, 2 Nov 2026 20:04:40 +0530 Subject: [PATCH 2/3] feat: add mascot cat terminal banner, one-line installers, and distribution workflows --- .github/workflows/publish.yml | 38 ++++++ .github/workflows/release-binaries.yml | 51 +++++++ .gitignore | 19 ++- README.md | 119 ++++++++++++----- install.ps1 | 78 +++++++++++ install.sh | 69 ++++++++++ src/devsetup/cli/commands/banner.py | 38 ++++++ src/devsetup/cli/main.py | 17 ++- src/devsetup/cli/ui/cat_art.py | 178 +++++++++++++++++++++++++ src/devsetup/cli/ui/console.py | 14 +- tests/test_banner.py | 80 +++++++++++ 11 files changed, 654 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release-binaries.yml create mode 100644 install.ps1 create mode 100644 install.sh create mode 100644 src/devsetup/cli/commands/banner.py create mode 100644 src/devsetup/cli/ui/cat_art.py create mode 100644 tests/test_banner.py 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 5ba4a41..50af9fb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,71 @@ -# DevSetup +# DevSetup 🐾 + +DevSetup is a universal, cross-platform CLI for bootstrapping GitHub projects with one command. + +```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 -DevSetup is a cross-platform CLI for bootstrapping GitHub projects with one command. +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 +--- +## 💻 Usage ```bash devsetup setup https://github.com/user/project @@ -12,82 +73,72 @@ devsetup setup https://github.com/user/project It scans the repository, detects the language, framework, and package manager, then runs the install and health-check flow with Rich progress output. - ### Clone only - ```bash devsetup clone https://github.com/user/project ``` ### Scan a project - ```bash devsetup scan . ``` ### Run diagnostics - ```bash devsetup doctor . ``` -### List installed plugins +### 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 ``` -## Highlights +--- + +## ✨ Highlights - Python 3.12+ -- Typer CLI with Rich output +- Typer CLI with Rich output & cute mascot cat terminal splash screen +- Universal one-line installer for Windows, macOS, and Linux - Plugin-driven architecture -- Detects package managers from manifests and lock files -- Ready for external plugins through entry points +- Automatic package manager and framework detection + +--- -## Install +## 🛠️ Development + +To develop locally: ```bash +git clone https://github.com/DEMOCODE675/CloneCode.git +cd CloneCode pip install -e .[dev] ``` -## Commands - -- `devsetup setup ` -- `devsetup clone ` -- `devsetup scan [path]` -- `devsetup doctor [path]` -- `devsetup plugins` -- `devsetup clean` -- `devsetup cache clear` -- `devsetup version` - -## Development - -Run the checks locally: - +Run checks: ```bash ruff check src tests -mypy src/devsetup pytest ``` - -See [docs/Architecture.md](docs/Architecture.md) and [docs/CreatingPlugin.md](docs/CreatingPlugin.md) for the extension model. \ No newline at end of file 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..3a4dde0 --- /dev/null +++ b/src/devsetup/cli/commands/banner.py @@ -0,0 +1,38 @@ +"""Banner and cat commands.""" + +from __future__ import annotations + +import typer + +from devsetup.cli.ui.console import console, render_banner + + +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 = "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 = "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 From 96eddac0a1545c77121bb432215a3a5df9d5206c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:40:05 +0000 Subject: [PATCH 3/3] Fix banner style typing for mypy in CLI commands Co-authored-by: DEMOCODE675 <194059281+DEMOCODE675@users.noreply.github.com> --- src/devsetup/cli/commands/banner.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/devsetup/cli/commands/banner.py b/src/devsetup/cli/commands/banner.py index 3a4dde0..4a00c01 100644 --- a/src/devsetup/cli/commands/banner.py +++ b/src/devsetup/cli/commands/banner.py @@ -2,10 +2,14 @@ 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.""" @@ -20,7 +24,7 @@ def banner( ), ) -> None: """Display the DevSetup cat banner.""" - chosen_style = "card" if style.lower() == "card" else "matrix" + chosen_style: BannerStyle = "card" if style.lower() == "card" else "matrix" console.print(render_banner(style=chosen_style)) @app.command() @@ -33,6 +37,5 @@ def cat( ), ) -> None: """Show the mascot cat.""" - chosen_style = "card" if style.lower() == "card" else "matrix" + chosen_style: BannerStyle = "card" if style.lower() == "card" else "matrix" console.print(render_banner(style=chosen_style)) -