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