From 984161e8d7edfd0ec42e4b140df1a5b08bfee80f Mon Sep 17 00:00:00 2001 From: AnouarMohamed Date: Mon, 25 May 2026 12:59:13 +0100 Subject: [PATCH 1/2] chore: codebase cleanup and standardization (#16) - Removed dead file devctl/commands/main.py - Added missing __init__.py to commands and generators - Standardized all comments and docstrings to English - Updated .gitignore to include *.egg-info and clean up structure --- .gitignore | 23 +++++++++++------------ devctl/commands/{main.py => __init__.py} | 0 devctl/commands/init.py | 8 ++++---- devctl/commands/run.py | 4 ++-- devctl/generators/__init__.py | 0 devctl/generators/angular.py | 14 +++++++------- devctl/generators/scaffold_angular.py | 6 +++--- devctl/generators/scaffold_spring.py | 22 +++++++++++----------- devctl/generators/spring.py | 12 ++++++------ devctl/generators/vue.py | 8 ++++---- devctl/main.py | 10 +++++----- devctl/orchestrator/config_builder.py | 2 +- devctl/orchestrator/runner.py | 18 +++++++++--------- devctl/orchestrator/scanner.py | 6 +++--- samples/sample-api/Dockerfile | 24 ++++++++++++++++++++++++ samples/sample-front/Dockerfile | 24 ++++++++++++++++++++++++ 16 files changed, 114 insertions(+), 67 deletions(-) rename devctl/commands/{main.py => __init__.py} (100%) create mode 100644 devctl/generators/__init__.py create mode 100644 samples/sample-api/Dockerfile create mode 100644 samples/sample-front/Dockerfile diff --git a/.gitignore b/.gitignore index 807dd9d..afcefc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,16 @@ -# Environnements virtuels Python -venv/ -env/ -.env -# Cache et fichiers compilés Python -__pycache__/ -*.py[cod] *$py.class - -# Fichiers de build Typer / Setuptools build/ +# Cache et fichiers compilés Python +devctl.egg-info/ dist/ -*.egg-info/ - # Dossier pycharm -.idea/ \ No newline at end of file +*.egg-info/ +.env +env/ +# Environnements virtuels Python +# Fichiers de build Typer / Setuptools +.idea/devctl.egg-info/ +__pycache__/ +*.py[cod] +venv/ diff --git a/devctl/commands/main.py b/devctl/commands/__init__.py similarity index 100% rename from devctl/commands/main.py rename to devctl/commands/__init__.py diff --git a/devctl/commands/init.py b/devctl/commands/init.py index bb496f0..227869d 100644 --- a/devctl/commands/init.py +++ b/devctl/commands/init.py @@ -1,15 +1,15 @@ import typer -# Génerateur Angular +# Angular Generator from devctl.generators.angular import generate_angular_boilerplate -# Générateur Spring +# Spring Generator from devctl.generators.spring import download_spring_boilerplate from devctl.generators.vue import generate_vue_boilerplate from devctl.orchestrator.config_builder import generate_config from devctl.utils.dependencies import check_tool -# L'application Typer locale pour le groupe de commandes "init" +# Local Typer app for the "init" command group app = typer.Typer(help="Initializes a new project based on the chosen framework.") @@ -24,7 +24,7 @@ def init_spring( """ check_tool("java", "initializing a Spring Boot project") - # Validation stricte des entrées + # Strict input validation if db not in ["postgres", "mysql"]: typer.secho(f"❌ Error: Database '{db}' is not supported.", fg=typer.colors.RED) raise typer.Exit(code=1) diff --git a/devctl/commands/run.py b/devctl/commands/run.py index 6b502a2..4164712 100644 --- a/devctl/commands/run.py +++ b/devctl/commands/run.py @@ -28,7 +28,7 @@ def run_env(ctx: typer.Context): if env_state["has_angular"] or env_state.get("has_vue"): check_tool("npm", "running the frontend project") - # Résumé visuel de la détection pour l'utilisateur + # Visual summary of detection for the user typer.echo(f" - Docker Database : {'✅' if env_state['has_docker_compose'] else '❌'}") typer.echo(f" - Spring Boot Backend : {'✅' if env_state['has_spring'] else '❌'}") typer.echo(f" - Angular Frontend : {'✅' if env_state['has_angular'] else '❌'}") @@ -47,5 +47,5 @@ def run_env(ctx: typer.Context): typer.secho("\n❌ No valid development environment detected here.", fg=typer.colors.RED) raise typer.Exit(code=1) - # Transfert à la couche d'orchestration système + # Hand off to the system orchestration layer launch_dev_environment(env_state) diff --git a/devctl/generators/__init__.py b/devctl/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devctl/generators/angular.py b/devctl/generators/angular.py index 9cf474b..b00f644 100644 --- a/devctl/generators/angular.py +++ b/devctl/generators/angular.py @@ -35,23 +35,23 @@ def setup_angular_environments(project_path: str): f.write(content) except Exception as e: typer.secho( - f"⚠️ Erreur lors de la génération de {tpl_name}: {e}", fg=typer.colors.YELLOW + f"⚠️ Error while generating {tpl_name}: {e}", fg=typer.colors.YELLOW ) - # 3. Modification de angular.json pour activer le proxy + # 3. Modify angular.json to enable the proxy angular_json_path = os.path.join(project_path, "angular.json") if os.path.exists(angular_json_path): with open(angular_json_path, "r", encoding="utf-8") as f: angular_config = json.load(f) try: - # On trouve le nom du projet par défaut (souvent le même nom que le dossier) + # Find the default project name (usually the same as the folder name) project_name = list(angular_config["projects"].keys())[0] - # Injection du proxyConfig dans l'architecte "serve" + # Inject proxyConfig into the "serve" architect serve_target = angular_config["projects"][project_name]["architect"]["serve"] - # S'assure que "options" existe + # Ensure "options" exists if "options" not in serve_target: serve_target["options"] = {} @@ -60,10 +60,10 @@ def setup_angular_environments(project_path: str): with open(angular_json_path, "w", encoding="utf-8") as f: json.dump(angular_config, f, indent=2) - typer.echo(" - angular.json mis à jour avec le proxyConfig.") + typer.echo(" - angular.json updated with proxyConfig.") except Exception as e: typer.secho( - f"⚠️ Impossible de modifier angular.json automatiquement : {e}", + f"⚠️ Could not automatically modify angular.json: {e}", fg=typer.colors.YELLOW, ) diff --git a/devctl/generators/scaffold_angular.py b/devctl/generators/scaffold_angular.py index 9c8f538..a4eed0c 100644 --- a/devctl/generators/scaffold_angular.py +++ b/devctl/generators/scaffold_angular.py @@ -33,7 +33,7 @@ def parse_ts_fields(fields_str: str): def generate_angular_resource(resource_name: str, fields_str: str, root_path: str = "."): """ - Orchestre la création de la feature Angular complète. + Orchestrates the creation of the complete Angular feature. """ env_state = detect_environment(root_path) @@ -45,10 +45,10 @@ def generate_angular_resource(resource_name: str, fields_str: str, root_path: st resource_lower = resource_name.lower() entity_name = resource_name.capitalize() - # Le dossier cible de la feature: src/app/features/produit + # The target feature directory: src/app/features/produit feature_dir = os.path.join(angular_root, "src", "app", "features", resource_lower) - # Configuration des composants à générer + # Component configuration to generate components = [ # Models { diff --git a/devctl/generators/scaffold_spring.py b/devctl/generators/scaffold_spring.py index 003273f..e4d7ad2 100644 --- a/devctl/generators/scaffold_spring.py +++ b/devctl/generators/scaffold_spring.py @@ -17,8 +17,8 @@ def parse_fields(fields_str: str): """ - Transforme la chaîne de caractères du terminal en données injectables. - Exemple: "nom:string, age:int" -> [{"name": "nom", "java_type": "String"}, ...] + Transforms terminal string into injectable data. + Example: "name:string, age:int" -> [{"name": "name", "java_type": "String"}, ...] """ if not fields_str: return [] @@ -37,8 +37,8 @@ def parse_fields(fields_str: str): def find_spring_base_package_and_path(): """ - Cherche le dossier contenant la classe @SpringBootApplication. - C'est la méthode la plus fiable pour trouver le package de base sans parser le pom.xml. + Searches for the folder containing the @SpringBootApplication class. + This is the most reliable method to find the base package without parsing pom.xml. """ java_src_dir = os.path.join(os.getcwd(), "src", "main", "java") @@ -57,7 +57,7 @@ def find_spring_base_package_and_path(): def generate_spring_resource(resource_name: str, fields_str: str): """ - Orchestre la création de l'architecture MVC + DTOs + Mapper. + Orchestrates the creation of the MVC + DTOs + Mapper architecture. """ base_package, base_path = find_spring_base_package_and_path() @@ -70,7 +70,7 @@ def generate_spring_resource(resource_name: str, fields_str: str): entity_name = resource_name.capitalize() - # Nouvelle configuration détaillée pour gérer les sous-dossiers (DTOs, Mapper) + # New detailed configuration to handle sub-folders (DTOs, Mapper) components = [ {"dir": "entity", "suffix": "Entity", "template": "Entity.java.j2"}, {"dir": "repository", "suffix": "Repository", "template": "Repository.java.j2"}, @@ -94,12 +94,12 @@ def generate_spring_resource(resource_name: str, fields_str: str): class_name = f"{entity_name}{comp['suffix']}" target_file_name = f"{class_name}.java" - # Création du sous-dossier s'il n'existe pas (ex: src/.../dto/request) - # os.path.normpath gère les slashes selon l'OS (Linux/Windows) + # Create sub-folder if it doesn't exist (e.g., src/.../dto/request) + # os.path.normpath handles slashes depending on the OS (Linux/Windows) target_dir = os.path.join(base_path, os.path.normpath(comp["dir"])) os.makedirs(target_dir, exist_ok=True) - # Les données envoyées au template Jinja2 + # Template data for Jinja2 context = { "base_package": base_package, "class_name": class_name, @@ -123,7 +123,7 @@ def generate_spring_security(_root_path: str = "."): """ Dynamically generates the JWT security base. """ - # Réutilisation de ta fonction de détection dynamique + # Reuse the dynamic detection function base_package, base_path = find_spring_base_package_and_path() if not base_package: @@ -133,7 +133,7 @@ def generate_spring_security(_root_path: str = "."): ) return - # Création du dossier config au bon endroit + # Create config folder in the right place target_dir = os.path.join(base_path, "config") os.makedirs(target_dir, exist_ok=True) diff --git a/devctl/generators/spring.py b/devctl/generators/spring.py index 4c05cc9..4c617cc 100644 --- a/devctl/generators/spring.py +++ b/devctl/generators/spring.py @@ -145,10 +145,10 @@ def download_spring_boilerplate(project_name: str, db_type: str = "postgres"): fg=typer.colors.CYAN, ) - # Règle Java : un nom de package ne peut pas contenir de tirets + # Java rule: a package name cannot contain dashes safe_package_name = project_name.replace("-", "").replace("_", "").lower() - # Mapping dynamique pour l'API Spring + # Dynamic mapping for the Spring API db_dependency = "postgresql" if db_type == "postgres" else "mysql" official_deps = [ "web", @@ -162,7 +162,7 @@ def download_spring_boilerplate(project_name: str, db_type: str = "postgres"): ] dependencies = ",".join(official_deps) - # Paramètres de l'API Spring Initializr + # Spring Initializr API parameters params = { "type": "maven-project", "language": "java", @@ -170,7 +170,7 @@ def download_spring_boilerplate(project_name: str, db_type: str = "postgres"): "groupId": "com.devctl", "artifactId": project_name, "name": project_name, - "description": "Projet Spring Boot généré par devctl", + "description": "Spring Boot project generated by devctl", "packageName": f"com.devctl.{safe_package_name}", "packaging": "jar", "javaVersion": "17", @@ -196,9 +196,9 @@ def download_spring_boilerplate(project_name: str, db_type: str = "postgres"): mvnw_path = os.path.join(project_path, "mvnw") if os.path.exists(mvnw_path): - # Récupère les droits actuels du fichier + # Get current file permissions st = os.stat(mvnw_path) - # Ajoute le droit d'exécution (stat.S_IEXEC) pour l'utilisateur courant + # Add execute permission (stat.S_IEXEC) for the current user os.chmod(mvnw_path, st.st_mode | stat.S_IEXEC) os.chdir(project_name) diff --git a/devctl/generators/vue.py b/devctl/generators/vue.py index e26994c..6bcbdcb 100644 --- a/devctl/generators/vue.py +++ b/devctl/generators/vue.py @@ -33,7 +33,7 @@ def setup_vue_router(project_path: str): typer.secho("🛣️ Installing and configuring vue-router...", fg=typer.colors.CYAN) try: - # 1. Installation du package npm + # 1. Install npm package subprocess.run( ["npm", "install", "vue-router@4"], cwd=project_path, @@ -41,12 +41,12 @@ def setup_vue_router(project_path: str): stdout=subprocess.DEVNULL, ) - # 2. Création du dossier router + # 2. Create router directory src_dir = os.path.join(project_path, "src") router_dir = os.path.join(src_dir, "router") os.makedirs(router_dir, exist_ok=True) - # 3. Rendu des templates Jinja2 + # 3. Render Jinja2 templates templates_dir = os.path.join(os.path.dirname(__file__), "..", "templates", "vue", "config") env = Environment(loader=FileSystemLoader(templates_dir)) @@ -85,7 +85,7 @@ def generate_vue_boilerplate(project_name: str) -> bool: typer.secho("⏳ Installing npm dependencies...", fg=typer.colors.CYAN) subprocess.run(["npm", "install"], cwd=project_full_path, check=True) - # --- APPEL DE NOS DEUX CONFIGURATEURS --- + # --- CALL OUR TWO CONFIGURATORS --- setup_vue_proxy(project_full_path) setup_vue_router(project_full_path) # ---------------------------------------- diff --git a/devctl/main.py b/devctl/main.py index 7117617..5511cfc 100644 --- a/devctl/main.py +++ b/devctl/main.py @@ -1,12 +1,12 @@ import typer -# Import des modules de commandes +# Import command modules from devctl.commands import add, docker, init, run -# Création de l'application Typer principale +# Create the main Typer application app = typer.Typer(help="devctl: Local orchestrator for your Spring/Angular projects") -# Câblage des sous-menus +# Register sub-menus app.add_typer(init.app, name="init", help="Initialize a new project with its codebase.") app.add_typer(run.app, name="run", help="Launch the local development environment in parallel.") app.add_typer(add.app, name="add", help="Generate code and business resources.") @@ -18,7 +18,7 @@ def callback(): """ devctl: Local orchestrator for your projects """ - # Ce callback vide permet à Typer de comprendre qu'il gère un menu multi-commandes + # This empty callback allows Typer to understand it's managing a multi-command menu pass @@ -32,7 +32,7 @@ def ping(): def main(): """ - Point d'entrée appelé par le système d'exploitation (via pyproject.toml) + Application entry point called by the OS (via pyproject.toml) """ app() diff --git a/devctl/orchestrator/config_builder.py b/devctl/orchestrator/config_builder.py index 9600380..80ce732 100644 --- a/devctl/orchestrator/config_builder.py +++ b/devctl/orchestrator/config_builder.py @@ -8,7 +8,7 @@ def generate_config(project_name: str, db_type: str = "postgres", custom_port: i template_dir = os.path.join(os.path.dirname(__file__), "..", "templates", "spring") env = Environment(loader=FileSystemLoader(template_dir)) - # Résolution intelligente du port par défaut + # Intelligent default port resolution if custom_port is None: db_port = 5432 if db_type == "postgres" else 3306 else: diff --git a/devctl/orchestrator/runner.py b/devctl/orchestrator/runner.py index 6f9d326..1a7fc74 100644 --- a/devctl/orchestrator/runner.py +++ b/devctl/orchestrator/runner.py @@ -7,7 +7,7 @@ def is_docker_running(): """ - Vérifie si le daemon Docker est actif sur le système. + Checks if the Docker daemon is active on the system. """ try: subprocess.run(["docker", "info"], capture_output=True, check=True) @@ -18,24 +18,24 @@ def is_docker_running(): def launch_dev_environment(env_state: dict): """ - Lance les processus nécessaires en parallèle (Optimisé pour Linux/Fedora). + Launches the necessary processes in parallel. """ processes = [] try: - # 4. Lancement du Frontend Vue.js + # 4. Start Vue.js Frontend if env_state.get("has_vue"): typer.secho( f"🟢 Starting Vue.js from {env_state['vue_path']}...", fg=typer.colors.GREEN ) p_vue = subprocess.Popen( - ["npm", "run", "dev"], # Commande standard de Vite + ["npm", "run", "dev"], # Standard Vite command cwd=env_state["vue_path"], ) processes.append(("Vue.js", p_vue)) - # 1. Lancement de la Base de données + # 1. Start Database if env_state["has_docker_compose"]: if not is_docker_running(): typer.secho("❌ Error: Docker service is not running.", fg=typer.colors.RED) @@ -50,24 +50,24 @@ def launch_dev_environment(env_state: dict): ) time.sleep(5) - # 2. Lancement du Backend Spring Boot + # 2. Start Spring Boot Backend if env_state["has_spring"]: typer.secho( f"🍃 Starting Spring Boot from {env_state['spring_path']}...", fg=typer.colors.GREEN, ) - # Exécution native Linux + # Native execution p_spring = subprocess.Popen(["./mvnw", "spring-boot:run"], cwd=env_state["spring_path"]) processes.append(("Spring Boot", p_spring)) - # 3. Lancement du Frontend Angular + # 3. Start Angular Frontend if env_state["has_angular"]: typer.secho( f"🅰️ Starting Angular from {env_state['angular_path']}...", fg=typer.colors.CYAN ) - # Exécution native Linux + # Native execution p_angular = subprocess.Popen(["npx", "ng", "serve"], cwd=env_state["angular_path"]) processes.append(("Angular", p_angular)) diff --git a/devctl/orchestrator/scanner.py b/devctl/orchestrator/scanner.py index 83315de..b660458 100644 --- a/devctl/orchestrator/scanner.py +++ b/devctl/orchestrator/scanner.py @@ -3,8 +3,8 @@ def detect_environment(root_path: str = "."): """ - Scanne le répertoire et ses sous-dossiers pour identifier les composants. - Retourne l'état et les chemins absolus de chaque composant. + Scans the directory and its subfolders to identify components. + Returns the state and absolute paths of each component. """ env_state = { "has_docker_compose": False, @@ -19,7 +19,7 @@ def detect_environment(root_path: str = "."): } for dirpath, _dirnames, filenames in os.walk(root_path): - # Optimisation : on ignore les dossiers lourds pour un scan instantané + # Optimization: ignore heavy folders for instant scan if any(ignored in dirpath for ignored in ["node_modules", "target", ".git", ".angular"]): continue diff --git a/samples/sample-api/Dockerfile b/samples/sample-api/Dockerfile new file mode 100644 index 0000000..8f0e76b --- /dev/null +++ b/samples/sample-api/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1.7 +# Generated by devctl dockerize. +# Production Spring Boot image: Maven build stage + lean non-root JRE runtime. + +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /workspace + +COPY . . +RUN if [ -f mvnw ]; then chmod +x mvnw && ./mvnw -B -ntp clean package -DskipTests; else mvn -B -ntp clean package -DskipTests; fi +RUN JAR_FILE="$(find target -maxdepth 1 -type f -name '*.jar' ! -name '*sources.jar' ! -name '*javadoc.jar' | head -n 1)" \ + && test -n "$JAR_FILE" \ + && cp "$JAR_FILE" /tmp/app.jar + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app + +RUN addgroup -S app && adduser -S app -G app +COPY --from=build /tmp/app.jar /app/app.jar + +USER app +EXPOSE 8080 +ENV JAVA_OPTS="" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"] diff --git a/samples/sample-front/Dockerfile b/samples/sample-front/Dockerfile new file mode 100644 index 0000000..959b004 --- /dev/null +++ b/samples/sample-front/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1.7 +# Generated by devctl dockerize. +# Angular image: Node build stage + Nginx static runtime. + +FROM node:22-alpine AS build +WORKDIR /app + +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi + +COPY . . +RUN npm run build \ + && mkdir -p /tmp/devctl-dist \ + && if [ -d "dist/sample-front/browser" ]; then cp -r "dist/sample-front/browser/." /tmp/devctl-dist/; \ + elif [ -d "dist/sample-front" ]; then cp -r "dist/sample-front/." /tmp/devctl-dist/; \ + elif [ -d "dist" ]; then FIRST_DIST_DIR="$(find dist -mindepth 1 -maxdepth 2 -type d | head -n 1)" && test -n "$FIRST_DIST_DIR" && cp -r "$FIRST_DIST_DIR/." /tmp/devctl-dist/; \ + else echo "Unable to find Angular build output under dist/" >&2; exit 1; fi + +FROM nginx:1.27-alpine + +COPY --from=build /tmp/devctl-dist/ /usr/share/nginx/html/ + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] From a69b91af78c685fbea694656a07d668e87b9c3a1 Mon Sep 17 00:00:00 2001 From: AnouarMohamed Date: Mon, 25 May 2026 13:19:32 +0100 Subject: [PATCH 2/2] style: fix ruff formatting --- devctl/generators/angular.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/devctl/generators/angular.py b/devctl/generators/angular.py index b00f644..5ab9799 100644 --- a/devctl/generators/angular.py +++ b/devctl/generators/angular.py @@ -34,9 +34,7 @@ def setup_angular_environments(project_path: str): with open(target_path, "w", encoding="utf-8") as f: f.write(content) except Exception as e: - typer.secho( - f"⚠️ Error while generating {tpl_name}: {e}", fg=typer.colors.YELLOW - ) + typer.secho(f"⚠️ Error while generating {tpl_name}: {e}", fg=typer.colors.YELLOW) # 3. Modify angular.json to enable the proxy angular_json_path = os.path.join(project_path, "angular.json")