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/init.py b/devctl/commands/init.py index 2ccd6b4..2e1b273 100644 --- a/devctl/commands/init.py +++ b/devctl/commands/init.py @@ -6,7 +6,7 @@ import typer -# Angular generator +# Angular Generator from devctl.generators.angular import generate_angular_boilerplate from devctl.generators.django import generate_django_boilerplate from devctl.generators.fastapi import generate_fastapi_boilerplate @@ -16,14 +16,14 @@ from devctl.generators.nodejs import generate_nodejs_boilerplate from devctl.generators.react import generate_react_boilerplate -# Spring generator +# Spring Generator from devctl.generators.spring import download_spring_boilerplate from devctl.generators.svelte import generate_svelte_boilerplate from devctl.generators.vue import generate_vue_boilerplate from devctl.orchestrator.config_builder import generate_config from devctl.utils.dependencies import check_tool -# Local Typer application for "init" command group +# Local Typer app for the "init" command group app = typer.Typer(help="Initializes a new project based on the chosen framework.") diff --git a/devctl/commands/run.py b/devctl/commands/run.py index 0ecb89b..6fb5e98 100644 --- a/devctl/commands/run.py +++ b/devctl/commands/run.py @@ -63,5 +63,5 @@ def get_status(condition: bool): typer.secho("\nError: No valid development environment detected here.", fg=typer.colors.RED) raise typer.Exit(code=1) - # Transfer control to the system orchestration layer - launch_dev_environment(projects, docker_composes) + # Hand off to the system orchestration layer + launch_dev_environment(env_state) diff --git a/devctl/generators/angular.py b/devctl/generators/angular.py index 977f1be..f3b6755 100644 --- a/devctl/generators/angular.py +++ b/devctl/generators/angular.py @@ -39,7 +39,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"Warning: Failed to generate {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") @@ -48,10 +48,10 @@ def setup_angular_environments(project_path: str): angular_config = json.load(f) try: - # Find the default project name (usually the same name as the folder) + # Find the default project name (usually the same as the folder name) project_name = list(angular_config["projects"].keys())[0] - # Injection of proxyConfig into the "serve" architect + # Inject proxyConfig into the "serve" architect serve_target = angular_config["projects"][project_name]["architect"]["serve"] # Ensure "options" exists @@ -66,7 +66,7 @@ def setup_angular_environments(project_path: str): typer.echo(" - angular.json updated with proxyConfig.") except Exception as e: typer.secho( - f"Warning: Unable to modify angular.json automatically: {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 52e3793..7cf9864 100644 --- a/devctl/generators/scaffold_angular.py +++ b/devctl/generators/scaffold_angular.py @@ -38,7 +38,7 @@ def parse_ts_fields(fields_str: str): def generate_angular_resource(resource_name: str, fields_str: str, root_path: str = "."): """ - Orchestrates the creation of a complete Angular feature. + Orchestrates the creation of the complete Angular feature. """ env_state = detect_environment(root_path) @@ -50,10 +50,10 @@ def generate_angular_resource(resource_name: str, fields_str: str, root_path: st resource_lower = resource_name.lower() entity_name = resource_name.capitalize() - # Feature target directory: src/app/features/resource_name + # The target feature directory: src/app/features/produit feature_dir = os.path.join(angular_root, "src", "app", "features", resource_lower) - # Components to generate + # Component configuration to generate components = [ # Models { diff --git a/devctl/generators/scaffold_spring.py b/devctl/generators/scaffold_spring.py index 39ce0b3..5324622 100644 --- a/devctl/generators/scaffold_spring.py +++ b/devctl/generators/scaffold_spring.py @@ -22,7 +22,7 @@ def parse_fields(fields_str: str): """ - Transforms terminal field strings into injectable data. + Transforms terminal string into injectable data. Example: "name:string, age:int" -> [{"name": "name", "java_type": "String"}, ...] """ if not fields_str: @@ -42,8 +42,8 @@ def parse_fields(fields_str: str): def find_spring_base_package_and_path(): """ - Searches for the directory containing the @SpringBootApplication class. - This is the most reliable way to find the base package without parsing 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") @@ -62,7 +62,7 @@ def find_spring_base_package_and_path(): def generate_spring_resource(resource_name: str, fields_str: str): """ - Orchestrates the creation of MVC + DTOs + Mapper architecture. + Orchestrates the creation of the MVC + DTOs + Mapper architecture. """ base_package, base_path = find_spring_base_package_and_path() @@ -75,7 +75,7 @@ def generate_spring_resource(resource_name: str, fields_str: str): entity_name = resource_name.capitalize() - # Detailed configuration for sub-folders (DTOs, Mappers) + # 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"}, @@ -100,11 +100,11 @@ def generate_spring_resource(resource_name: str, fields_str: str): target_file_name = f"{class_name}.java" # Create sub-folder if it doesn't exist (e.g., src/.../dto/request) - # os.path.normpath handles OS-specific slashes + # 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) - # Data passed to the Jinja2 template + # Template data for Jinja2 context = { "base_package": base_package, "class_name": class_name, diff --git a/devctl/generators/spring.py b/devctl/generators/spring.py index be8e17b..46730fb 100644 --- a/devctl/generators/spring.py +++ b/devctl/generators/spring.py @@ -150,7 +150,7 @@ def download_spring_boilerplate(project_name: str, db_type: str = "postgres"): fg=typer.colors.CYAN, ) - # Java rule: a package name cannot contain hyphens + # Java rule: a package name cannot contain dashes safe_package_name = project_name.replace("-", "").replace("_", "").lower() # Dynamic mapping for the Spring API diff --git a/devctl/generators/vue.py b/devctl/generators/vue.py index dcd7280..15941ee 100644 --- a/devctl/generators/vue.py +++ b/devctl/generators/vue.py @@ -38,7 +38,7 @@ def setup_vue_router(project_path: str): typer.secho("Installing and configuring vue-router...", fg=typer.colors.CYAN) try: - # 1. NPM package installation + # 1. Install npm package subprocess.run( ["npm", "install", "vue-router@4"], cwd=project_path, @@ -46,12 +46,12 @@ def setup_vue_router(project_path: str): stdout=subprocess.DEVNULL, ) - # 2. Router directory creation + # 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. Jinja2 template rendering + # 3. Render Jinja2 templates templates_dir = os.path.join(os.path.dirname(__file__), "..", "templates", "vue", "config") env = Environment(loader=FileSystemLoader(templates_dir)) diff --git a/devctl/main.py b/devctl/main.py index b02f56b..19cca05 100644 --- a/devctl/main.py +++ b/devctl/main.py @@ -1,12 +1,12 @@ import typer # Import command modules -from devctl.commands import add, deploy, docker, init, run +from devctl.commands import add, docker, init, run # Create the main Typer application app = typer.Typer(help="devctl: Local orchestrator for your Spring/Angular projects") -# Register sub-commands +# 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.") @@ -22,7 +22,7 @@ def callback(): """ devctl: Local orchestrator for your projects """ - # This empty callback allows Typer to understand it handles a multi-command menu + # This empty callback allows Typer to understand it's managing a multi-command menu pass @@ -36,7 +36,7 @@ def ping(): def main(): """ - Entry point called by the operating system (via pyproject.toml) + Application entry point called by the OS (via pyproject.toml) """ app() diff --git a/devctl/orchestrator/runner.py b/devctl/orchestrator/runner.py index cf38eeb..d7fe913 100644 --- a/devctl/orchestrator/runner.py +++ b/devctl/orchestrator/runner.py @@ -47,7 +47,7 @@ def stream_logs(name: str, process: subprocess.Popen, color: str): def launch_dev_environment(projects: List[DockerProject], docker_composes: List[Path]): """ - Launches the necessary processes in parallel with structured startup and log streaming. + Launches the necessary processes in parallel. """ global active_processes @@ -59,58 +59,8 @@ def signal_handler(_sig, _frame): signal.signal(signal.SIGTERM, signal_handler) try: - # 1. Start Databases - if docker_composes: - if not is_docker_running(): - typer.secho("Error: Docker service is not running.", fg=typer.colors.RED) - sys.exit(1) - - 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) - - typer.echo("Waiting 5s for databases to initialize...") - time.sleep(5) - - # 2. Start Backends (Spring Boot) - 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), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - 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"] - ] - for p in frontends: - if p.kind == "angular": - color = "cyan" - cmd = ["npx", "ng", "serve"] - elif p.kind == "vue": - color = "magenta" - cmd = ["npm", "run", "dev"] - elif p.kind == "react": - color = "blue" - cmd = ["npm", "run", "dev"] - elif p.kind == "nextjs": - color = "yellow" - cmd = ["npm", "run", "dev"] - else: # svelte - color = "red" - cmd = ["npm", "run", "dev"] - + # 4. Start Vue.js Frontend + if env_state.get("has_vue"): typer.secho( f"Starting {p.kind.capitalize()}: {p.name}...", fg=getattr(typer.colors, color.upper()), @@ -125,9 +75,12 @@ def signal_handler(_sig, _frame): ) 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) + # 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) + typer.echo("💡 Hint: sudo systemctl start docker") + sys.exit(1) # 4. Start NestJS Backends nest_apps = [p for p in projects if p.kind == "nest"] @@ -161,71 +114,28 @@ def signal_handler(_sig, _frame): ) 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) - - # 6. Start FastAPI Backends - 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), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - 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) - - # 7. Start Django Backends - 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), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, + # 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, ) 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) - - # 8. Start Go Backends - 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) + # Native execution + p_spring = subprocess.Popen(["./mvnw", "spring-boot:run"], cwd=env_state["spring_path"]) + processes.append(("Spring Boot", p_spring)) - proc = subprocess.Popen( - ["go", "run", "."], - cwd=str(p.path), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, + # 3. Start Angular Frontend + if env_state["has_angular"]: + typer.secho( + f"🅰️ Starting Angular from {env_state['angular_path']}...", fg=typer.colors.CYAN ) 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) + # Native execution + p_angular = subprocess.Popen(["npx", "ng", "serve"], cwd=env_state["angular_path"]) + processes.append(("Angular", p_angular)) if not active_processes and not docker_composes: typer.secho( diff --git a/devctl/orchestrator/scanner.py b/devctl/orchestrator/scanner.py index d65bbba..e4b5c9e 100644 --- a/devctl/orchestrator/scanner.py +++ b/devctl/orchestrator/scanner.py @@ -58,12 +58,10 @@ def detect_environment(root_path: str = "."): "project_root": str(root), } - for dirpath, dirnames, filenames in os.walk(root): - # In-place modification of dirnames to prune the traversal - dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRECTORIES] - - current_path = Path(dirpath) - filename_set = set(filenames) + for dirpath, _dirnames, filenames in os.walk(root_path): + # Optimization: ignore heavy folders for instant scan + if any(ignored in dirpath for ignored in ["node_modules", "target", ".git", ".angular"]): + continue # 1. Docker Compose detection if "docker-compose.yml" in filename_set and not env_state["has_docker_compose"]: 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;"]