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/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..80e4170
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+# Contributing to devctl
+
+Thank you for your interest in contributing to `devctl`! We welcome contributions from the community to make this tool even better.
+
+## How to Contribute
+
+1. **Report Bugs**: If you find a bug, please open an issue on GitHub with a detailed description and steps to reproduce it.
+2. **Suggest Features**: Have an idea for a new feature? Open an issue to discuss it.
+3. **Submit Pull Requests**:
+ * Fork the repository.
+ * Create a new branch for your feature or bugfix.
+ * Ensure your code follows the project's style (we use [Ruff](https://github.com/astral-sh/ruff)).
+ * Add tests for any new functionality.
+ * Submit a PR with a clear description of your changes.
+
+## Development Setup
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/your-username/devctl.git
+ cd devctl
+ ```
+2. Create a virtual environment:
+ ```bash
+ python -m venv .venv
+ source .venv/bin/activate
+ ```
+3. Install dependencies in editable mode:
+ ```bash
+ pip install -e .
+ ```
+4. Run tests:
+ ```bash
+ pytest
+ ```
+
+## Code Quality
+
+We use `ruff` for linting and formatting. You can run it with:
+```bash
+ruff check .
+ruff format .
+```
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT License.
diff --git a/README.md b/README.md
index 8a6e5b5..61742f5 100644
--- a/README.md
+++ b/README.md
@@ -1,126 +1,86 @@
# Devctl
-`devctl` is a command-line interface (CLI) that automates and orchestrates the
-local development lifecycle for Spring Boot, Angular, and Vue.js
-architectures.
+`devctl` is a professional command-line interface (CLI) designed to automate and
+orchestrate the development lifecycle for modern full-stack architectures. It
+provides a unified workflow for managing backends (Spring Boot, NestJS, Go,
+FastAPI), frontends (Angular, Vue, React, NextJS, Svelte), and their
+containerization.
+
+## Core Features
+
+- **Multi-Stack Orchestration**: Launch databases, multiple microservices, and
+ frontend dev servers with a single command. Includes real-time, prefixed log
+ streaming.
+- **Full-Stack Scaffolding**: Generate consistent business resources
+ (Entities, DTOs, Controllers, Services) across both backend and frontend
+ layers simultaneously.
+- **Instant Infrastructure**: Automatically generate optimized, multi-stage
+ `Dockerfiles` and global `docker-compose.yml` configurations by scanning
+ your project tree.
+- **Security by Default**: Inject standardized JWT authentication and security
+ configurations into new projects.
-## Purpose
-
-Full-stack development involves repetitive configuration and environment
-management tasks. `devctl` provides a unified control point to automate these
-processes, allowing you to focus on implementation rather than infrastructure.
-
-The tool addresses the following challenges:
-- Standardized database configuration using Docker.
-- Automated security boilerplate generation (JWT, filters, and configuration).
-- Surgical CRUD layer generation (entities, repositories, services, and
- controllers).
-- Concurrent process management for multi-tier applications.
-- Automated environment cleanup.
+## Installation
-## Architecture and integration
+You can install `devctl` locally for development:
-The tool uses a modular Python 3 architecture and industry-standard libraries:
+```bash
+git clone https://github.com/yss-ef/devctl.git
+cd devctl
+pip install -e .
+```
-- CLI engine: Typer-based interface for type-safe command execution.
-- Templating: Jinja2 for dynamic generation of Java, TypeScript, and
- configuration assets.
-- Spring integration: Connects with the Spring Initializr API for project
- bootstrapping.
-- Frontend orchestration: Native support for @angular/cli and Vite.
-- Orchestration engine: Recursive scanning for project detection and parallel
- process management.
-- Container management: Lifecycle management for Docker Compose, including
- volume persistence control.
+## Quick Start
-## System requirements
+### 1. Initialize a Full-Stack Project
-The following dependencies must be in the system path:
+Create a new backend and frontend in seconds:
-- Python: version 3.9 or later.
-- Docker and Docker Compose.
-- Java: version 17 or later for Spring Boot modules.
-- Node.js and npm for Angular and Vue.js modules.
-- Angular CLI for Angular project initialization.
+```bash
+mkdir my-app && cd my-app
+devctl init spring api --db postgres
+devctl init angular front
+```
-## Installation
+### 2. Add a Business Resource
-1. Clone the repository:
- ```bash
- git clone https://github.com/your-username/devctl.git
- cd devctl
- ```
-2. Configure a virtual environment:
- ```bash
- python -m venv .venv
- source .venv/bin/activate # Linux/macOS
- ```
-3. Install the package:
- ```bash
- pip install -e .
- ```
-4. Verify the installation:
- ```bash
- devctl ping
- ```
-
-## Command reference
-
-### Project initialization
-
-Bootstrap new projects with pre-configured defaults and security standards.
-
-- Spring Boot:
- ```bash
- devctl init spring "api-service" --db [postgres|mysql] --port 5432
- ```
-- Angular:
- ```bash
- devctl init angular "web-client"
- ```
-- Vue.js:
- ```bash
- devctl init vue "vue-client"
- ```
-
-### Resource scaffolding
-
-Inject business resources into existing project structures. The command detects
-active modules and updates both backend and frontend layers.
+Generate a complete vertical slice of your application:
```bash
-devctl add resource "Product" --fields "name:string,price:double,quantity:int"
+devctl add resource Product -f "name:string,price:double,quantity:int"
```
-### Orchestration
+### 3. Run the Environment
-Scan the current directory tree and launch all detected components (database,
-backend, and frontend) in parallel.
+Launch everything in parallel with automatic database startup:
```bash
devctl run
```
-Upon termination (Ctrl+C), `devctl` stops all processes and performs a clean
-teardown of Docker resources.
+## Supported Stacks
-### Dockerfile scaffolding
+| Type | Frameworks / Technologies |
+| :--- | :--- |
+| **Backends** | Spring Boot, NestJS, Express (NodeJS), FastAPI, Django, Go (Fiber) |
+| **Frontends** | Angular, Vue.js, ReactJS, NextJS, Svelte |
+| **Databases** | PostgreSQL, MySQL, MongoDB |
-Generate Dockerfiles for detected Spring Boot, Angular, and Vue/Vite services.
+## Documentation
-```bash
-devctl dockerize
-devctl dockerize ./my-workspace --dry-run
-devctl dockerize --force
-```
+For a detailed reference of all available commands and their options, see the
+[Commands Reference](COMMANDS.md).
+
+## Contributing
-Generated assets are limited to service-local `Dockerfile` files. Existing
-files are skipped unless you use the `--force` flag.
+Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for
+development setup and code quality guidelines.
## License
-This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
for details.
+---
Authored by Youssef Fellah.
-Personal project.
+Professional Development Orchestrator.
diff --git a/devctl/commands/add.py b/devctl/commands/add.py
index d2c0d3f..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, and Vue.js scaffolding.
+Supports Spring Boot, Angular, Vue.js, NestJS, NodeJS, React, NextJS, FastAPI,
+Django, Svelte, and Go scaffolding.
"""
import os
@@ -8,7 +9,15 @@
import typer
from devctl.generators.scaffold_angular import generate_angular_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_spring import generate_spring_resource
+from devctl.generators.scaffold_svelte import generate_svelte_resource
from devctl.generators.scaffold_vue import generate_vue_resource
from devctl.orchestrator.scanner import detect_environment
from devctl.utils.dependencies import check_tool
@@ -26,7 +35,7 @@ def resource(
"""
Scans the current folder and generates a suitable business architecture.
"""
- typer.secho("š Analyzing current context...", fg=typer.colors.CYAN)
+ typer.secho("Analyzing current context...", fg=typer.colors.CYAN)
env_state = detect_environment(".")
original_dir = os.getcwd()
@@ -37,13 +46,13 @@ def resource(
check_tool("java", "generating Spring Boot resources")
project_detected = True
typer.secho(
- "š Spring Boot project detected. Launching Java generator...", fg=typer.colors.GREEN
+ "Spring Boot project detected. Launching Java generator...", fg=typer.colors.GREEN
)
os.chdir(env_state["spring_path"])
try:
generate_spring_resource(name, fields)
except Exception as e:
- typer.secho(f"ā Error during Spring generation: {e}", fg=typer.colors.RED)
+ typer.secho(f"Error: Spring generation failed: {e}", fg=typer.colors.RED)
finally:
os.chdir(original_dir)
@@ -52,28 +61,106 @@ def resource(
check_tool("npm", "generating Angular resources")
project_detected = True
typer.secho(
- "š
°ļø Angular project detected. Launching TypeScript generator...", fg=typer.colors.CYAN
+ "Angular project detected. Launching TypeScript generator...", fg=typer.colors.CYAN
)
try:
generate_angular_resource(name, fields, root_path=".")
except Exception as e:
- typer.secho(f"ā Error during Angular generation: {e}", fg=typer.colors.RED)
+ typer.secho(f"Error: Angular generation failed: {e}", fg=typer.colors.RED)
# Check for Vue.js project
if env_state.get("has_vue"):
check_tool("npm", "generating Vue.js resources")
project_detected = True
- typer.secho("š¢ Vue.js project detected. Launching Vue generator...", fg=typer.colors.GREEN)
+ typer.secho("Vue.js project detected. Launching Vue generator...", fg=typer.colors.GREEN)
try:
generate_vue_resource(name, fields, root_path=".")
except Exception as e:
- typer.secho(f"ā Error during Vue generation: {e}", fg=typer.colors.RED)
+ typer.secho(f"Error: Vue generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for NestJS project
+ if env_state.get("has_nest"):
+ project_detected = True
+ typer.secho("NestJS project detected. Launching Nest generator...", fg=typer.colors.CYAN)
+ try:
+ generate_nest_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: Nest generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for React project
+ if env_state.get("has_react"):
+ project_detected = True
+ typer.secho("React project detected. Launching React generator...", fg=typer.colors.BLUE)
+ try:
+ generate_react_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: React generation failed: {e}", fg=typer.colors.RED)
+
+ # 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
+ )
+ try:
+ generate_nextjs_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: NextJS generation failed: {e}", fg=typer.colors.RED)
+
+ # 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
+ )
+ try:
+ generate_fastapi_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: FastAPI generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for Django project
+ if env_state.get("has_django"):
+ project_detected = True
+ typer.secho("Django project detected. Launching Django generator...", fg=typer.colors.GREEN)
+ try:
+ generate_django_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: Django generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for Svelte project
+ if env_state.get("has_svelte"):
+ project_detected = True
+ typer.secho("Svelte project detected. Launching Svelte generator...", fg=typer.colors.RED)
+ try:
+ generate_svelte_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: Svelte generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for Go project
+ if env_state.get("has_go"):
+ project_detected = True
+ typer.secho("Go project detected. Launching Go generator...", fg=typer.colors.CYAN)
+ try:
+ generate_go_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: Go generation failed: {e}", fg=typer.colors.RED)
+
+ # Check for NodeJS project
+ if os.path.exists("package.json") and not project_detected:
+ # Heuristic for generic nodejs project if not already caught by others
+ project_detected = True
+ typer.secho("NodeJS project detected. Launching NodeJS generator...", fg=typer.colors.GREEN)
+ try:
+ generate_nodejs_resource(name, fields, root_path=".")
+ except Exception as e:
+ typer.secho(f"Error: NodeJS generation failed: {e}", fg=typer.colors.RED)
# Error message only if NO project detected
if not project_detected:
typer.secho(
- "ā Unable to determine project type. "
- "Please run from within a Spring, Angular, or Vue.js project directory.",
+ "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).",
fg=typer.colors.RED,
)
raise typer.Exit(code=1)
diff --git a/devctl/commands/deploy.py b/devctl/commands/deploy.py
index 06ed213..cece1cb 100644
--- a/devctl/commands/deploy.py
+++ b/devctl/commands/deploy.py
@@ -3,6 +3,7 @@
from typing import Any, Dict, Optional
import typer
+import yaml
from jinja2 import Environment, FileSystemLoader
from devctl.generators.docker_scaffold import (
@@ -30,81 +31,156 @@ 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
+ )
+
user_match = re.search(r"spring\.datasource\.username=([\w-]+)", content)
pass_match = re.search(r"spring\.datasource\.password=([\w-]+)", content)
- if not url_match:
+ if not url_match and not mongo_match:
return extract_db_from_compose(project_path / "docker-compose.yml")
- db_type_raw = url_match.group(1)
- db_type = "postgresql" if "postgres" in db_type_raw else "mysql"
- db_port = url_match.group(2)
- db_name = url_match.group(3)
- db_user = user_match.group(1) if user_match else "admin"
- db_pass = pass_match.group(1) if pass_match else "password"
+ if mongo_match:
+ db_type = "mongodb"
+ db_user = mongo_match.group(1)
+ db_pass = mongo_match.group(2)
+ db_port = mongo_match.group(3)
+ db_name = mongo_match.group(4)
+ else:
+ db_type_raw = url_match.group(1)
+ db_type = "postgresql" if "postgres" in db_type_raw else "mysql"
+ db_port = url_match.group(2)
+ db_name = url_match.group(3)
+ db_user = user_match.group(1) if user_match else "admin"
+ 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():
- compose_content = compose_path.read_text(encoding="utf-8", errors="ignore")
- service_match = re.search(r"services:\s*\n\s+([\w-]+):", compose_content)
- if service_match:
- db_dict["service_name"] = service_match.group(1)
+ try:
+ with open(compose_path, "r", encoding="utf-8") as f:
+ config = yaml.safe_load(f)
+ if config and "services" in config:
+ # Find the first service that looks like a database
+ for s_name, s_cfg in config["services"].items():
+ image = str(s_cfg.get("image", ""))
+ if db_type == "postgresql" and "postgres" in image:
+ db_dict["service_name"] = s_name
+ break
+ if db_type == "mysql" and "mysql" in image:
+ db_dict["service_name"] = s_name
+ break
+ if db_type == "mongodb" and "mongo" in image:
+ db_dict["service_name"] = s_name
+ break
+ except Exception:
+ pass
return db_dict
def extract_db_from_compose(compose_path: Path) -> Optional[Dict[str, Any]]:
"""
- Extract database information from a docker-compose.yml file using regex.
+ Extract database information from a docker-compose.yml file using PyYAML.
"""
if not compose_path.exists():
return None
- content = compose_path.read_text(encoding="utf-8", errors="ignore")
-
- # This is a bit hacky without PyYAML but follows devctl's generated structure
- # Try to find the first service name under 'services:'
- service_match = re.search(r"services:\s*\n\s+([\w-]+):", content)
- original_service_name = service_match.group(1) if service_match else None
-
- image_match = re.search(r"image:\s+(postgres|mysql)(:[\w.-]+)?", content)
- if not image_match:
+ try:
+ with open(compose_path, "r", encoding="utf-8") as f:
+ config = yaml.safe_load(f)
+ except Exception:
return None
- db_type = "postgresql" if image_match.group(1) == "postgres" else "mysql"
-
- # Try to find env vars
- if db_type == "postgresql":
- user = re.search(r"POSTGRES_USER:\s+([\w-]+)", content)
- password = re.search(r"POSTGRES_PASSWORD:\s+([\w-]+)", content)
- db_name = re.search(r"POSTGRES_DB:\s+([\w-]+)", content)
- else:
- user = re.search(r"MYSQL_USER:\s+([\w-]+)", content)
- password = re.search(r"MYSQL_PASSWORD:\s+([\w-]+)", content)
- db_name = re.search(r"MYSQL_DATABASE:\s+([\w-]+)", content)
-
- port_match = re.search(r"\"(\d+):(\d+)\"", content)
-
- db_dict = _build_db_dict(
- db_type,
- port_match.group(1) if port_match else ("5432" if db_type == "postgresql" else "3306"),
- db_name.group(1) if db_name else "db",
- user.group(1) if user else "admin",
- password.group(1) if password else "password",
- )
+ if not config or "services" not in config:
+ return None
- if original_service_name:
- db_dict["service_name"] = original_service_name
+ for service_name, service_cfg in config["services"].items():
+ image = str(service_cfg.get("image", ""))
+ if "postgres" in image or "mysql" in image or "mongo" in image:
+ if "postgres" in image:
+ db_type = "postgresql"
+ elif "mysql" in image:
+ db_type = "mysql"
+ else:
+ db_type = "mongodb"
+
+ # Extract environment variables
+ env = service_cfg.get("environment", {})
+ env_dict = {}
+ if isinstance(env, list):
+ for item in env:
+ if "=" in item:
+ k, v = item.split("=", 1)
+ env_dict[k] = v
+ elif ":" in item:
+ k, v = item.split(":", 1)
+ env_dict[k] = v.strip()
+ elif isinstance(env, dict):
+ env_dict = env
+
+ # Extract info based on db_type
+ if db_type == "postgresql":
+ user = env_dict.get("POSTGRES_USER", "admin")
+ password = env_dict.get("POSTGRES_PASSWORD", "password")
+ 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")
+ )
+ db_name = env_dict.get("MYSQL_DATABASE", "db")
+ else:
+ user = env_dict.get("MONGO_INITDB_ROOT_USERNAME", "admin")
+ password = env_dict.get("MONGO_INITDB_ROOT_PASSWORD", "password")
+ db_name = env_dict.get("MONGO_INITDB_DATABASE", "db")
+
+ # Extract port
+ ports = service_cfg.get("ports", [])
+ host_port = None
+ if ports and isinstance(ports, list):
+ first_port = str(ports[0])
+ if ":" in first_port:
+ host_port = first_port.split(":")[0].strip("'").strip('"')
+
+ db_dict = _build_db_dict(
+ db_type,
+ host_port
+ or (
+ "5432"
+ if db_type == "postgresql"
+ else ("3306" if db_type == "mysql" else "27017")
+ ),
+ db_name,
+ user,
+ password,
+ )
+ db_dict["service_name"] = service_name
+ return db_dict
- return db_dict
+ return None
def _build_db_dict(db_type: str, port: str, name: str, user: str, password: str) -> Dict[str, Any]:
is_postgres = db_type == "postgresql"
- internal_port = "5432" if is_postgres else "3306"
+ is_mysql = db_type == "mysql"
+
+ if is_postgres:
+ internal_port = "5432"
+ image = "postgres:15-alpine"
+ vol_path = "/var/lib/postgresql/data"
+ elif is_mysql:
+ internal_port = "3306"
+ image = "mysql:8.0"
+ vol_path = "/var/lib/mysql/data"
+ else:
+ internal_port = "27017"
+ image = "mongo:6.0"
+ vol_path = "/data/db"
env = {}
if is_postgres:
@@ -113,13 +189,19 @@ def _build_db_dict(db_type: str, port: str, name: str, user: str, password: str)
"POSTGRES_PASSWORD": password,
"POSTGRES_DB": name,
}
- else:
+ elif is_mysql:
env = {
"MYSQL_ROOT_PASSWORD": password,
"MYSQL_DATABASE": name,
"MYSQL_USER": user,
"MYSQL_PASSWORD": password,
}
+ else:
+ env = {
+ "MONGO_INITDB_ROOT_USERNAME": user,
+ "MONGO_INITDB_ROOT_PASSWORD": password,
+ "MONGO_INITDB_DATABASE": name,
+ }
return {
"type": db_type,
@@ -129,9 +211,9 @@ def _build_db_dict(db_type: str, port: str, name: str, user: str, password: str)
"user": user,
"password": password,
"service_name": f"{name}-db",
- "image": "postgres:15-alpine" if is_postgres else "mysql:8.0",
+ "image": image,
"volume_name": f"{name}_data",
- "volume_path": "/var/lib/postgresql/data" if is_postgres else "/var/lib/mysql/data",
+ "volume_path": vol_path,
"env": env,
}
@@ -141,16 +223,18 @@ def deploy(path: Path = PATH_ARGUMENT):
"""
Generate a global docker-compose.yml by scanning subdirectories.
"""
- typer.secho(f"š Preparing deployment for {path.resolve()}...", fg=typer.colors.CYAN, bold=True)
+ typer.secho(f"Preparing deployment for {path.resolve()}...", fg=typer.colors.CYAN, bold=True)
try:
projects = discover_docker_projects(path)
except Exception as e:
- typer.secho(f"ā Error scanning projects: {e}", fg=typer.colors.RED)
+ typer.secho(f"Error: Scanning failed: {e}", fg=typer.colors.RED)
raise typer.Exit(1) from e
if not projects:
- typer.secho("ā 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 = []
@@ -161,7 +245,7 @@ def deploy(path: Path = PATH_ARGUMENT):
dockerfile_path = project.path / "Dockerfile"
if not dockerfile_path.exists():
typer.secho(
- f"ā ļø Warning: No Dockerfile found in {project.path}. "
+ f"Warning: No Dockerfile found in {project.path}. "
"You may need to run 'devctl dockerize' first.",
fg=typer.colors.YELLOW,
)
@@ -199,5 +283,5 @@ def deploy(path: Path = PATH_ARGUMENT):
output_path = path / "docker-compose.yml"
output_path.write_text(output, encoding="utf-8")
- typer.secho(f"ā
Generated {output_path}", fg=typer.colors.GREEN, bold=True)
+ typer.secho(f"Generated {output_path}", fg=typer.colors.GREEN, bold=True)
typer.echo(f"Summary: {len(services_data)} services, {len(databases)} databases.")
diff --git a/devctl/commands/docker.py b/devctl/commands/docker.py
index 989916b..8faa8f5 100644
--- a/devctl/commands/docker.py
+++ b/devctl/commands/docker.py
@@ -43,11 +43,11 @@ def dockerize(
dry_run=dry_run,
)
except DockerScaffoldError as exc:
- typer.secho(f"ā {exc}", fg=typer.colors.RED)
+ typer.secho(f"Error: {exc}", fg=typer.colors.RED)
raise typer.Exit(code=1) from exc
mode = "Dry run" if dry_run else "Docker scaffolding"
- typer.secho(f"š³ {mode} complete for {result.root_path}", fg=typer.colors.CYAN, bold=True)
+ typer.secho(f"{mode} complete for {result.root_path}", fg=typer.colors.CYAN, bold=True)
typer.echo("\nDetected services:")
for service in result.services:
diff --git a/devctl/commands/init.py b/devctl/commands/init.py
index bc819fe..2ccd6b4 100644
--- a/devctl/commands/init.py
+++ b/devctl/commands/init.py
@@ -1,15 +1,24 @@
"""
CLI command group for initializing new projects.
-Supports Spring Boot, Angular, and Vue.js.
+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
+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
# 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
@@ -21,7 +30,7 @@
@app.command("spring")
def init_spring(
name: str,
- db: str = typer.Option("postgres", help="Database type (postgres or mysql)"),
+ db: str = typer.Option("postgres", help="Database type (postgres, mysql, or mongodb)"),
port: int = typer.Option(None, help="Local port (optional)"),
):
"""
@@ -30,17 +39,17 @@ def init_spring(
check_tool("java", "initializing a Spring Boot project")
# Strict input validation
- if db not in ["postgres", "mysql"]:
- typer.secho(f"ā Error: Database '{db}' is not supported.", fg=typer.colors.RED)
+ if db not in ["postgres", "mysql", "mongodb"]:
+ typer.secho(f"Error: Database '{db}' is not supported.", fg=typer.colors.RED)
raise typer.Exit(code=1)
- typer.secho(f"š Initializing Spring Boot project: '{name}'...", fg=typer.colors.CYAN)
+ typer.secho(f"Initializing Spring Boot project: '{name}'...", fg=typer.colors.CYAN)
success_download = download_spring_boilerplate(name, db_type=db)
if success_download:
generate_config(name, db_type=db, custom_port=port)
- typer.secho("\n⨠Spring project ready!", fg=typer.colors.GREEN)
+ typer.secho("\nSpring project ready!", fg=typer.colors.GREEN)
@app.command("angular")
@@ -51,11 +60,11 @@ def init_angular(name: str):
check_tool("npm", "initializing an Angular project")
check_tool("ng", "initializing an Angular project")
- typer.secho(f"š Initializing Angular project: '{name}'...", fg=typer.colors.CYAN)
+ typer.secho(f"Initializing Angular project: '{name}'...", fg=typer.colors.CYAN)
success = generate_angular_boilerplate(name)
if success:
- typer.secho("\n⨠Angular project ready!", fg=typer.colors.GREEN)
+ typer.secho("\nAngular project ready!", fg=typer.colors.GREEN)
@app.command("vue")
@@ -65,8 +74,120 @@ def init_vue(name: str):
"""
check_tool("npm", "initializing a Vue.js project")
- typer.secho(f"š Initializing Vue.js project: '{name}'...", fg=typer.colors.CYAN)
+ typer.secho(f"Initializing Vue.js project: '{name}'...", fg=typer.colors.CYAN)
success = generate_vue_boilerplate(name)
if success:
- typer.secho("\n⨠Vue.js project ready!", fg=typer.colors.GREEN)
+ typer.secho("\nVue.js project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("nest")
+def init_nest(name: str):
+ """
+ Initializes a new NestJS backend project.
+ """
+ check_tool("npm", "initializing a NestJS project")
+
+ typer.secho(f"Initializing NestJS project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_nest_boilerplate(name)
+
+ if success:
+ typer.secho("\nNestJS project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("nodejs")
+def init_nodejs(name: str):
+ """
+ Initializes a new NodeJS/Express backend project.
+ """
+ check_tool("npm", "initializing a NodeJS project")
+
+ typer.secho(f"Initializing NodeJS project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_nodejs_boilerplate(name)
+
+ if success:
+ typer.secho("\nNodeJS project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("react")
+def init_react(name: str):
+ """
+ Initializes a new ReactJS frontend project (Vite + TS).
+ """
+ check_tool("npm", "initializing a ReactJS project")
+
+ typer.secho(f"Initializing ReactJS project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_react_boilerplate(name)
+
+ if success:
+ typer.secho("\nReactJS project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("nextjs")
+def init_nextjs(name: str):
+ """
+ Initializes a new NextJS frontend project.
+ """
+ check_tool("npm", "initializing a NextJS project")
+
+ typer.secho(f"Initializing NextJS project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_nextjs_boilerplate(name)
+
+ if success:
+ typer.secho("\nNextJS project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("fastapi")
+def init_fastapi(name: str):
+ """
+ Initializes a new FastAPI backend project.
+ """
+ check_tool("python3", "initializing a FastAPI project")
+
+ typer.secho(f"Initializing FastAPI project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_fastapi_boilerplate(name)
+
+ if success:
+ typer.secho("\nFastAPI project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("django")
+def init_django(name: str):
+ """
+ Initializes a new Django backend project.
+ """
+ check_tool("python3", "initializing a Django project")
+
+ typer.secho(f"Initializing Django project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_django_boilerplate(name)
+
+ if success:
+ typer.secho("\nDjango project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("svelte")
+def init_svelte(name: str):
+ """
+ Initializes a new SvelteKit frontend project.
+ """
+ check_tool("npm", "initializing a Svelte project")
+
+ typer.secho(f"Initializing Svelte project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_svelte_boilerplate(name)
+
+ if success:
+ typer.secho("\nSvelte project ready!", fg=typer.colors.GREEN)
+
+
+@app.command("go")
+def init_go(name: str):
+ """
+ Initializes a new Go/Fiber backend project.
+ """
+ check_tool("go", "initializing a Go project")
+
+ typer.secho(f"Initializing Go project: '{name}'...", fg=typer.colors.CYAN)
+ success = generate_go_boilerplate(name)
+
+ if success:
+ typer.secho("\nGo project ready!", fg=typer.colors.GREEN)
diff --git a/devctl/commands/run.py b/devctl/commands/run.py
index e3d52f3..0ecb89b 100644
--- a/devctl/commands/run.py
+++ b/devctl/commands/run.py
@@ -3,10 +3,12 @@
Automatically detects and launches backend, frontend, and database services.
"""
+from pathlib import Path
+
import typer
+from devctl.generators.docker_scaffold import discover_docker_projects
from devctl.orchestrator.runner import launch_dev_environment
-from devctl.orchestrator.scanner import detect_environment
from devctl.utils.dependencies import check_tool
app = typer.Typer(help="Local execution and development commands.")
@@ -15,42 +17,51 @@
@app.callback(invoke_without_command=True)
def run_env(ctx: typer.Context):
"""
- Scans the current tree and automatically launches Spring, Angular, and the Database.
+ Scans the current tree and automatically launches Spring, Angular, Vue, and Databases.
"""
if ctx.invoked_subcommand is not None:
return
- typer.secho("š Analyzing the current directory tree...", fg=typer.colors.CYAN)
- env_state = detect_environment(".")
+ 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)
+
+ 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
- if env_state["has_docker_compose"]:
+ if has_docker:
check_tool("docker", "running the database environment")
- if env_state["has_spring"]:
- check_tool("java", "running the Spring Boot backend")
-
- if env_state["has_angular"] or env_state.get("has_vue"):
- check_tool("npm", "running the frontend project")
+ # Group counts
+ counts = {}
+ for p in projects:
+ counts[p.kind] = counts.get(p.kind, 0) + 1
# 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 'ā'}")
- typer.echo(f" - Vue.js Frontend : {'ā
' if env_state['has_vue'] else 'ā'}")
-
- has_env = any(
- [
- env_state["has_docker_compose"],
- env_state["has_spring"],
- env_state["has_angular"],
- env_state.get("has_vue"),
- ]
- )
-
- if not has_env:
- typer.secho("\nā No valid development environment detected here.", fg=typer.colors.RED)
+ def get_status(condition: bool):
+ 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)}")
+
+ if not projects and not docker_composes:
+ 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(env_state)
+ launch_dev_environment(projects, docker_composes)
diff --git a/devctl/generators/angular.py b/devctl/generators/angular.py
index 9cf0a46..977f1be 100644
--- a/devctl/generators/angular.py
+++ b/devctl/generators/angular.py
@@ -15,7 +15,7 @@ def setup_angular_environments(project_path: str):
"""
Generates environments and proxy for an Angular project.
"""
- typer.secho("āļø Configuring Proxy and Environments...", fg=typer.colors.CYAN)
+ typer.secho("Configuring Proxy and Environments...", fg=typer.colors.CYAN)
# 1. Target paths
src_dir = os.path.join(project_path, "src")
@@ -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"ā ļø Error generating {tpl_name}: {e}", fg=typer.colors.YELLOW)
+ typer.secho(f"Warning: Failed to generate {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")
@@ -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"ā ļø Unable to modify angular.json automatically: {e}",
+ f"Warning: Unable to modify angular.json automatically: {e}",
fg=typer.colors.YELLOW,
)
@@ -75,7 +75,7 @@ def generate_angular_boilerplate(project_name: str) -> bool:
"""
Generates an Angular project via the native CLI (@angular/cli) and configures it.
"""
- typer.secho(f"š Generating Angular frontend '{project_name}'...", fg=typer.colors.CYAN)
+ typer.secho(f"Generating Angular frontend '{project_name}'...", fg=typer.colors.CYAN)
safe_name = project_name.lower().replace("_", "-")
@@ -83,20 +83,18 @@ def generate_angular_boilerplate(project_name: str) -> bool:
subprocess.run(["ng", "version"], capture_output=True, check=True)
except FileNotFoundError:
typer.secho(
- "ā Error: Angular CLI ('ng') not found on your system.",
+ "Error: Angular CLI ('ng') not found on your system.",
fg=typer.colors.RED,
)
return False
except subprocess.CalledProcessError:
- typer.secho("ā Error: Angular CLI is installed but not responding.", fg=typer.colors.RED)
+ typer.secho("Error: Angular CLI is installed but not responding.", fg=typer.colors.RED)
return False
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
@@ -104,11 +102,11 @@ def generate_angular_boilerplate(project_name: str) -> bool:
setup_angular_environments(project_full_path)
typer.secho(
- f"ā
Frontend '{safe_name}' successfully generated and configured!",
+ f"Frontend '{safe_name}' successfully generated and configured!",
fg=typer.colors.GREEN,
)
return True
except subprocess.CalledProcessError as e:
- typer.secho(f"ā Angular process failed with code: {e.returncode}", fg=typer.colors.RED)
+ typer.secho(f"Error: Angular process failed with code: {e.returncode}", fg=typer.colors.RED)
return False
diff --git a/devctl/generators/django.py b/devctl/generators/django.py
new file mode 100644
index 0000000..c56a4ec
--- /dev/null
+++ b/devctl/generators/django.py
@@ -0,0 +1,61 @@
+"""
+Generators for Django projects.
+Includes boilerplate generation with Django and DRF.
+"""
+
+import os
+import subprocess
+
+import typer
+
+
+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
+ project_path = os.path.join(os.getcwd(), project_name)
+
+ try:
+ os.makedirs(project_path, exist_ok=True)
+
+ # 1. Create requirements.txt
+ requirements = """django
+djangorestframework
+django-cors-headers
+"""
+ with open(os.path.join(project_path, "requirements.txt"), "w") as f:
+ f.write(requirements)
+
+ # 2. Create virtual environment
+ 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)
+ pip_path = os.path.join(".venv", "bin", "pip")
+ 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)
+ 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
+ )
+ return True
+
+ except Exception as e:
+ 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 826ceca..040ca0f 100644
--- a/devctl/generators/docker_scaffold.py
+++ b/devctl/generators/docker_scaffold.py
@@ -16,6 +16,7 @@
".angular",
".git",
".mvn",
+ ".next",
".pytest_cache",
".venv",
"__pycache__",
@@ -83,7 +84,7 @@ def sanitize_service_name(raw_name: str, fallback: str = "service") -> str:
def discover_docker_projects(root_path: Union[str, Path]) -> list[DockerProject]:
- """Discover all Spring Boot, Angular, and Vue/Vite projects under ``root_path``."""
+ """Discover all supported projects under ``root_path``."""
root = Path(root_path).resolve()
if not root.exists():
raise DockerScaffoldError(f"Path does not exist: {root}")
@@ -102,9 +103,65 @@ 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:
- candidates.append(("vue", project_path))
+ # Check package.json to distinguish between vue and react
+ pkg_path = project_path / "package.json"
+ if pkg_path.exists():
+ try:
+ pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
+ 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
+ except Exception:
+ candidates.append(("vue", project_path))
+ else:
+ candidates.append(("vue", project_path))
+
+ 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()
+ if "fastapi" in reqs.lower():
+ candidates.append(("fastapi", project_path))
+ elif "django" in reqs.lower():
+ candidates.append(("django", project_path))
+ except Exception:
+ pass
+
+ if "manage.py" in filename_set and "requirements.txt" in filename_set:
+ try:
+ reqs = (project_path / "requirements.txt").read_text()
+ if "django" in reqs.lower():
+ candidates.append(("django", project_path))
+ except Exception:
+ 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
+ ):
+ candidates.append(("nodejs", project_path))
used_names: set[str] = set()
projects: list[DockerProject] = []
@@ -123,7 +180,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"} 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
@@ -144,7 +203,7 @@ def scaffold_docker_assets(
root = Path(root_path).resolve()
projects = discover_docker_projects(root)
if not projects:
- raise DockerScaffoldError("No Spring Boot, Angular, or Vue/Vite project detected.")
+ raise DockerScaffoldError("No supported project detected.")
env = _template_environment()
operations = [
@@ -163,6 +222,20 @@ def scaffold_docker_assets(
def _dockerfile_content(env: Environment, project: DockerProject) -> str:
if project.kind == "spring":
return env.get_template("spring/Dockerfile.j2").render(project=project)
+ if project.kind == "nest":
+ return env.get_template("nestjs/Dockerfile.j2").render(project=project)
+ if project.kind == "nodejs":
+ return env.get_template("nodejs/Dockerfile.j2").render(project=project)
+ if project.kind == "nextjs":
+ return env.get_template("nextjs/Dockerfile.j2").render(project=project)
+ if project.kind == "fastapi":
+ return env.get_template("fastapi/Dockerfile.j2").render(project=project)
+ if project.kind == "django":
+ return env.get_template("django/Dockerfile.j2").render(project=project)
+ if project.kind == "svelte":
+ return env.get_template("svelte/Dockerfile.j2").render(project=project)
+ if project.kind == "go":
+ return env.get_template("go/Dockerfile.j2").render(project=project)
return env.get_template("frontend/Dockerfile.j2").render(project=project)
@@ -294,6 +367,9 @@ def _node_version(project_path: Path, kind: str) -> str:
return "20"
return "18"
+ if kind in ["nest", "nodejs", "nextjs", "svelte"]:
+ return "20"
+
return "22"
diff --git a/devctl/generators/fastapi.py b/devctl/generators/fastapi.py
new file mode 100644
index 0000000..1a3a11b
--- /dev/null
+++ b/devctl/generators/fastapi.py
@@ -0,0 +1,67 @@
+"""
+Generators for FastAPI projects.
+Includes boilerplate generation with Uvicorn and Pydantic.
+"""
+
+import os
+import subprocess
+
+import typer
+
+
+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)
+ 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
+
+app = FastAPI(title="devctl FastAPI project")
+
+@app.get("/")
+def read_root():
+ return {"Hello": "from devctl FastAPI!"}
+
+@app.get("/items/{item_id}")
+def read_item(item_id: int, q: str = None):
+ return {"item_id": item_id, "q": q}
+"""
+ with open(os.path.join(project_path, "main.py"), "w") as f:
+ f.write(main_py)
+
+ # 2. Create requirements.txt
+ requirements = """fastapi
+uvicorn[standard]
+pydantic
+"""
+ with open(os.path.join(project_path, "requirements.txt"), "w") as f:
+ f.write(requirements)
+
+ # 3. Create virtual environment
+ 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)
+ # 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)
+ return True
+
+ except Exception as e:
+ 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
new file mode 100644
index 0000000..628e655
--- /dev/null
+++ b/devctl/generators/go_fiber.py
@@ -0,0 +1,61 @@
+"""
+Generators for Go (Fiber) projects.
+Includes boilerplate generation with Fiber framework.
+"""
+
+import os
+import subprocess
+
+import typer
+
+
+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)
+ 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)
+ 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,
+ )
+
+ # 3. Create main.go
+ main_go = """package main
+
+import (
+ "log"
+ "github.com/gofiber/fiber/v2"
+)
+
+func main() {
+ app := fiber.New()
+
+ app.Get("/", func(c *fiber.Ctx) error {
+ return c.SendString("Hello from devctl Go/Fiber!")
+ })
+
+ log.Fatal(app.Listen(":3000"))
+}
+"""
+ 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)
+ return True
+
+ except Exception as e:
+ 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
new file mode 100644
index 0000000..c1ad0d7
--- /dev/null
+++ b/devctl/generators/nestjs.py
@@ -0,0 +1,49 @@
+"""
+Generators for NestJS projects.
+Includes boilerplate generation via Nest CLI.
+"""
+
+import subprocess
+
+import typer
+
+
+def generate_nest_boilerplate(project_name: str) -> bool:
+ """
+ Generates a new NestJS project using the Nest CLI via npx.
+ """
+ typer.secho(f"Generating NestJS project '{project_name}'...", fg=typer.colors.CYAN)
+ safe_name = project_name.lower().replace("_", "-")
+
+ try:
+ # Use npx to run Nest CLI without requiring global installation
+ # --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)
+ subprocess.run(
+ [
+ "npx",
+ "-p",
+ "@nestjs/cli",
+ "nest",
+ "new",
+ safe_name,
+ "--package-manager",
+ "npm",
+ "--skip-git",
+ ],
+ check=True,
+ )
+
+ typer.secho(f"NestJS project '{safe_name}' successfully generated!", fg=typer.colors.GREEN)
+ return True
+
+ except subprocess.CalledProcessError as e:
+ typer.secho(
+ f"Error: Nest CLI process failed with code: {e.returncode}", fg=typer.colors.RED
+ )
+ return False
+ except FileNotFoundError:
+ typer.secho("Error: 'npx' or 'npm' not found in path.", fg=typer.colors.RED)
+ return False
diff --git a/devctl/generators/nextjs.py b/devctl/generators/nextjs.py
new file mode 100644
index 0000000..892d205
--- /dev/null
+++ b/devctl/generators/nextjs.py
@@ -0,0 +1,51 @@
+"""
+Generators for NextJS projects.
+Includes boilerplate generation via create-next-app.
+"""
+
+import subprocess
+
+import typer
+
+
+def generate_nextjs_boilerplate(project_name: str) -> bool:
+ """
+ Generates a new NextJS project using create-next-app via npx.
+ """
+ typer.secho(f"Generating NextJS project '{project_name}'...", fg=typer.colors.CYAN)
+ safe_name = project_name.lower().replace("_", "-")
+
+ try:
+ typer.secho("Scaffolding NextJS project (this may take a minute)...", fg=typer.colors.CYAN)
+ # --ts: TypeScript
+ # --eslint: ESLint
+ # --tailwind: Tailwind CSS
+ # --src-dir: Use src/ directory
+ # --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,
+ )
+
+ typer.secho(f"NextJS project '{safe_name}' successfully generated!", fg=typer.colors.GREEN)
+ return True
+
+ except subprocess.CalledProcessError as e:
+ typer.secho(f"Error: NextJS creation failed with code: {e.returncode}", fg=typer.colors.RED)
+ return False
+ except Exception as e:
+ typer.secho(f"Error: NextJS initialization failed: {e}", fg=typer.colors.RED)
+ return False
diff --git a/devctl/generators/nodejs.py b/devctl/generators/nodejs.py
new file mode 100644
index 0000000..f601714
--- /dev/null
+++ b/devctl/generators/nodejs.py
@@ -0,0 +1,110 @@
+"""
+Generators for NodeJS (Express) projects.
+Includes boilerplate generation with TypeScript and Express.
+"""
+
+import json
+import os
+import subprocess
+
+import typer
+
+
+def generate_nodejs_boilerplate(project_name: str) -> bool:
+ """
+ Generates a new NodeJS + Express + TypeScript project.
+ """
+ typer.secho(f"Generating NodeJS/Express 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. 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
+ )
+
+ # 2. Install dependencies
+ 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,
+ )
+ subprocess.run(
+ [
+ "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
+ )
+
+ # 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';
+
+dotenv.config();
+
+const app = express();
+const port = process.env.PORT || 3000;
+
+app.use(express.json());
+
+app.get('/', (req: Request, res: Response) => {
+ res.send('Hello from devctl NodeJS/Express!');
+});
+
+app.listen(port, () => {
+ console.log(`[server]: Server is running at http://localhost:${port}`);
+});
+"""
+ with open(os.path.join(project_path, "src", "index.ts"), "w") as f:
+ f.write(index_ts)
+
+ # 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",
+ }
+
+ 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,
+ )
+ return True
+
+ except Exception as e:
+ 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
new file mode 100644
index 0000000..5256afb
--- /dev/null
+++ b/devctl/generators/react.py
@@ -0,0 +1,43 @@
+"""
+Generators for ReactJS projects via Vite.
+Includes boilerplate generation and basic configuration.
+"""
+
+import os
+import subprocess
+
+import typer
+
+
+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)
+ safe_name = project_name.lower().replace("_", "-")
+
+ try:
+ typer.secho("Scaffolding React project...", fg=typer.colors.CYAN)
+ subprocess.run(
+ ["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)
+ subprocess.run(["npm", "install"], cwd=project_full_path, check=True)
+
+ typer.secho(
+ f"ReactJS frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN
+ )
+ return True
+
+ except subprocess.CalledProcessError as e:
+ 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"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 06f4255..52e3793 100644
--- a/devctl/generators/scaffold_angular.py
+++ b/devctl/generators/scaffold_angular.py
@@ -43,7 +43,7 @@ def generate_angular_resource(resource_name: str, fields_str: str, root_path: st
env_state = detect_environment(root_path)
if not env_state["has_angular"]:
- typer.secho("ā Error: No Angular project detected here.", fg=typer.colors.RED)
+ typer.secho("Error: No Angular project detected here.", fg=typer.colors.RED)
raise typer.Exit(code=1)
angular_root = env_state["angular_path"]
@@ -120,7 +120,7 @@ def generate_angular_resource(resource_name: str, fields_str: str, root_path: st
templates_dir = os.path.join(os.path.dirname(__file__), "..", "templates", "angular", "feature")
env = Environment(loader=FileSystemLoader(templates_dir))
- typer.secho(f"āļø Generating Angular feature '{entity_name}'...", fg=typer.colors.CYAN)
+ typer.secho(f"Generating Angular feature '{entity_name}'...", fg=typer.colors.CYAN)
# Template data
context = {
@@ -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"ā ļø Error on {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)
+ 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
new file mode 100644
index 0000000..174af2f
--- /dev/null
+++ b/devctl/generators/scaffold_django.py
@@ -0,0 +1,79 @@
+"""
+Django resource scaffolding generator.
+Handles the creation of models, serializers, and views.
+"""
+
+import os
+
+import typer
+
+from devctl.orchestrator.scanner import detect_environment
+
+
+def generate_django_resource(resource_name: str, fields_str: str, root_path: str = "."):
+ """
+ Scaffolds a Django resource.
+ """
+ env_state = detect_environment(root_path)
+
+ if not env_state["has_django"]:
+ typer.secho("ā Error: No Django project detected here.", fg=typer.colors.RED)
+ raise typer.Exit(code=1)
+
+ django_root = env_state["django_path"]
+ resource_name.lower()
+ entity_name = resource_name.capitalize()
+
+ # Structure: core/models.py, core/serializers.py, core/views.py
+ # For simplicity, we inject into the 'core' app created during init
+ core_dir = os.path.join(django_root, "core")
+ if not os.path.exists(core_dir):
+ os.makedirs(core_dir, exist_ok=True)
+
+ typer.secho(f"āļø Generating Django resource '{entity_name}'...", fg=typer.colors.CYAN)
+
+ # 1. Append Model
+ model_snippet = f"""
+class {entity_name}(models.Model):
+ # Fields: {fields_str}
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"{entity_name} {{self.id}}"
+"""
+ with open(os.path.join(core_dir, "models.py"), "a") as f:
+ f.write(model_snippet)
+
+ # 2. Append Serializer
+ serializer_path = os.path.join(core_dir, "serializers.py")
+ 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:
+ model = {entity_name}
+ fields = '__all__'
+"""
+ with open(serializer_path, "a") as f:
+ f.write(serializer_snippet)
+
+ # 3. Append View
+ view_snippet = f"""
+from rest_framework import viewsets
+from .models import {entity_name}
+from .serializers import {entity_name}Serializer
+
+class {entity_name}ViewSet(viewsets.ModelViewSet):
+ queryset = {entity_name}.objects.all()
+ serializer_class = {entity_name}Serializer
+"""
+ with open(os.path.join(core_dir, "views.py"), "a") as f:
+ f.write(view_snippet)
+
+ typer.secho(f"ā
{entity_name} Django feature successfully generated!", fg=typer.colors.GREEN)
+ 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
new file mode 100644
index 0000000..d6d71da
--- /dev/null
+++ b/devctl/generators/scaffold_fastapi.py
@@ -0,0 +1,101 @@
+"""
+FastAPI resource scaffolding generator.
+Handles the creation of routers, schemas, and models.
+"""
+
+import os
+
+import typer
+
+from devctl.orchestrator.scanner import detect_environment
+
+
+def generate_fastapi_resource(resource_name: str, fields_str: str, root_path: str = "."):
+ """
+ Scaffolds a FastAPI resource.
+ """
+ env_state = detect_environment(root_path)
+
+ if not env_state["has_fastapi"]:
+ typer.secho("ā Error: No FastAPI project detected here.", fg=typer.colors.RED)
+ raise typer.Exit(code=1)
+
+ fastapi_root = env_state["fastapi_path"]
+ resource_lower = resource_name.lower()
+ entity_name = resource_name.capitalize()
+
+ # Create directories
+ 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
+
+ typer.secho(f"Generating FastAPI resource '{entity_name}'...", fg=typer.colors.CYAN)
+
+ # 1. Generate Schema (Pydantic)
+ schema_content = f"""from pydantic import BaseModel
+from typing import Optional
+
+class {entity_name}Base(BaseModel):
+ # Fields: {fields_str}
+ pass
+
+class {entity_name}Create({entity_name}Base):
+ pass
+
+class {entity_name}({entity_name}Base):
+ id: int
+
+ class Config:
+ orm_mode = True
+"""
+ with open(os.path.join(schemas_dir, f"{resource_lower}.py"), "w") as f:
+ f.write(schema_content)
+
+ # 2. Generate Router
+ 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"]
+)
+
+@{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 []
+
+@router.post("/", response_model=schemas.{entity_name})
+def create_{resource_lower}(item: schemas.{entity_name}Create):
+ return {{"id": 1, **item.dict()}}
+"""
+ 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.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
new file mode 100644
index 0000000..0f0499e
--- /dev/null
+++ b/devctl/generators/scaffold_go.py
@@ -0,0 +1,67 @@
+"""
+Go (Fiber) resource scaffolding generator.
+Handles the creation of handlers and models.
+"""
+
+import os
+
+import typer
+
+from devctl.orchestrator.scanner import detect_environment
+
+
+def generate_go_resource(resource_name: str, fields_str: str, root_path: str = "."):
+ """
+ Scaffolds a Go resource.
+ """
+ env_state = detect_environment(root_path)
+
+ if not env_state["has_go"]:
+ typer.secho("ā Error: No Go project detected here.", fg=typer.colors.RED)
+ raise typer.Exit(code=1)
+
+ go_root = env_state["go_path"]
+ resource_lower = resource_name.lower()
+ entity_name = resource_name.capitalize()
+
+ # 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)
+
+ typer.secho(f"āļø Generating Go resource '{entity_name}'...", fg=typer.colors.CYAN)
+
+ # 1. Generate Model
+ model_content = f"""package models
+
+type {entity_name} struct {{
+ ID uint `json:"id"`
+ // Fields: {fields_str}
+}}
+"""
+ with open(os.path.join(models_dir, f"{resource_lower}.go"), "w") as f:
+ f.write(model_content)
+
+ # 2. Generate Handler
+ handler_content = f"""package handlers
+
+import (
+ "github.com/gofiber/fiber/v2"
+)
+
+func Get{entity_name}s(c *fiber.Ctx) error {{
+ return c.JSON(fiber.Map{{"message": "Get all {resource_lower}s"}})
+}}
+
+func Create{entity_name}(c *fiber.Ctx) error {{
+ return c.Status(201).JSON(fiber.Map{{"message": "{entity_name} created"}})
+}}
+"""
+ with open(os.path.join(handlers_dir, f"{resource_lower}.go"), "w") as f:
+ f.write(handler_content)
+
+ typer.secho(f"ā
{entity_name} Go feature successfully generated!", fg=typer.colors.GREEN)
+ typer.echo(f" - Created: models/{resource_lower}.go")
+ typer.echo(f" - Created: handlers/{resource_lower}.go")
diff --git a/devctl/generators/scaffold_nestjs.py b/devctl/generators/scaffold_nestjs.py
new file mode 100644
index 0000000..35d5f5e
--- /dev/null
+++ b/devctl/generators/scaffold_nestjs.py
@@ -0,0 +1,51 @@
+"""
+NestJS resource scaffolding generator.
+Handles the creation of modules, controllers, and services using Nest CLI.
+"""
+
+import subprocess
+
+import typer
+
+from devctl.orchestrator.scanner import detect_environment
+
+
+def generate_nest_resource(resource_name: str, fields_str: str, root_path: str = "."):
+ """
+ Scaffolds a NestJS resource using the Nest CLI.
+ """
+ env_state = detect_environment(root_path)
+
+ if not env_state["has_nest"]:
+ typer.secho("ā Error: No NestJS project detected here.", fg=typer.colors.RED)
+ raise typer.Exit(code=1)
+
+ nest_root = env_state["nest_path"]
+ resource_lower = resource_name.lower()
+
+ typer.secho(f"āļø Generating NestJS resource '{resource_name}'...", fg=typer.colors.CYAN)
+
+ try:
+ # Use npx to run Nest CLI
+ # 'g res' is shorthand for 'generate resource'
+ # --no-spec skips test files for a cleaner scaffold
+ subprocess.run(
+ ["npx", "@nestjs/cli", "g", "resource", resource_lower, "--no-spec"],
+ cwd=nest_root,
+ check=True,
+ )
+
+ typer.secho(
+ f"{resource_name} NestJS resource successfully generated!", fg=typer.colors.GREEN
+ )
+ typer.echo(
+ f"Hint: Fields [{fields_str}] were provided but manual DTO update is recommended."
+ )
+
+ except subprocess.CalledProcessError as e:
+ typer.secho(
+ f"Error: Nest CLI resource generation failed with code: {e.returncode}",
+ fg=typer.colors.RED,
+ )
+ except Exception as e:
+ typer.secho(f"Warning: An unexpected error occurred: {e}", fg=typer.colors.YELLOW)
diff --git a/devctl/generators/scaffold_nextjs.py b/devctl/generators/scaffold_nextjs.py
new file mode 100644
index 0000000..5014a12
--- /dev/null
+++ b/devctl/generators/scaffold_nextjs.py
@@ -0,0 +1,73 @@
+"""
+NextJS resource scaffolding generator.
+Handles the creation of pages and components in the App Router.
+"""
+
+import os
+
+import typer
+
+from devctl.orchestrator.scanner import detect_environment
+
+
+def generate_nextjs_resource(resource_name: str, _fields_str: str, root_path: str = "."):
+ """
+ Scaffolds a NextJS resource (Page, Component).
+ """
+ env_state = detect_environment(root_path)
+
+ if not env_state["has_nextjs"]:
+ typer.secho("ā Error: No NextJS project detected here.", fg=typer.colors.RED)
+ raise typer.Exit(code=1)
+
+ nextjs_root = env_state["nextjs_path"]
+ resource_lower = resource_name.lower()
+ entity_name = resource_name.capitalize()
+
+ # 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)
+
+ typer.secho(f"āļø Generating NextJS resource '{entity_name}'...", fg=typer.colors.CYAN)
+
+ # 1. Generate Page (tsx)
+ page_content = f"""import React from 'react';
+import {{ {entity_name}List }} from '@/components/{entity_name}List';
+
+export default function {entity_name}Page() {{
+ return (
+ {entity_name} Management
+ <{entity_name}List />
+
This component was generated by devctl.
+Manage your {resource_lower}s here.
+Generated by devctl
+