diff --git a/COMMANDS.md b/COMMANDS.md new file mode 100644 index 0000000..827787a --- /dev/null +++ b/COMMANDS.md @@ -0,0 +1,99 @@ +# devctl Commands Reference + +This document provides a comprehensive list of all commands available in the `devctl` CLI. + +## Overview + +`devctl` is organized into logical command groups. You can always run `devctl --help` or `devctl [command] --help` to get immediate assistance. + +--- + +## 1. Project Initialization (`devctl init`) + +Initialize a new project with a standard boilerplate and optimized configuration. + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Spring Boot** | `devctl init spring [name]` | Downloads a Spring Boot project from start.spring.io. | +| **Angular** | `devctl init angular [name]` | Scaffolds a new Angular project. | +| **Vue.js** | `devctl init vue [name]` | Scaffolds a new Vue 3 project via Vite. | +| **NestJS** | `devctl init nest [name]` | Scaffolds a new NestJS backend. | +| **NodeJS** | `devctl init nodejs [name]` | Scaffolds a clean Express + TypeScript project. | +| **ReactJS** | `devctl init react [name]` | Scaffolds a new React project via Vite. | +| **NextJS** | `devctl init nextjs [name]` | Scaffolds a new NextJS project with App Router. | +| **FastAPI** | `devctl init fastapi [name]` | Scaffolds a new FastAPI (Python) project. | +| **Django** | `devctl init django [name]` | Scaffolds a new Django REST Framework project. | +| **Svelte** | `devctl init svelte [name]` | Scaffolds a new SvelteKit project. | +| **Go** | `devctl init go [name]` | Scaffolds a new Go (Fiber) project. | + +### Options +* `--db [postgres|mysql|mongodb]`: Specify the database driver for Spring Boot projects (Default: `postgres`). +* `--port [number]`: Specify a custom local port for the project. + +--- + +## 2. Resource Scaffolding (`devctl add`) + +Inject business resources (Entities, Controllers, Services) into your existing project layers. + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Resource** | `devctl add resource [Name]` | Generates a full-stack feature for the given name. | + +### Parameters +* `--fields, -f`: Define fields in `name:type` format. + * Example: `devctl add resource Product -f "name:string,price:double"` + * Supported types: `string`, `int`, `double`, `float`, `boolean`, `date`. + +--- + +## 3. Local Orchestration (`devctl run`) + +Launch your entire development environment in a single terminal. + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Run** | `devctl run` | Scans for databases, backends, and frontends and runs them. | + +### Execution Logic +1. **Databases**: Starts Docker Compose services first and waits for initialization. +2. **Backends**: Launches Spring, Nest, Express, Python, or Go APIs in parallel. +3. **Frontends**: Launches Angular, Vue, React, Svelte, or NextJS dev servers. +4. **Logging**: All output is prefixed by service name and color-coded. + +--- + +## 4. Containerization (`devctl dockerize`) + +Generate optimized Dockerfiles for all detected projects. + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Dockerize** | `devctl dockerize [path]` | Scaffolds Multi-stage Dockerfiles for all services. | + +### Options +* `--force`: Overwrite existing Dockerfiles. +* `--dry-run`: Preview actions without writing files. + +--- + +## 5. Deployment Preparation (`devctl deploy`) + +Prepare for multi-service production or staging deployments. + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Deploy** | `devctl deploy [path]` | Generates a global `docker-compose.yml` for all services. | + +### Features +* **Automatic Linking**: Detects database configurations from backends and automatically links them to database services in the global compose file. +* **Context Aware**: Uses relative paths for builds to ensure the compose file is portable. + +--- + +## 6. Utilities + +| Command | Usage | Description | +| :--- | :--- | :--- | +| **Ping** | `devctl ping` | Returns "pong" to verify the CLI is operational. | +| **Help** | `devctl --help` | Displays the global or command-specific help menu. | diff --git a/devctl/commands/add.py b/devctl/commands/add.py index 1d149c2..dee58b0 100644 --- a/devctl/commands/add.py +++ b/devctl/commands/add.py @@ -1,6 +1,7 @@ """ CLI command group for adding new resources to existing projects. -Supports Spring Boot, Angular, Vue.js, NestJS, NodeJS, React, NextJS, FastAPI, Django, Svelte, and Go scaffolding. +Supports Spring Boot, Angular, Vue.js, NestJS, NodeJS, React, NextJS, FastAPI, +Django, Svelte, and Go scaffolding. """ import os @@ -8,16 +9,16 @@ import typer from devctl.generators.scaffold_angular import generate_angular_resource -from devctl.generators.scaffold_spring import generate_spring_resource -from devctl.generators.scaffold_vue import generate_vue_resource +from devctl.generators.scaffold_django import generate_django_resource +from devctl.generators.scaffold_fastapi import generate_fastapi_resource +from devctl.generators.scaffold_go import generate_go_resource from devctl.generators.scaffold_nestjs import generate_nest_resource +from devctl.generators.scaffold_nextjs import generate_nextjs_resource from devctl.generators.scaffold_nodejs import generate_nodejs_resource from devctl.generators.scaffold_react import generate_react_resource -from devctl.generators.scaffold_nextjs import generate_nextjs_resource -from devctl.generators.scaffold_fastapi import generate_fastapi_resource -from devctl.generators.scaffold_django import generate_django_resource +from devctl.generators.scaffold_spring import generate_spring_resource from devctl.generators.scaffold_svelte import generate_svelte_resource -from devctl.generators.scaffold_go import generate_go_resource +from devctl.generators.scaffold_vue import generate_vue_resource from devctl.orchestrator.scanner import detect_environment from devctl.utils.dependencies import check_tool @@ -98,7 +99,9 @@ def resource( # Check for NextJS project if env_state.get("has_nextjs"): project_detected = True - typer.secho("NextJS project detected. Launching NextJS generator...", fg=typer.colors.YELLOW) + typer.secho( + "NextJS project detected. Launching NextJS generator...", fg=typer.colors.YELLOW + ) try: generate_nextjs_resource(name, fields, root_path=".") except Exception as e: @@ -107,7 +110,9 @@ def resource( # Check for FastAPI project if env_state.get("has_fastapi"): project_detected = True - typer.secho("FastAPI project detected. Launching FastAPI generator...", fg=typer.colors.CYAN) + typer.secho( + "FastAPI project detected. Launching FastAPI generator...", fg=typer.colors.CYAN + ) try: generate_fastapi_resource(name, fields, root_path=".") except Exception as e: @@ -154,7 +159,8 @@ def resource( if not project_detected: typer.secho( "Error: Unable to determine project type. " - "Please run from within a supported project directory (Spring, Angular, React, NextJS, FastAPI, Django, Svelte, Go, or Vue.js).", + "Please run from within a supported project directory " + "(Spring, Angular, React, NextJS, FastAPI, Django, Svelte, Go, or Vue.js).", fg=typer.colors.RED, ) raise typer.Exit(code=1) diff --git a/devctl/commands/deploy.py b/devctl/commands/deploy.py index a32bc7f..3e135a6 100644 --- a/devctl/commands/deploy.py +++ b/devctl/commands/deploy.py @@ -32,8 +32,10 @@ def extract_db_info(project_path: Path) -> Optional[Dict[str, Any]]: # spring.datasource.url=jdbc:postgresql://localhost:5432/sample_api_db url_match = re.search(r"spring\.datasource\.url=jdbc:([^:]+)://[^:]+:(\d+)/([\w-]+)", content) # spring.data.mongodb.uri=mongodb://admin:password@localhost:27017/db_name - mongo_match = re.search(r"spring\.data\.mongodb\.uri=mongodb://([^:]+):([^@]+)@[^:]+:(\d+)/([\w-]+)", content) - + mongo_match = re.search( + r"spring\.data\.mongodb\.uri=mongodb://([^:]+):([^@]+)@[^:]+:(\d+)/([\w-]+)", content + ) + user_match = re.search(r"spring\.datasource\.username=([\w-]+)", content) pass_match = re.search(r"spring\.datasource\.password=([\w-]+)", content) @@ -55,7 +57,7 @@ def extract_db_info(project_path: Path) -> Optional[Dict[str, Any]]: db_pass = pass_match.group(1) if pass_match else "password" db_dict = _build_db_dict(db_type, db_port, db_name, db_user, db_pass) - ... + # Try to refine service name from existing docker-compose if possible compose_path = project_path / "docker-compose.yml" if compose_path.exists(): @@ -75,7 +77,8 @@ def extract_db_info(project_path: Path) -> Optional[Dict[str, Any]]: if db_type == "mongodb" and "mongo" in image: db_dict["service_name"] = s_name break - except Exception: + except (OSError, yaml.YAMLError): + # Ignore parsing errors; fall back to default service name pass return db_dict @@ -128,7 +131,9 @@ def extract_db_from_compose(compose_path: Path) -> Optional[Dict[str, Any]]: db_name = env_dict.get("POSTGRES_DB", "db") elif db_type == "mysql": user = env_dict.get("MYSQL_USER", env_dict.get("MYSQL_ROOT_PASSWORD", "admin")) - password = env_dict.get("MYSQL_PASSWORD", env_dict.get("MYSQL_ROOT_PASSWORD", "password")) + password = env_dict.get( + "MYSQL_PASSWORD", env_dict.get("MYSQL_ROOT_PASSWORD", "password") + ) db_name = env_dict.get("MYSQL_DATABASE", "db") else: user = env_dict.get("MONGO_INITDB_ROOT_USERNAME", "admin") @@ -145,7 +150,12 @@ def extract_db_from_compose(compose_path: Path) -> Optional[Dict[str, Any]]: db_dict = _build_db_dict( db_type, - host_port or ("5432" if db_type == "postgresql" else ("3306" if db_type == "mysql" else "27017")), + host_port + or ( + "5432" + if db_type == "postgresql" + else ("3306" if db_type == "mysql" else "27017") + ), db_name, user, password, @@ -159,7 +169,6 @@ def extract_db_from_compose(compose_path: Path) -> Optional[Dict[str, Any]]: def _build_db_dict(db_type: str, port: str, name: str, user: str, password: str) -> Dict[str, Any]: is_postgres = db_type == "postgresql" is_mysql = db_type == "mysql" - is_mongo = db_type == "mongodb" if is_postgres: internal_port = "5432" @@ -210,7 +219,6 @@ def _build_db_dict(db_type: str, port: str, name: str, user: str, password: str) } - @app.command() def deploy(path: Path = PATH_ARGUMENT): """ @@ -225,7 +233,9 @@ def deploy(path: Path = PATH_ARGUMENT): raise typer.Exit(1) from e if not projects: - typer.secho("Error: No supported projects (Spring, Angular, Vue) found.", fg=typer.colors.RED) + typer.secho( + "Error: No supported projects (Spring, Angular, Vue) found.", fg=typer.colors.RED + ) raise typer.Exit(1) services_data = [] diff --git a/devctl/commands/init.py b/devctl/commands/init.py index 4b908c6..2ccd6b4 100644 --- a/devctl/commands/init.py +++ b/devctl/commands/init.py @@ -1,24 +1,25 @@ """ CLI command group for initializing new projects. -Supports Spring Boot, Angular, Vue.js, NestJS, NodeJS, React, NextJS, FastAPI, Django, Svelte, and Go. +Supports Spring Boot, Angular, Vue.js, NestJS, NodeJS, React, NextJS, FastAPI, +Django, Svelte, and Go. """ import typer # Angular generator from devctl.generators.angular import generate_angular_boilerplate - -# Spring generator -from devctl.generators.spring import download_spring_boilerplate -from devctl.generators.vue import generate_vue_boilerplate +from devctl.generators.django import generate_django_boilerplate +from devctl.generators.fastapi import generate_fastapi_boilerplate +from devctl.generators.go_fiber import generate_go_boilerplate from devctl.generators.nestjs import generate_nest_boilerplate +from devctl.generators.nextjs import generate_nextjs_boilerplate from devctl.generators.nodejs import generate_nodejs_boilerplate from devctl.generators.react import generate_react_boilerplate -from devctl.generators.nextjs import generate_nextjs_boilerplate -from devctl.generators.fastapi import generate_fastapi_boilerplate -from devctl.generators.django import generate_django_boilerplate + +# Spring generator +from devctl.generators.spring import download_spring_boilerplate from devctl.generators.svelte import generate_svelte_boilerplate -from devctl.generators.go_fiber import generate_go_boilerplate +from devctl.generators.vue import generate_vue_boilerplate from devctl.orchestrator.config_builder import generate_config from devctl.utils.dependencies import check_tool diff --git a/devctl/commands/run.py b/devctl/commands/run.py index c5d43e6..0ecb89b 100644 --- a/devctl/commands/run.py +++ b/devctl/commands/run.py @@ -3,11 +3,12 @@ Automatically detects and launches backend, frontend, and database services. """ -import typer from pathlib import Path -from devctl.orchestrator.runner import launch_dev_environment +import typer + from devctl.generators.docker_scaffold import discover_docker_projects +from devctl.orchestrator.runner import launch_dev_environment from devctl.utils.dependencies import check_tool app = typer.Typer(help="Local execution and development commands.") @@ -22,18 +23,18 @@ def run_env(ctx: typer.Context): return typer.secho("Analyzing the current directory tree...", fg=typer.colors.CYAN) - + projects = discover_docker_projects(".") - + # Check for docker-compose.yml files docker_composes = [] for p in Path(".").rglob("docker-compose.yml"): if "node_modules" not in str(p) and "target" not in str(p) and ".git" not in str(p): docker_composes.append(p.parent) - has_spring = any(p.kind == "spring" for p in projects) - has_angular = any(p.kind == "angular" for p in projects) - has_vue = any(p.kind == "vue" for p in projects) + any(p.kind == "spring" for p in projects) + any(p.kind == "angular" for p in projects) + any(p.kind == "vue" for p in projects) has_docker = len(docker_composes) > 0 # Check dependencies based on detection @@ -47,10 +48,14 @@ def run_env(ctx: typer.Context): # Visual summary of detection for the user def get_status(condition: bool): - return typer.style("FOUND", fg=typer.colors.GREEN) if condition else typer.style("MISSING", fg=typer.colors.RED) + return ( + typer.style("FOUND", fg=typer.colors.GREEN) + if condition + else typer.style("MISSING", fg=typer.colors.RED) + ) typer.echo(f" - Docker Compose ({len(docker_composes)}) : {get_status(has_docker)}") - + for kind in sorted(counts.keys()): typer.echo(f" - {kind.capitalize()} ({counts[kind]}) : {get_status(True)}") diff --git a/devctl/generators/angular.py b/devctl/generators/angular.py index 531cc10..977f1be 100644 --- a/devctl/generators/angular.py +++ b/devctl/generators/angular.py @@ -94,9 +94,7 @@ def generate_angular_boilerplate(project_name: str) -> bool: try: command = ["ng", "new", safe_name, "--routing=true", "--style=scss", "--skip-git=true"] - typer.secho( - "Downloading npm packages... (This may take 1-2 minutes)", fg=typer.colors.CYAN - ) + typer.secho("Downloading npm packages... (This may take 1-2 minutes)", fg=typer.colors.CYAN) subprocess.run(command, check=True) # Post-installation configuration diff --git a/devctl/generators/django.py b/devctl/generators/django.py index b70ae07..c56a4ec 100644 --- a/devctl/generators/django.py +++ b/devctl/generators/django.py @@ -5,6 +5,7 @@ import os import subprocess + import typer @@ -12,13 +13,13 @@ def generate_django_boilerplate(project_name: str) -> bool: """ Generates a new Django project. """ - typer.secho(f"šŸ”„ Generating Django project '{project_name}'...", fg=typer.colors.CYAN) - safe_name = project_name.lower().replace("-", "_") # Django prefers underscores + typer.secho(f"Generating Django project '{project_name}'...", fg=typer.colors.CYAN) + safe_name = project_name.lower().replace("-", "_") # Django prefers underscores project_path = os.path.join(os.getcwd(), project_name) try: os.makedirs(project_path, exist_ok=True) - + # 1. Create requirements.txt requirements = """django djangorestframework @@ -28,28 +29,33 @@ def generate_django_boilerplate(project_name: str) -> bool: f.write(requirements) # 2. Create virtual environment - typer.secho("šŸ“¦ Creating virtual environment...", fg=typer.colors.CYAN) + typer.secho("Creating virtual environment...", fg=typer.colors.CYAN) subprocess.run(["python3", "-m", "venv", ".venv"], cwd=project_path, check=True) - + # 3. Install Django - typer.secho("ā³ Installing Django and DRF...", fg=typer.colors.CYAN) + typer.secho("Installing Django and DRF...", fg=typer.colors.CYAN) pip_path = os.path.join(".venv", "bin", "pip") - subprocess.run([pip_path, "install", "django", "djangorestframework"], cwd=project_path, check=True, stdout=subprocess.DEVNULL) + subprocess.run( + [pip_path, "install", "django", "djangorestframework"], + cwd=project_path, + check=True, + stdout=subprocess.DEVNULL, + ) # 4. Start Project - typer.secho("šŸš€ Scaffolding Django project structure...", fg=typer.colors.CYAN) + typer.secho("Scaffolding Django project structure...", fg=typer.colors.CYAN) django_admin = os.path.join(".venv", "bin", "django-admin") subprocess.run([django_admin, "startproject", safe_name, "."], cwd=project_path, check=True) - + # 5. Create core app python_path = os.path.join(".venv", "bin", "python") subprocess.run([python_path, "manage.py", "startapp", "core"], cwd=project_path, check=True) typer.secho( - f"āœ… Django project '{project_name}' successfully generated!", fg=typer.colors.GREEN + f"Django project '{project_name}' successfully generated!", fg=typer.colors.GREEN ) return True except Exception as e: - typer.secho(f"āŒ Django initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: Django initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/docker_scaffold.py b/devctl/generators/docker_scaffold.py index 26d7901..9cac912 100644 --- a/devctl/generators/docker_scaffold.py +++ b/devctl/generators/docker_scaffold.py @@ -103,7 +103,7 @@ def discover_docker_projects(root_path: Union[str, Path]) -> list[DockerProject] candidates.append(("spring", project_path)) if "angular.json" in filename_set: candidates.append(("angular", project_path)) - + has_vite_config = {"vite.config.ts", "vite.config.js"} & filename_set if has_vite_config and "angular.json" not in filename_set: # Check package.json to distinguish between vue and react @@ -114,13 +114,13 @@ def discover_docker_projects(root_path: Union[str, Path]) -> list[DockerProject] deps = pkg.get("dependencies", {}) dev_deps = pkg.get("devDependencies", {}) all_deps = {**deps, **dev_deps} - + if "vue" in all_deps: candidates.append(("vue", project_path)) elif "react" in all_deps: candidates.append(("react", project_path)) else: - candidates.append(("vue", project_path)) # Fallback to vue + candidates.append(("vue", project_path)) # Fallback to vue except Exception: candidates.append(("vue", project_path)) else: @@ -128,35 +128,41 @@ def discover_docker_projects(root_path: Union[str, Path]) -> list[DockerProject] if "nest-cli.json" in filename_set: candidates.append(("nest", project_path)) - + if any(f.startswith("next.config.") for f in filename_set): candidates.append(("nextjs", project_path)) - + if "svelte.config.js" in filename_set: candidates.append(("svelte", project_path)) if "main.py" in filename_set and "requirements.txt" in filename_set: try: - reqs = (project_path / "requirements.txt").read_text() + reqs = (project_path / "requirements.txt").read_text(encoding="utf-8") if "fastapi" in reqs.lower(): candidates.append(("fastapi", project_path)) elif "django" in reqs.lower(): candidates.append(("django", project_path)) - except Exception: + except (OSError, UnicodeDecodeError): + # Best-effort discovery: unreadable requirements files should not stop scanning. pass if "manage.py" in filename_set and "requirements.txt" in filename_set: try: - reqs = (project_path / "requirements.txt").read_text() + reqs = (project_path / "requirements.txt").read_text(encoding="utf-8") if "django" in reqs.lower(): candidates.append(("django", project_path)) - except Exception: + except (OSError, UnicodeDecodeError): + # Best-effort discovery: ignore unreadable/invalid requirements.txt here. pass - + if "go.mod" in filename_set: candidates.append(("go", project_path)) - if "package.json" in filename_set and not any(k in ["angular", "vue", "react", "nest", "nextjs", "svelte"] for k, p in candidates if p == project_path): + if "package.json" in filename_set and not any( + k in ["angular", "vue", "react", "nest", "nextjs", "svelte"] + for k, p in candidates + if p == project_path + ): candidates.append(("nodejs", project_path)) used_names: set[str] = set() @@ -176,7 +182,9 @@ def discover_docker_projects(root_path: Union[str, Path]) -> list[DockerProject] relative_context=_relative_context(root, project_path), java_version=_spring_java_version(project_path) if kind == "spring" else None, node_version=( - _node_version(project_path, kind) if kind in {"angular", "vue", "react", "nest", "nodejs", "nextjs", "svelte"} else None + _node_version(project_path, kind) + if kind in {"angular", "vue", "react", "nest", "nodejs", "nextjs", "svelte"} + else None ), angular_output_name=( _angular_output_name(project_path) if kind == "angular" else None diff --git a/devctl/generators/fastapi.py b/devctl/generators/fastapi.py index 420358d..1a3a11b 100644 --- a/devctl/generators/fastapi.py +++ b/devctl/generators/fastapi.py @@ -5,6 +5,7 @@ import os import subprocess + import typer @@ -12,13 +13,13 @@ def generate_fastapi_boilerplate(project_name: str) -> bool: """ Generates a new FastAPI project. """ - typer.secho(f"šŸ”„ Generating FastAPI project '{project_name}'...", fg=typer.colors.CYAN) + typer.secho(f"Generating FastAPI project '{project_name}'...", fg=typer.colors.CYAN) safe_name = project_name.lower().replace("_", "-") project_path = os.path.join(os.getcwd(), safe_name) try: os.makedirs(project_path, exist_ok=True) - + # 1. Create main.py main_py = """from fastapi import FastAPI @@ -34,7 +35,7 @@ def read_item(item_id: int, q: str = None): """ with open(os.path.join(project_path, "main.py"), "w") as f: f.write(main_py) - + # 2. Create requirements.txt requirements = """fastapi uvicorn[standard] @@ -44,20 +45,23 @@ def read_item(item_id: int, q: str = None): f.write(requirements) # 3. Create virtual environment - typer.secho("šŸ“¦ Creating virtual environment...", fg=typer.colors.CYAN) + typer.secho("Creating virtual environment...", fg=typer.colors.CYAN) subprocess.run(["python3", "-m", "venv", ".venv"], cwd=project_path, check=True) - + # 4. Install dependencies - typer.secho("ā³ Installing dependencies (fastapi, uvicorn)...", fg=typer.colors.CYAN) + typer.secho("Installing dependencies (fastapi, uvicorn)...", fg=typer.colors.CYAN) # Note: on Linux it's .venv/bin/pip pip_path = os.path.join(".venv", "bin", "pip") - subprocess.run([pip_path, "install", "-r", "requirements.txt"], cwd=project_path, check=True, stdout=subprocess.DEVNULL) - - typer.secho( - f"āœ… FastAPI project '{safe_name}' successfully generated!", fg=typer.colors.GREEN + subprocess.run( + [pip_path, "install", "-r", "requirements.txt"], + cwd=project_path, + check=True, + stdout=subprocess.DEVNULL, ) + + typer.secho(f"FastAPI project '{safe_name}' successfully generated!", fg=typer.colors.GREEN) return True except Exception as e: - typer.secho(f"āŒ FastAPI initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: FastAPI initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/go_fiber.py b/devctl/generators/go_fiber.py index 3a74b8f..628e655 100644 --- a/devctl/generators/go_fiber.py +++ b/devctl/generators/go_fiber.py @@ -5,6 +5,7 @@ import os import subprocess + import typer @@ -12,19 +13,24 @@ def generate_go_boilerplate(project_name: str) -> bool: """ Generates a new Go + Fiber project. """ - typer.secho(f"šŸ”„ Generating Go project '{project_name}'...", fg=typer.colors.CYAN) + typer.secho(f"Generating Go project '{project_name}'...", fg=typer.colors.CYAN) project_path = os.path.join(os.getcwd(), project_name) try: os.makedirs(project_path, exist_ok=True) - + # 1. Go mod init - typer.secho("šŸ“¦ Initializing Go module...", fg=typer.colors.CYAN) + typer.secho("Initializing Go module...", fg=typer.colors.CYAN) subprocess.run(["go", "mod", "init", project_name], cwd=project_path, check=True) - + # 2. Install Fiber - typer.secho("ā³ Installing Fiber framework...", fg=typer.colors.CYAN) - subprocess.run(["go", "get", "github.com/gofiber/fiber/v2"], cwd=project_path, check=True, stdout=subprocess.DEVNULL) + typer.secho("Installing Fiber framework...", fg=typer.colors.CYAN) + subprocess.run( + ["go", "get", "github.com/gofiber/fiber/v2"], + cwd=project_path, + check=True, + stdout=subprocess.DEVNULL, + ) # 3. Create main.go main_go = """package main @@ -47,11 +53,9 @@ def generate_go_boilerplate(project_name: str) -> bool: with open(os.path.join(project_path, "main.go"), "w") as f: f.write(main_go) - typer.secho( - f"āœ… Go project '{project_name}' successfully generated!", fg=typer.colors.GREEN - ) + typer.secho(f"Go project '{project_name}' successfully generated!", fg=typer.colors.GREEN) return True except Exception as e: - typer.secho(f"āŒ Go initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: Go initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/nestjs.py b/devctl/generators/nestjs.py index d00daaa..bd833ad 100644 --- a/devctl/generators/nestjs.py +++ b/devctl/generators/nestjs.py @@ -3,7 +3,6 @@ Includes boilerplate generation via Nest CLI. """ -import os import subprocess import typer @@ -21,10 +20,20 @@ def generate_nest_boilerplate(project_name: str) -> bool: # --package-manager npm: ensures npm is used # --strict: enables strict mode # --skip-git: devctl might be in a git repo already - typer.secho("šŸ“¦ Scaffolding NestJS project (this may take a minute)...", fg=typer.colors.CYAN) + typer.secho("Scaffolding NestJS project (this may take a minute)...", fg=typer.colors.CYAN) subprocess.run( - ["npx", "-p", "@nestjs/cli", "nest", "new", safe_name, "--package-manager", "npm", "--skip-git"], - check=True + [ + "npx", + "-p", + "@nestjs/cli", + "nest", + "new", + safe_name, + "--package-manager", + "npm", + "--skip-git", + ], + check=True, ) typer.secho( diff --git a/devctl/generators/nextjs.py b/devctl/generators/nextjs.py index 7a3a1c7..295d295 100644 --- a/devctl/generators/nextjs.py +++ b/devctl/generators/nextjs.py @@ -3,8 +3,8 @@ Includes boilerplate generation via create-next-app. """ -import os import subprocess + import typer @@ -16,7 +16,7 @@ def generate_nextjs_boilerplate(project_name: str) -> bool: safe_name = project_name.lower().replace("_", "-") try: - typer.secho("šŸ“¦ Scaffolding NextJS project (this may take a minute)...", fg=typer.colors.CYAN) + typer.secho("Scaffolding NextJS project (this may take a minute)...", fg=typer.colors.CYAN) # --ts: TypeScript # --eslint: ESLint # --tailwind: Tailwind CSS @@ -24,8 +24,20 @@ def generate_nextjs_boilerplate(project_name: str) -> bool: # --app: Use App Router # --import-alias: alias for imports subprocess.run( - ["npx", "create-next-app@latest", safe_name, "--ts", "--eslint", "--tailwind", "--src-dir", "--app", "--import-alias", "@/*", "--use-npm"], - check=True + [ + "npx", + "create-next-app@latest", + safe_name, + "--ts", + "--eslint", + "--tailwind", + "--src-dir", + "--app", + "--import-alias", + "@/*", + "--use-npm", + ], + check=True, ) typer.secho( diff --git a/devctl/generators/nodejs.py b/devctl/generators/nodejs.py index 8b846d8..49d720a 100644 --- a/devctl/generators/nodejs.py +++ b/devctl/generators/nodejs.py @@ -3,9 +3,10 @@ Includes boilerplate generation with TypeScript and Express. """ +import json import os import subprocess -import json + import typer @@ -19,29 +20,50 @@ def generate_nodejs_boilerplate(project_name: str) -> bool: try: os.makedirs(project_path, exist_ok=True) - + # 1. Initialize package.json - typer.secho("šŸ“¦ Initializing package.json...", fg=typer.colors.CYAN) - subprocess.run(["npm", "init", "-y"], cwd=project_path, check=True, stdout=subprocess.DEVNULL) - + typer.secho("Initializing package.json...", fg=typer.colors.CYAN) + subprocess.run( + ["npm", "init", "-y"], cwd=project_path, check=True, stdout=subprocess.DEVNULL + ) + # 2. Install dependencies - typer.secho("ā³ Installing dependencies (express, typescript, ts-node, nodemon)...", fg=typer.colors.CYAN) + typer.secho( + "Installing dependencies (express, typescript, ts-node, nodemon)...", + fg=typer.colors.CYAN, + ) subprocess.run( - ["npm", "install", "express", "dotenv"], - cwd=project_path, check=True, stdout=subprocess.DEVNULL + ["npm", "install", "express", "dotenv"], + cwd=project_path, + check=True, + stdout=subprocess.DEVNULL, ) subprocess.run( - ["npm", "install", "-D", "typescript", "@types/node", "@types/express", "ts-node", "nodemon", "rimraf"], - cwd=project_path, check=True, stdout=subprocess.DEVNULL + [ + "npm", + "install", + "-D", + "typescript", + "@types/node", + "@types/express", + "ts-node", + "nodemon", + "rimraf", + ], + cwd=project_path, + check=True, + stdout=subprocess.DEVNULL, ) # 3. Initialize TypeScript - typer.secho("āš™ļø Configuring TypeScript...", fg=typer.colors.CYAN) - subprocess.run(["npx", "tsc", "--init"], cwd=project_path, check=True, stdout=subprocess.DEVNULL) - + typer.secho("Configuring TypeScript...", fg=typer.colors.CYAN) + subprocess.run( + ["npx", "tsc", "--init"], cwd=project_path, check=True, stdout=subprocess.DEVNULL + ) + # 4. Create folder structure os.makedirs(os.path.join(project_path, "src"), exist_ok=True) - + # 5. Create basic index.ts index_ts = """import express, { Request, Response } from 'express'; import dotenv from 'dotenv'; @@ -67,21 +89,21 @@ def generate_nodejs_boilerplate(project_name: str) -> bool: # 6. Update package.json scripts with open(os.path.join(project_path, "package.json"), "r") as f: pkg = json.load(f) - + pkg["scripts"] = { "start": "node dist/index.js", "build": "rimraf dist && tsc", - "dev": "nodemon src/index.ts" + "dev": "nodemon src/index.ts", } - + with open(os.path.join(project_path, "package.json"), "w") as f: json.dump(pkg, f, indent=2) typer.secho( - f"āœ… NodeJS/Express project '{safe_name}' successfully generated!", fg=typer.colors.GREEN + f"NodeJS/Express project '{safe_name}' successfully generated!", fg=typer.colors.GREEN ) return True except Exception as e: - typer.secho(f"āŒ NodeJS/Express initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: NodeJS/Express initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/react.py b/devctl/generators/react.py index e25c887..5256afb 100644 --- a/devctl/generators/react.py +++ b/devctl/generators/react.py @@ -5,6 +5,7 @@ import os import subprocess + import typer @@ -12,29 +13,31 @@ def generate_react_boilerplate(project_name: str) -> bool: """ Generates a new React + TypeScript project via Vite. """ - typer.secho(f"šŸ”„ Generating ReactJS frontend '{project_name}' via Vite...", fg=typer.colors.CYAN) + typer.secho(f"Generating ReactJS frontend '{project_name}' via Vite...", fg=typer.colors.CYAN) safe_name = project_name.lower().replace("_", "-") try: - typer.secho("šŸ“¦ Scaffolding React project...", fg=typer.colors.CYAN) + typer.secho("Scaffolding React project...", fg=typer.colors.CYAN) subprocess.run( - ["npm", "create", "vite@latest", safe_name, "--", "--template", "react-ts"], - check=True + ["npm", "create", "vite@latest", safe_name, "--", "--template", "react-ts"], + check=True, ) project_full_path = os.path.join(os.getcwd(), safe_name) - typer.secho("ā³ Installing npm dependencies...", fg=typer.colors.CYAN) + typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN) subprocess.run(["npm", "install"], cwd=project_full_path, check=True) typer.secho( - f"āœ… ReactJS frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN + f"ReactJS frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN ) return True except subprocess.CalledProcessError as e: - typer.secho(f"āŒ React/Vite process failed with code: {e.returncode}", fg=typer.colors.RED) + typer.secho( + f"Error: React/Vite process failed with code: {e.returncode}", fg=typer.colors.RED + ) return False except Exception as e: - typer.secho(f"āŒ React/Vite initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: React/Vite initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/scaffold_angular.py b/devctl/generators/scaffold_angular.py index 9901f3f..52e3793 100644 --- a/devctl/generators/scaffold_angular.py +++ b/devctl/generators/scaffold_angular.py @@ -155,6 +155,9 @@ def generate_angular_resource(resource_name: str, fields_str: str, root_path: st f.write("") typer.echo(f" - Created (empty): {comp['dir']}/{target_file_name}") else: - typer.secho(f"Warning: Failed to generate {comp['template']}: {e}", fg=typer.colors.YELLOW) + typer.secho( + f"Warning: Failed to generate {comp['template']}: {e}", + fg=typer.colors.YELLOW, + ) typer.secho(f"{entity_name} feature successfully generated!", fg=typer.colors.GREEN) diff --git a/devctl/generators/scaffold_django.py b/devctl/generators/scaffold_django.py index 19eaeb2..174af2f 100644 --- a/devctl/generators/scaffold_django.py +++ b/devctl/generators/scaffold_django.py @@ -4,7 +4,9 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment @@ -19,7 +21,7 @@ def generate_django_resource(resource_name: str, fields_str: str, root_path: str raise typer.Exit(code=1) django_root = env_state["django_path"] - resource_lower = resource_name.lower() + resource_name.lower() entity_name = resource_name.capitalize() # Structure: core/models.py, core/serializers.py, core/views.py @@ -48,7 +50,7 @@ def __str__(self): if not os.path.exists(serializer_path): with open(serializer_path, "w") as f: f.write("from rest_framework import serializers\nfrom .models import *\n") - + serializer_snippet = f""" class {entity_name}Serializer(serializers.ModelSerializer): class Meta: @@ -72,6 +74,6 @@ class {entity_name}ViewSet(viewsets.ModelViewSet): f.write(view_snippet) typer.secho(f"āœ… {entity_name} Django feature successfully generated!", fg=typer.colors.GREEN) - typer.echo(f" - Updated: core/models.py") - typer.echo(f" - Updated: core/serializers.py") - typer.echo(f" - Updated: core/views.py") + typer.echo(" - Updated: core/models.py") + typer.echo(" - Updated: core/serializers.py") + typer.echo(" - Updated: core/views.py") diff --git a/devctl/generators/scaffold_fastapi.py b/devctl/generators/scaffold_fastapi.py index ce11210..b7788ff 100644 --- a/devctl/generators/scaffold_fastapi.py +++ b/devctl/generators/scaffold_fastapi.py @@ -4,7 +4,9 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment @@ -26,11 +28,12 @@ def generate_fastapi_resource(resource_name: str, fields_str: str, root_path: st routers_dir = os.path.join(fastapi_root, "routers") schemas_dir = os.path.join(fastapi_root, "schemas") models_dir = os.path.join(fastapi_root, "models") - + for d in [routers_dir, schemas_dir, models_dir]: os.makedirs(d, exist_ok=True) # Ensure __init__.py exists - with open(os.path.join(d, "__init__.py"), "a"): pass + with open(os.path.join(d, "__init__.py"), "a"): + pass typer.secho(f"āš™ļø Generating FastAPI resource '{entity_name}'...", fg=typer.colors.CYAN) @@ -64,24 +67,6 @@ class Config: tags=["{resource_lower}s"] ) -@{resource_lower}.get("/", response_model=List[schemas.{entity_name}]) -def read_{resource_lower}s(): - return [] - -@{resource_lower}.post("/", response_model=schemas.{entity_name}) -def create_{resource_lower}({resource_lower}: schemas.{entity_name}Create): - return {{"id": 1, **{resource_lower}.dict()}} -""" - # Note: fixed typo in template during thought but let's correct it properly - router_content = f"""from fastapi import APIRouter, HTTPException -from typing import List -from schemas import {resource_lower} as schemas - -router = APIRouter( - prefix="/{resource_lower}s", - tags=["{resource_lower}s"] -) - @router.get("/", response_model=List[schemas.{entity_name}]) def read_{resource_lower}s(): return [] @@ -93,6 +78,6 @@ def create_{resource_lower}(item: schemas.{entity_name}Create): with open(os.path.join(routers_dir, f"{resource_lower}.py"), "w") as f: f.write(router_content) - typer.secho(f"āœ… {entity_name} FastAPI feature successfully generated!", fg=typer.colors.GREEN) + typer.secho(f"{entity_name} FastAPI feature successfully generated!", fg=typer.colors.GREEN) typer.echo(f" - Created: schemas/{resource_lower}.py") typer.echo(f" - Created: routers/{resource_lower}.py") diff --git a/devctl/generators/scaffold_go.py b/devctl/generators/scaffold_go.py index 24012ab..0f0499e 100644 --- a/devctl/generators/scaffold_go.py +++ b/devctl/generators/scaffold_go.py @@ -4,7 +4,9 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment @@ -25,7 +27,7 @@ def generate_go_resource(resource_name: str, fields_str: str, root_path: str = " # Structure: handlers/resource.go, models/resource.go handlers_dir = os.path.join(go_root, "handlers") models_dir = os.path.join(go_root, "models") - + os.makedirs(handlers_dir, exist_ok=True) os.makedirs(models_dir, exist_ok=True) diff --git a/devctl/generators/scaffold_nestjs.py b/devctl/generators/scaffold_nestjs.py index cc88aa5..9d0e488 100644 --- a/devctl/generators/scaffold_nestjs.py +++ b/devctl/generators/scaffold_nestjs.py @@ -3,9 +3,10 @@ Handles the creation of modules, controllers, and services using Nest CLI. """ -import os import subprocess + import typer + from devctl.orchestrator.scanner import detect_environment @@ -31,13 +32,21 @@ def generate_nest_resource(resource_name: str, fields_str: str, root_path: str = subprocess.run( ["npx", "@nestjs/cli", "g", "resource", resource_lower, "--no-spec"], cwd=nest_root, - check=True + check=True, ) - typer.secho(f"āœ… {resource_name} NestJS resource successfully generated!", fg=typer.colors.GREEN) - typer.echo(f"šŸ’” Note: Fields [{fields_str}] were provided but manual DTO update is recommended for NestJS.") + typer.secho( + f"āœ… {resource_name} NestJS resource successfully generated!", fg=typer.colors.GREEN + ) + typer.echo( + f"šŸ’” Note: Fields [{fields_str}] were provided but manual DTO update " + "is recommended for NestJS." + ) except subprocess.CalledProcessError as e: - typer.secho(f"āŒ Nest CLI resource generation failed with code: {e.returncode}", fg=typer.colors.RED) + typer.secho( + f"āŒ Nest CLI resource generation failed with code: {e.returncode}", + fg=typer.colors.RED, + ) except Exception as e: typer.secho(f"āš ļø An unexpected error occurred: {e}", fg=typer.colors.YELLOW) diff --git a/devctl/generators/scaffold_nextjs.py b/devctl/generators/scaffold_nextjs.py index 63ff3ae..5014a12 100644 --- a/devctl/generators/scaffold_nextjs.py +++ b/devctl/generators/scaffold_nextjs.py @@ -4,11 +4,13 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment -def generate_nextjs_resource(resource_name: str, fields_str: str, root_path: str = "."): +def generate_nextjs_resource(resource_name: str, _fields_str: str, root_path: str = "."): """ Scaffolds a NextJS resource (Page, Component). """ @@ -25,7 +27,7 @@ def generate_nextjs_resource(resource_name: str, fields_str: str, root_path: str # Structure: src/app/resource-name/page.tsx app_dir = os.path.join(nextjs_root, "src", "app", resource_lower) components_dir = os.path.join(nextjs_root, "src", "components") - + os.makedirs(app_dir, exist_ok=True) os.makedirs(components_dir, exist_ok=True) diff --git a/devctl/generators/scaffold_nodejs.py b/devctl/generators/scaffold_nodejs.py index c0d0233..0e4b776 100644 --- a/devctl/generators/scaffold_nodejs.py +++ b/devctl/generators/scaffold_nodejs.py @@ -4,30 +4,32 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment -def generate_nodejs_resource(resource_name: str, fields_str: str, root_path: str = "."): +def generate_nodejs_resource(resource_name: str, _fields_str: str, root_path: str = "."): """ Scaffolds a NodeJS/Express resource. """ # Using basic manual file writing for simplicity and speed # In a more advanced version, we could use Jinja2 templates - + resource_lower = resource_name.lower() entity_name = resource_name.capitalize() - + src_dir = os.path.join(root_path, "src") if not os.path.exists(src_dir): # Try to find nodejs path from scanner - env_state = detect_environment(root_path) + detect_environment(root_path) # We need to add has_nodejs to scanner or just check if src exists in root src_dir = os.path.join(root_path, "src") routes_dir = os.path.join(src_dir, "routes") controllers_dir = os.path.join(src_dir, "controllers") - + os.makedirs(routes_dir, exist_ok=True) os.makedirs(controllers_dir, exist_ok=True) @@ -47,9 +49,9 @@ def generate_nodejs_resource(resource_name: str, fields_str: str, root_path: str export const create{entity_name} = (req: Request, res: Response) => {{ const data = req.body; - res.status(201).json({{ + res.status(201).json({{ message: '{entity_name} created', - data + data }}); }}; """ @@ -76,4 +78,4 @@ def generate_nodejs_resource(resource_name: str, fields_str: str, root_path: str typer.secho(f"āœ… {entity_name} NodeJS resource successfully generated!", fg=typer.colors.GREEN) typer.echo(f" - Created: controllers/{resource_lower}.controller.ts") typer.echo(f" - Created: routes/{resource_lower}.routes.ts") - typer.echo(f"šŸ’” Don't forget to register the route in your main app file.") + typer.echo("šŸ’” Don't forget to register the route in your main app file.") diff --git a/devctl/generators/scaffold_react.py b/devctl/generators/scaffold_react.py index 2d98e9e..e6f751a 100644 --- a/devctl/generators/scaffold_react.py +++ b/devctl/generators/scaffold_react.py @@ -4,7 +4,9 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment @@ -25,7 +27,7 @@ def generate_react_resource(resource_name: str, fields_str: str, root_path: str # Structure: src/components/ResourceName/... components_dir = os.path.join(react_root, "src", "components", entity_name) services_dir = os.path.join(react_root, "src", "services") - + os.makedirs(components_dir, exist_ok=True) os.makedirs(services_dir, exist_ok=True) diff --git a/devctl/generators/scaffold_svelte.py b/devctl/generators/scaffold_svelte.py index 61684e8..21cd384 100644 --- a/devctl/generators/scaffold_svelte.py +++ b/devctl/generators/scaffold_svelte.py @@ -4,11 +4,13 @@ """ import os + import typer + from devctl.orchestrator.scanner import detect_environment -def generate_svelte_resource(resource_name: str, fields_str: str, root_path: str = "."): +def generate_svelte_resource(resource_name: str, _fields_str: str, root_path: str = "."): """ Scaffolds a Svelte resource (Route, Component). """ @@ -25,7 +27,7 @@ def generate_svelte_resource(resource_name: str, fields_str: str, root_path: str # Structure: src/routes/resource-name/+page.svelte routes_dir = os.path.join(svelte_root, "src", "routes", resource_lower) components_dir = os.path.join(svelte_root, "src", "lib", "components") - + os.makedirs(routes_dir, exist_ok=True) os.makedirs(components_dir, exist_ok=True) diff --git a/devctl/generators/svelte.py b/devctl/generators/svelte.py index 09fa4c6..d3c7563 100644 --- a/devctl/generators/svelte.py +++ b/devctl/generators/svelte.py @@ -5,6 +5,7 @@ import os import subprocess + import typer @@ -12,31 +13,40 @@ def generate_svelte_boilerplate(project_name: str) -> bool: """ Generates a new SvelteKit project using create-svelte via npx. """ - typer.secho(f"šŸ”„ Generating Svelte project '{project_name}'...", fg=typer.colors.CYAN) + typer.secho(f"Generating Svelte project '{project_name}'...", fg=typer.colors.CYAN) safe_name = project_name.lower().replace("_", "-") try: - typer.secho("šŸ“¦ Scaffolding SvelteKit project...", fg=typer.colors.CYAN) + typer.secho("Scaffolding SvelteKit project...", fg=typer.colors.CYAN) # Using a non-interactive way to scaffold svelte # We'll use the 'skeleton' template with TypeScript subprocess.run( - ["npx", "sv", "create", safe_name, "--template", "skeleton", "--types", "typescript", "--no-install", "--no-git"], - check=True + [ + "npx", + "sv", + "create", + safe_name, + "--template", + "skeleton", + "--types", + "typescript", + "--no-install", + "--no-git", + ], + check=True, ) project_full_path = os.path.join(os.getcwd(), safe_name) - typer.secho("ā³ Installing npm dependencies...", fg=typer.colors.CYAN) + typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN) subprocess.run(["npm", "install"], cwd=project_full_path, check=True) - typer.secho( - f"āœ… Svelte project '{safe_name}' successfully generated!", fg=typer.colors.GREEN - ) + typer.secho(f"Svelte project '{safe_name}' successfully generated!", fg=typer.colors.GREEN) return True except subprocess.CalledProcessError as e: - typer.secho(f"āŒ Svelte creation failed with code: {e.returncode}", fg=typer.colors.RED) + typer.secho(f"Error: Svelte creation failed with code: {e.returncode}", fg=typer.colors.RED) return False except Exception as e: - typer.secho(f"āŒ Svelte initialization failed: {e}", fg=typer.colors.RED) + typer.secho(f"Error: Svelte initialization failed: {e}", fg=typer.colors.RED) return False diff --git a/devctl/generators/vue.py b/devctl/generators/vue.py index a04893b..dcd7280 100644 --- a/devctl/generators/vue.py +++ b/devctl/generators/vue.py @@ -95,11 +95,11 @@ def generate_vue_boilerplate(project_name: str) -> bool: setup_vue_router(project_full_path) # ---------------------------------------- - typer.secho( - f"Vue.js frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN - ) + typer.secho(f"Vue.js frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN) return True except subprocess.CalledProcessError as e: - typer.secho(f"Error: Vue/Vite process failed with code: {e.returncode}", fg=typer.colors.RED) + typer.secho( + f"Error: Vue/Vite process failed with code: {e.returncode}", fg=typer.colors.RED + ) return False diff --git a/devctl/orchestrator/runner.py b/devctl/orchestrator/runner.py index 61fc28a..cf38eeb 100644 --- a/devctl/orchestrator/runner.py +++ b/devctl/orchestrator/runner.py @@ -3,12 +3,12 @@ Handles parallel process management for multi-tier applications with log prefixing. """ +import os +import signal import subprocess import sys -import time import threading -import signal -import os +import time from pathlib import Path from typing import List @@ -51,7 +51,7 @@ def launch_dev_environment(projects: List[DockerProject], docker_composes: List[ """ global active_processes - def signal_handler(sig, frame): + def signal_handler(_sig, _frame): typer.echo("\nShutdown requested. Cleaning up...") cleanup_and_exit(docker_composes) @@ -67,10 +67,8 @@ def signal_handler(sig, frame): for compose_path in docker_composes: typer.secho(f"Starting Docker Compose in {compose_path}...", fg=typer.colors.CYAN) - subprocess.run( - ["docker", "compose", "up", "-d"], cwd=str(compose_path), check=True - ) - + subprocess.run(["docker", "compose", "up", "-d"], cwd=str(compose_path), check=True) + typer.echo("Waiting 5s for databases to initialize...") time.sleep(5) @@ -78,7 +76,7 @@ def signal_handler(sig, frame): backends = [p for p in projects if p.kind == "spring"] for p in backends: typer.secho(f"Starting Spring Boot: {p.name}...", fg=typer.colors.GREEN) - + proc = subprocess.Popen( ["./mvnw", "spring-boot:run"], cwd=str(p.path), @@ -87,13 +85,15 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "green"), daemon=True) t.start() active_threads.append(t) # 3. Start Frontends (Angular / Vue / React / NextJS / Svelte) - frontends = [p for p in projects if p.kind in ["angular", "vue", "react", "nextjs", "svelte"]] + frontends = [ + p for p in projects if p.kind in ["angular", "vue", "react", "nextjs", "svelte"] + ] for p in frontends: if p.kind == "angular": color = "cyan" @@ -107,12 +107,15 @@ def signal_handler(sig, frame): elif p.kind == "nextjs": color = "yellow" cmd = ["npm", "run", "dev"] - else: # svelte + else: # svelte color = "red" cmd = ["npm", "run", "dev"] - - typer.secho(f"Starting {p.kind.capitalize()}: {p.name}...", fg=getattr(typer.colors, color.upper())) - + + typer.secho( + f"Starting {p.kind.capitalize()}: {p.name}...", + fg=getattr(typer.colors, color.upper()), + ) + proc = subprocess.Popen( cmd, cwd=str(p.path), @@ -121,7 +124,7 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, color), daemon=True) t.start() active_threads.append(t) @@ -130,7 +133,7 @@ def signal_handler(sig, frame): nest_apps = [p for p in projects if p.kind == "nest"] for p in nest_apps: typer.secho(f"Starting NestJS: {p.name}...", fg=typer.colors.MAGENTA) - + proc = subprocess.Popen( ["npm", "run", "start:dev"], cwd=str(p.path), @@ -139,7 +142,7 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "magenta"), daemon=True) t.start() active_threads.append(t) @@ -148,7 +151,7 @@ def signal_handler(sig, frame): nodejs_apps = [p for p in projects if p.kind == "nodejs"] for p in nodejs_apps: typer.secho(f"Starting NodeJS: {p.name}...", fg=typer.colors.GREEN) - + proc = subprocess.Popen( ["npm", "run", "dev"], cwd=str(p.path), @@ -157,7 +160,7 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "green"), daemon=True) t.start() active_threads.append(t) @@ -166,11 +169,11 @@ def signal_handler(sig, frame): fastapi_apps = [p for p in projects if p.kind == "fastapi"] for p in fastapi_apps: typer.secho(f"Starting FastAPI: {p.name}...", fg=typer.colors.CYAN) - + venv_python = os.path.join(str(p.path), ".venv", "bin", "python3") if not os.path.exists(venv_python): venv_python = "python3" - + proc = subprocess.Popen( [venv_python, "-m", "uvicorn", "main:app", "--reload"], cwd=str(p.path), @@ -179,7 +182,7 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "cyan"), daemon=True) t.start() active_threads.append(t) @@ -188,11 +191,11 @@ def signal_handler(sig, frame): django_apps = [p for p in projects if p.kind == "django"] for p in django_apps: typer.secho(f"Starting Django: {p.name}...", fg=typer.colors.GREEN) - + venv_python = os.path.join(str(p.path), ".venv", "bin", "python3") if not os.path.exists(venv_python): venv_python = "python3" - + proc = subprocess.Popen( [venv_python, "manage.py", "runserver"], cwd=str(p.path), @@ -201,7 +204,7 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "green"), daemon=True) t.start() active_threads.append(t) @@ -210,7 +213,7 @@ def signal_handler(sig, frame): go_apps = [p for p in projects if p.kind == "go"] for p in go_apps: typer.secho(f"Starting Go: {p.name}...", fg=typer.colors.CYAN) - + proc = subprocess.Popen( ["go", "run", "."], cwd=str(p.path), @@ -219,13 +222,16 @@ def signal_handler(sig, frame): bufsize=1, ) active_processes.append((p.name, proc)) - + t = threading.Thread(target=stream_logs, args=(p.name, proc, "cyan"), daemon=True) t.start() active_threads.append(t) if not active_processes and not docker_composes: - typer.secho("Warning: No projects or databases detected to run.", fg=typer.colors.YELLOW) + typer.secho( + "Warning: No projects or databases detected to run.", + fg=typer.colors.YELLOW, + ) return typer.secho( @@ -233,15 +239,14 @@ def signal_handler(sig, frame): fg=typer.colors.GREEN, bold=True, ) - # Keep the main thread alive while True: # Monitor process health - for name, p in processes: - exit_code = p.poll() + for name, proc in active_processes: + exit_code = proc.poll() if exit_code is not None: typer.secho( - f"\nāŒ Critical Error: {name} process terminated unexpectedly " + f"\nError: {name} process terminated unexpectedly " f"(Exit code: {exit_code}).", fg=typer.colors.RED, bold=True, @@ -251,9 +256,13 @@ def signal_handler(sig, frame): time.sleep(1) # Check if any process has died unexpectedly + # This is a bit redundant with the monitor above but kept for compatibility for name, proc in active_processes: if proc.poll() is not None: - typer.secho(f"Warning: Process {name} exited with code {proc.returncode}", fg=typer.colors.RED) + typer.secho( + f"Warning: Process {name} exited with code {proc.returncode}", + fg=typer.colors.RED, + ) active_processes.remove((name, proc)) except Exception as e: diff --git a/devctl/orchestrator/scanner.py b/devctl/orchestrator/scanner.py index 2961689..d65bbba 100644 --- a/devctl/orchestrator/scanner.py +++ b/devctl/orchestrator/scanner.py @@ -82,9 +82,7 @@ def detect_environment(root_path: str = "."): # 4. Vite-based detection (Vue/React) vue_markers = {"vite.config.ts", "vite.config.js"} - if (vue_markers & filename_set) and not any( - [env_state["has_vue"], env_state["has_react"]] - ): + if (vue_markers & filename_set) and not any([env_state["has_vue"], env_state["has_react"]]): # Distinguish by package.json pkg_path = current_path / "package.json" if pkg_path.exists(): @@ -133,14 +131,15 @@ def detect_environment(root_path: str = "."): if "requirements.txt" in filename_set: req_path = current_path / "requirements.txt" try: - req_content = req_path.read_text().lower() + req_content = req_path.read_text(encoding="utf-8").lower() if "fastapi" in req_content and not env_state["has_fastapi"]: env_state["has_fastapi"] = True env_state["fastapi_path"] = str(current_path) if "django" in req_content and not env_state["has_django"]: env_state["has_django"] = True env_state["django_path"] = str(current_path) - except Exception: + except (OSError, UnicodeDecodeError): + # Ignore unreadable requirements files during environment scanning pass # 9. Svelte detection diff --git a/test/test_add.py b/test/test_add.py new file mode 100644 index 0000000..57d1d24 --- /dev/null +++ b/test/test_add.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from typer.testing import CliRunner + +from devctl.main import app + +runner = CliRunner() + + +def test_add_resource_spring(tmp_path): + with patch("devctl.commands.add.detect_environment") as mock_detect: + mock_detect.return_value = { + "has_spring": True, + "spring_path": str(tmp_path), + "has_angular": False, + } + with patch("devctl.commands.add.generate_spring_resource") as mock_gen: + # Create a mock pom.xml so it looks like a spring project + (tmp_path / "pom.xml").write_text("") + result = runner.invoke(app, ["add", "resource", "Product"]) + assert result.exit_code == 0 + mock_gen.assert_called_once() + + +def test_add_resource_angular(tmp_path): + with patch("devctl.commands.add.detect_environment") as mock_detect: + mock_detect.return_value = { + "has_spring": False, + "has_angular": True, + "angular_path": str(tmp_path), + } + with patch("devctl.commands.add.generate_angular_resource") as mock_gen: + result = runner.invoke(app, ["add", "resource", "User"]) + assert result.exit_code == 0 + mock_gen.assert_called_once() diff --git a/test/test_django.py b/test/test_django.py index 427134d..a49a608 100644 --- a/test/test_django.py +++ b/test/test_django.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_django_calls_boilerplate(tmp_path): + +def test_init_django_calls_boilerplate(): """Ensure init django calls the boilerplate generator.""" with patch("devctl.commands.init.generate_django_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,24 +17,26 @@ def test_init_django_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-django-app") + def test_add_resource_django(tmp_path): """Ensure add resource works in a Django project.""" (tmp_path / "manage.py").write_text("import os", encoding="utf-8") (tmp_path / "requirements.txt").write_text("django", encoding="utf-8") (tmp_path / "core").mkdir() - + with patch("devctl.commands.add.generate_django_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_django(tmp_path): """Ensure dockerize detects Django and generates a Dockerfile.""" (tmp_path / "manage.py").write_text("import os", encoding="utf-8") (tmp_path / "requirements.txt").write_text("django", encoding="utf-8") (tmp_path / "core").mkdir() - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_fastapi.py b/test/test_fastapi.py index df018a9..d5c5d39 100644 --- a/test/test_fastapi.py +++ b/test/test_fastapi.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_fastapi_calls_boilerplate(tmp_path): + +def test_init_fastapi_calls_boilerplate(): """Ensure init fastapi calls the boilerplate generator.""" with patch("devctl.commands.init.generate_fastapi_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,22 +17,24 @@ def test_init_fastapi_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-fast-app") + def test_add_resource_fastapi(tmp_path): """Ensure add resource works in a FastAPI project.""" (tmp_path / "main.py").write_text("from fastapi import FastAPI", encoding="utf-8") (tmp_path / "requirements.txt").write_text("fastapi", encoding="utf-8") - + with patch("devctl.commands.add.generate_fastapi_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_fastapi(tmp_path): """Ensure dockerize detects FastAPI and generates a Dockerfile.""" (tmp_path / "main.py").write_text("from fastapi import FastAPI", encoding="utf-8") (tmp_path / "requirements.txt").write_text("fastapi", encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_go.py b/test/test_go.py index 2d19433..371e778 100644 --- a/test/test_go.py +++ b/test/test_go.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_go_calls_boilerplate(tmp_path): + +def test_init_go_calls_boilerplate(): """Ensure init go calls the boilerplate generator.""" with patch("devctl.commands.init.generate_go_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,20 +17,22 @@ def test_init_go_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-go-app") + def test_add_resource_go(tmp_path): """Ensure add resource works in a Go project.""" (tmp_path / "go.mod").write_text("module my-go-app", encoding="utf-8") - + with patch("devctl.commands.add.generate_go_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_go(tmp_path): """Ensure dockerize detects Go and generates a Dockerfile.""" (tmp_path / "go.mod").write_text("module my-go-app", encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_init.py b/test/test_init.py new file mode 100644 index 0000000..6843ac7 --- /dev/null +++ b/test/test_init.py @@ -0,0 +1,33 @@ +from unittest.mock import patch + +from typer.testing import CliRunner + +from devctl.main import app + +runner = CliRunner() + + +def test_init_spring_success(): + with patch("devctl.commands.init.download_spring_boilerplate", return_value=True) as mock_dl: + with patch("devctl.commands.init.generate_config") as mock_cfg: + with patch("devctl.commands.init.check_tool"): + result = runner.invoke(app, ["init", "spring", "test-api"]) + assert result.exit_code == 0 + mock_dl.assert_called_once() + mock_cfg.assert_called_once() + + +def test_init_angular_success(): + with patch("devctl.commands.init.generate_angular_boilerplate", return_value=True) as mock_gen: + with patch("devctl.commands.init.check_tool"): + result = runner.invoke(app, ["init", "angular", "test-front"]) + assert result.exit_code == 0 + mock_gen.assert_called_once_with("test-front") + + +def test_init_vue_success(): + with patch("devctl.commands.init.generate_vue_boilerplate", return_value=True) as mock_gen: + with patch("devctl.commands.init.check_tool"): + result = runner.invoke(app, ["init", "vue", "test-vue"]) + assert result.exit_code == 0 + mock_gen.assert_called_once_with("test-vue") diff --git a/test/test_mongodb.py b/test/test_mongodb.py index 8add1fc..2ad3b25 100644 --- a/test/test_mongodb.py +++ b/test/test_mongodb.py @@ -1,21 +1,24 @@ import yaml -from pathlib import Path from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() + def test_deploy_mongodb(tmp_path): """Ensure deploy correctly detects and configures MongoDB.""" # Setup project with MongoDB properties backend = tmp_path / "mongo-api" backend.mkdir() - (backend / "pom.xml").write_text("mongo-api", encoding="utf-8") + (backend / "pom.xml").write_text( + "mongo-api", encoding="utf-8" + ) props = backend / "src" / "main" / "resources" props.mkdir(parents=True) (props / "application.properties").write_text( "spring.data.mongodb.uri=mongodb://admin:pass@localhost:27017/mydb?authSource=admin", - encoding="utf-8" + encoding="utf-8", ) result = runner.invoke(app, ["deploy", str(tmp_path)]) @@ -26,7 +29,7 @@ def test_deploy_mongodb(tmp_path): with open(compose_file, "r") as f: config = yaml.safe_load(f) - + services = config["services"] assert "mydb-db" in services db_service = services["mydb-db"] diff --git a/test/test_nestjs.py b/test/test_nestjs.py index f206620..1ea6b14 100644 --- a/test/test_nestjs.py +++ b/test/test_nestjs.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_nest_calls_boilerplate(tmp_path): + +def test_init_nest_calls_boilerplate(): """Ensure init nest calls the boilerplate generator.""" with patch("devctl.commands.init.generate_nest_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,11 +17,12 @@ def test_init_nest_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-nest-app") + def test_add_resource_nest(tmp_path): """Ensure add resource works in a NestJS project.""" # Setup a mock NestJS project (tmp_path / "nest-cli.json").write_text("{}", encoding="utf-8") - + with patch("devctl.commands.add.generate_nest_resource") as mock_gen: # We need to ensure detect_environment finds it os.chdir(tmp_path) @@ -30,11 +34,12 @@ def test_add_resource_nest(tmp_path): # Cleanup chdir if needed (though pytest tmp_path is usually fine) pass + def test_dockerize_nest(tmp_path): """Ensure dockerize detects NestJS and generates a Dockerfile.""" (tmp_path / "nest-cli.json").write_text("{}", encoding="utf-8") (tmp_path / "package.json").write_text('{"name": "nest-app"}', encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_nextjs.py b/test/test_nextjs.py index e2cae88..a13e76a 100644 --- a/test/test_nextjs.py +++ b/test/test_nextjs.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_nextjs_calls_boilerplate(tmp_path): + +def test_init_nextjs_calls_boilerplate(): """Ensure init nextjs calls the boilerplate generator.""" with patch("devctl.commands.init.generate_nextjs_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,21 +17,23 @@ def test_init_nextjs_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-next-app") + def test_add_resource_nextjs(tmp_path): """Ensure add resource works in a NextJS project.""" (tmp_path / "next.config.js").write_text("module.exports = {}", encoding="utf-8") - + with patch("devctl.commands.add.generate_nextjs_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_nextjs(tmp_path): """Ensure dockerize detects NextJS and generates a Dockerfile.""" (tmp_path / "next.config.js").write_text("module.exports = {}", encoding="utf-8") (tmp_path / "package.json").write_text('{"name": "next-app"}', encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_nodejs.py b/test/test_nodejs.py index b5a1cdb..16916a5 100644 --- a/test/test_nodejs.py +++ b/test/test_nodejs.py @@ -1,12 +1,14 @@ import os -import json from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_nodejs_calls_boilerplate(tmp_path): + +def test_init_nodejs_calls_boilerplate(): """Ensure init nodejs calls the boilerplate generator.""" with patch("devctl.commands.init.generate_nodejs_boilerplate") as mock_gen: mock_gen.return_value = True @@ -15,21 +17,23 @@ def test_init_nodejs_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-node-app") + def test_add_resource_nodejs(tmp_path): """Ensure add resource works in a NodeJS project.""" (tmp_path / "package.json").write_text('{"name": "node-app"}', encoding="utf-8") (tmp_path / "src").mkdir() - + with patch("devctl.commands.add.generate_nodejs_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_nodejs(tmp_path): """Ensure dockerize detects NodeJS and generates a Dockerfile.""" (tmp_path / "package.json").write_text('{"name": "node-app"}', encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_react.py b/test/test_react.py index 255b715..fcb3429 100644 --- a/test/test_react.py +++ b/test/test_react.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_react_calls_boilerplate(tmp_path): + +def test_init_react_calls_boilerplate(): """Ensure init react calls the boilerplate generator.""" with patch("devctl.commands.init.generate_react_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,22 +17,28 @@ def test_init_react_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-react-app") + def test_add_resource_react(tmp_path): """Ensure add resource works in a React project.""" - (tmp_path / "package.json").write_text('{"dependencies": {"react": "18.2.0"}}', encoding="utf-8") + (tmp_path / "package.json").write_text( + '{"dependencies": {"react": "18.2.0"}}', encoding="utf-8" + ) (tmp_path / "vite.config.ts").write_text("export default {}", encoding="utf-8") - + with patch("devctl.commands.add.generate_react_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_react(tmp_path): """Ensure dockerize detects React and generates a Dockerfile.""" - (tmp_path / "package.json").write_text('{"dependencies": {"react": "18.2.0"}}', encoding="utf-8") + (tmp_path / "package.json").write_text( + '{"dependencies": {"react": "18.2.0"}}', encoding="utf-8" + ) (tmp_path / "vite.config.ts").write_text("export default {}", encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_runner.py b/test/test_runner.py new file mode 100644 index 0000000..705b138 --- /dev/null +++ b/test/test_runner.py @@ -0,0 +1,65 @@ +import subprocess +from pathlib import Path +from unittest.mock import MagicMock, patch + +from devctl.generators.docker_scaffold import DockerProject +from devctl.orchestrator.runner import is_docker_running, launch_dev_environment + + +def test_is_docker_running_true(): + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + assert is_docker_running() is True + + +def test_is_docker_running_false(): + with patch("subprocess.run") as mock_run: + mock_run.side_effect = subprocess.CalledProcessError(1, "docker info") + assert is_docker_running() is False + + +def test_launch_dev_environment_empty(): + with patch("typer.secho") as mock_secho: + launch_dev_environment([], []) + mock_secho.assert_called_with( + "Warning: No projects or databases detected to run.", fg="yellow" + ) + + +@patch("subprocess.Popen") +@patch("subprocess.run") +@patch("time.sleep") +def test_launch_dev_environment_complex(mock_sleep, mock_run, mock_popen): + # Setup mocks + mock_popen.return_value = MagicMock(poll=lambda: None) + + projects = [ + DockerProject( + kind="spring", + path=Path("/tmp/spring"), + name="api", + service_name="api", + relative_context="./api", + ), + DockerProject( + kind="angular", + path=Path("/tmp/angular"), + name="front", + service_name="front", + relative_context="./front", + ), + ] + docker_composes = [Path("/tmp/db")] + + # Use an exception that is caught by launch_dev_environment to stop the loop + mock_sleep.side_effect = Exception("Stop Loop") + + with patch("devctl.orchestrator.runner.is_docker_running", return_value=True): + with patch("devctl.orchestrator.runner.cleanup_and_exit") as mock_cleanup: + launch_dev_environment(projects, docker_composes) + + # Verify Docker Compose started + mock_run.assert_any_call(["docker", "compose", "up", "-d"], cwd="/tmp/db", check=True) + + # Verify cleanup called + mock_cleanup.assert_called_once() diff --git a/test/test_svelte.py b/test/test_svelte.py index f011297..1725494 100644 --- a/test/test_svelte.py +++ b/test/test_svelte.py @@ -1,11 +1,14 @@ import os from unittest.mock import patch + from typer.testing import CliRunner + from devctl.main import app runner = CliRunner() -def test_init_svelte_calls_boilerplate(tmp_path): + +def test_init_svelte_calls_boilerplate(): """Ensure init svelte calls the boilerplate generator.""" with patch("devctl.commands.init.generate_svelte_boilerplate") as mock_gen: mock_gen.return_value = True @@ -14,21 +17,23 @@ def test_init_svelte_calls_boilerplate(tmp_path): assert result.exit_code == 0 mock_gen.assert_called_once_with("my-svelte-app") + def test_add_resource_svelte(tmp_path): """Ensure add resource works in a Svelte project.""" (tmp_path / "svelte.config.js").write_text("export default {}", encoding="utf-8") - + with patch("devctl.commands.add.generate_svelte_resource") as mock_gen: os.chdir(tmp_path) result = runner.invoke(app, ["add", "resource", "User"]) assert result.exit_code == 0 mock_gen.assert_called_once() + def test_dockerize_svelte(tmp_path): """Ensure dockerize detects Svelte and generates a Dockerfile.""" (tmp_path / "svelte.config.js").write_text("export default {}", encoding="utf-8") (tmp_path / "package.json").write_text('{"name": "svelte-app"}', encoding="utf-8") - + result = runner.invoke(app, ["dockerize", str(tmp_path)]) assert result.exit_code == 0 assert (tmp_path / "Dockerfile").exists() diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000..c72cbbb --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,21 @@ +from unittest.mock import patch + +import pytest +import typer + +from devctl.utils.dependencies import check_tool + + +def test_check_tool_success(): + """Ensure check_tool does nothing if the tool exists.""" + with patch("shutil.which", return_value="/usr/bin/docker"): + # Should not raise any exception + check_tool("docker") + + +def test_check_tool_failure(): + """Ensure check_tool raises Exit(1) if the tool is missing.""" + with patch("shutil.which", return_value=None): + with pytest.raises(typer.Exit) as excinfo: + check_tool("nonexistent-tool") + assert excinfo.value.exit_code == 1