devctl 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devctl/__init__.py +3 -0
- devctl/commands/__init__.py +3 -0
- devctl/commands/add.py +166 -0
- devctl/commands/deploy.py +61 -0
- devctl/commands/docker.py +65 -0
- devctl/commands/init.py +193 -0
- devctl/commands/run.py +67 -0
- devctl/generators/__init__.py +3 -0
- devctl/generators/angular.py +112 -0
- devctl/generators/django.py +61 -0
- devctl/generators/docker_scaffold.py +656 -0
- devctl/generators/fastapi.py +67 -0
- devctl/generators/go_fiber.py +61 -0
- devctl/generators/nestjs.py +49 -0
- devctl/generators/nextjs.py +53 -0
- devctl/generators/nodejs.py +109 -0
- devctl/generators/react.py +43 -0
- devctl/generators/scaffold_angular.py +163 -0
- devctl/generators/scaffold_django.py +79 -0
- devctl/generators/scaffold_fastapi.py +83 -0
- devctl/generators/scaffold_go.py +67 -0
- devctl/generators/scaffold_nestjs.py +52 -0
- devctl/generators/scaffold_nextjs.py +73 -0
- devctl/generators/scaffold_nodejs.py +81 -0
- devctl/generators/scaffold_react.py +80 -0
- devctl/generators/scaffold_spring.py +166 -0
- devctl/generators/scaffold_svelte.py +73 -0
- devctl/generators/scaffold_vue.py +111 -0
- devctl/generators/spring.py +221 -0
- devctl/generators/svelte.py +52 -0
- devctl/generators/vue.py +105 -0
- devctl/main.py +45 -0
- devctl/orchestrator/__init__.py +3 -0
- devctl/orchestrator/config_builder.py +64 -0
- devctl/orchestrator/runner.py +219 -0
- devctl/orchestrator/scanner.py +155 -0
- devctl/templates/angular/config/environment.development.ts.j2 +4 -0
- devctl/templates/angular/config/environment.ts.j2 +5 -0
- devctl/templates/angular/config/proxy.conf.json.j2 +8 -0
- devctl/templates/angular/feature/models/request.model.ts.j2 +5 -0
- devctl/templates/angular/feature/models/response.model.ts.j2 +6 -0
- devctl/templates/angular/feature/pages/form/form.component.html.j2 +21 -0
- devctl/templates/angular/feature/pages/form/form.component.scss.j2 +0 -0
- devctl/templates/angular/feature/pages/form/form.component.ts.j2 +63 -0
- devctl/templates/angular/feature/pages/list/list.component.html.j2 +28 -0
- devctl/templates/angular/feature/pages/list/list.component.scss.j2 +0 -0
- devctl/templates/angular/feature/pages/list/list.component.ts.j2 +34 -0
- devctl/templates/angular/feature/routes.ts.j2 +9 -0
- devctl/templates/angular/feature/services/service.ts.j2 +34 -0
- devctl/templates/docker/deploy.yml.j2 +37 -0
- devctl/templates/docker/django/Dockerfile.j2 +21 -0
- devctl/templates/docker/fastapi/Dockerfile.j2 +15 -0
- devctl/templates/docker/frontend/Dockerfile.j2 +31 -0
- devctl/templates/docker/go/Dockerfile.j2 +24 -0
- devctl/templates/docker/nestjs/Dockerfile.j2 +26 -0
- devctl/templates/docker/nextjs/Dockerfile.j2 +43 -0
- devctl/templates/docker/nodejs/Dockerfile.j2 +24 -0
- devctl/templates/docker/spring/Dockerfile.j2 +24 -0
- devctl/templates/docker/svelte/Dockerfile.j2 +24 -0
- devctl/templates/proxy.conf.json.j2 +0 -0
- devctl/templates/spring/Controller.java.j2 +50 -0
- devctl/templates/spring/Entity.java.j2 +22 -0
- devctl/templates/spring/Repository.java.j2 +9 -0
- devctl/templates/spring/Service.java.j2 +20 -0
- devctl/templates/spring/ServiceImpl.java.j2 +62 -0
- devctl/templates/spring/application.properties.j2 +19 -0
- devctl/templates/spring/config/ApplicationConfig.java.j2 +46 -0
- devctl/templates/spring/config/JwtAuthenticationFilter.java.j2 +54 -0
- devctl/templates/spring/config/JwtService.java.j2 +68 -0
- devctl/templates/spring/config/SecurityConfig.java.j2 +42 -0
- devctl/templates/spring/docker-compose.yml.j2 +29 -0
- devctl/templates/spring/dto/Request.java.j2 +20 -0
- devctl/templates/spring/dto/Response.java.j2 +22 -0
- devctl/templates/spring/mapper/Mapper.java.j2 +30 -0
- devctl/templates/vue/config/App.vue.j2 +35 -0
- devctl/templates/vue/config/main.ts.j2 +9 -0
- devctl/templates/vue/config/router.ts.j2 +18 -0
- devctl/templates/vue/config/vite.config.ts.j2 +16 -0
- devctl/templates/vue/feature/Form.vue.j2 +162 -0
- devctl/templates/vue/feature/List.vue.j2 +155 -0
- devctl/templates/vue/feature/models.ts.j2 +12 -0
- devctl/templates/vue/feature/routes.ts.j2 +19 -0
- devctl/templates/vue/feature/service.ts.j2 +44 -0
- devctl/utils/__init__.py +3 -0
- devctl/utils/dependencies.py +36 -0
- devctl/utils/env_loader.py +57 -0
- devctl-1.0.0.dist-info/METADATA +127 -0
- devctl-1.0.0.dist-info/RECORD +92 -0
- devctl-1.0.0.dist-info/WHEEL +5 -0
- devctl-1.0.0.dist-info/entry_points.txt +2 -0
- devctl-1.0.0.dist-info/licenses/LICENSE +21 -0
- devctl-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local development environment runner.
|
|
3
|
+
Handles parallel process management for multi-tier applications with log prefixing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
|
|
19
|
+
from devctl.generators.docker_scaffold import DockerProject
|
|
20
|
+
from devctl.utils.env_loader import get_project_env
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
# Global list to track processes for cleanup
|
|
25
|
+
active_processes = []
|
|
26
|
+
active_threads = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_docker_running():
|
|
30
|
+
"""Checks if the Docker daemon is active on the system."""
|
|
31
|
+
try:
|
|
32
|
+
subprocess.run(["docker", "info"], capture_output=True, check=True)
|
|
33
|
+
return True
|
|
34
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def stream_logs(name: str, process: subprocess.Popen, color: str):
|
|
39
|
+
"""Streams logs from a process with a colored prefix."""
|
|
40
|
+
try:
|
|
41
|
+
# Use line-buffered reading
|
|
42
|
+
for line in iter(process.stdout.readline, b""):
|
|
43
|
+
if line:
|
|
44
|
+
decoded_line = line.decode("utf-8", errors="ignore").rstrip()
|
|
45
|
+
console.print(f"[{color}]{name:>15} |[/{color}] {escape(decoded_line)}")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[red]Error streaming logs for {name}: {escape(str(e))}[/red]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _launch_process(p: DockerProject, cmd: List[str], color: str, label: str):
|
|
51
|
+
"""Helper to launch a process with log streaming and .env loading."""
|
|
52
|
+
global active_processes, active_threads
|
|
53
|
+
|
|
54
|
+
typer.secho(f"Starting {label}: {p.name}...", fg=getattr(typer.colors, color.upper()))
|
|
55
|
+
|
|
56
|
+
# Load environment variables including .env if present
|
|
57
|
+
env = get_project_env(p.path)
|
|
58
|
+
|
|
59
|
+
proc = subprocess.Popen(
|
|
60
|
+
cmd,
|
|
61
|
+
cwd=str(p.path),
|
|
62
|
+
stdout=subprocess.PIPE,
|
|
63
|
+
stderr=subprocess.STDOUT,
|
|
64
|
+
bufsize=1,
|
|
65
|
+
env=env,
|
|
66
|
+
)
|
|
67
|
+
active_processes.append((p.name, proc))
|
|
68
|
+
|
|
69
|
+
t = threading.Thread(target=stream_logs, args=(p.name, proc, color), daemon=True)
|
|
70
|
+
t.start()
|
|
71
|
+
active_threads.append(t)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def launch_dev_environment(projects: List[DockerProject], docker_composes: List[Path]):
|
|
75
|
+
"""
|
|
76
|
+
Launches the necessary processes in parallel with structured startup and log streaming.
|
|
77
|
+
"""
|
|
78
|
+
global active_processes
|
|
79
|
+
|
|
80
|
+
def signal_handler(_sig, _frame):
|
|
81
|
+
typer.echo("\nShutdown requested. Cleaning up...")
|
|
82
|
+
cleanup_and_exit(docker_composes)
|
|
83
|
+
|
|
84
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
85
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# 1. Start Databases
|
|
89
|
+
if docker_composes:
|
|
90
|
+
if not is_docker_running():
|
|
91
|
+
typer.secho("Error: Docker service is not running.", fg=typer.colors.RED)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
for compose_path in docker_composes:
|
|
95
|
+
typer.secho(
|
|
96
|
+
f"Starting Docker Compose DB in {compose_path}...",
|
|
97
|
+
fg=typer.colors.CYAN,
|
|
98
|
+
)
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["docker", "compose", "-f", "docker-compose-db.yml", "up", "-d"],
|
|
101
|
+
cwd=str(compose_path),
|
|
102
|
+
check=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
typer.echo("Waiting 5s for databases to initialize...")
|
|
106
|
+
time.sleep(5)
|
|
107
|
+
|
|
108
|
+
# 2. Start Projects
|
|
109
|
+
for p in projects:
|
|
110
|
+
if p.kind == "spring":
|
|
111
|
+
_launch_process(p, ["./mvnw", "spring-boot:run"], "green", "Spring Boot")
|
|
112
|
+
|
|
113
|
+
elif p.kind == "angular":
|
|
114
|
+
_launch_process(p, ["npx", "ng", "serve"], "cyan", "Angular")
|
|
115
|
+
|
|
116
|
+
elif p.kind == "vue":
|
|
117
|
+
_launch_process(p, ["npm", "run", "dev"], "magenta", "Vue")
|
|
118
|
+
|
|
119
|
+
elif p.kind == "react":
|
|
120
|
+
_launch_process(p, ["npm", "run", "dev"], "blue", "React")
|
|
121
|
+
|
|
122
|
+
elif p.kind == "nextjs":
|
|
123
|
+
_launch_process(p, ["npm", "run", "dev"], "yellow", "NextJS")
|
|
124
|
+
|
|
125
|
+
elif p.kind == "svelte":
|
|
126
|
+
_launch_process(p, ["npm", "run", "dev"], "red", "Svelte")
|
|
127
|
+
|
|
128
|
+
elif p.kind == "nest":
|
|
129
|
+
_launch_process(p, ["npm", "run", "start:dev"], "magenta", "NestJS")
|
|
130
|
+
|
|
131
|
+
elif p.kind == "nodejs":
|
|
132
|
+
_launch_process(p, ["npm", "run", "dev"], "green", "NodeJS")
|
|
133
|
+
|
|
134
|
+
elif p.kind == "fastapi":
|
|
135
|
+
venv_python = os.path.join(str(p.path), ".venv", "bin", "python3")
|
|
136
|
+
if not os.path.exists(venv_python):
|
|
137
|
+
venv_python = "python3"
|
|
138
|
+
_launch_process(
|
|
139
|
+
p, [venv_python, "-m", "uvicorn", "main:app", "--reload"], "cyan", "FastAPI"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
elif p.kind == "django":
|
|
143
|
+
venv_python = os.path.join(str(p.path), ".venv", "bin", "python3")
|
|
144
|
+
if not os.path.exists(venv_python):
|
|
145
|
+
venv_python = "python3"
|
|
146
|
+
_launch_process(p, [venv_python, "manage.py", "runserver"], "green", "Django")
|
|
147
|
+
|
|
148
|
+
elif p.kind == "go":
|
|
149
|
+
_launch_process(p, ["go", "run", "."], "cyan", "Go")
|
|
150
|
+
|
|
151
|
+
if not active_processes and not docker_composes:
|
|
152
|
+
typer.secho(
|
|
153
|
+
"Warning: No projects or databases detected to run.",
|
|
154
|
+
fg=typer.colors.YELLOW,
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
typer.secho(
|
|
159
|
+
"\nDevelopment environment active! Press Ctrl+C to stop everything gracefully.\n",
|
|
160
|
+
fg=typer.colors.GREEN,
|
|
161
|
+
bold=True,
|
|
162
|
+
)
|
|
163
|
+
# Keep the main thread alive
|
|
164
|
+
while True:
|
|
165
|
+
# Monitor process health
|
|
166
|
+
for name, proc in active_processes:
|
|
167
|
+
exit_code = proc.poll()
|
|
168
|
+
if exit_code is not None:
|
|
169
|
+
typer.secho(
|
|
170
|
+
f"\nError: {name} process terminated unexpectedly "
|
|
171
|
+
f"(Exit code: {exit_code}).",
|
|
172
|
+
fg=typer.colors.RED,
|
|
173
|
+
bold=True,
|
|
174
|
+
)
|
|
175
|
+
# Trigger shutdown logic
|
|
176
|
+
raise KeyboardInterrupt
|
|
177
|
+
|
|
178
|
+
time.sleep(1)
|
|
179
|
+
# Check if any process has died unexpectedly
|
|
180
|
+
# This is a bit redundant with the monitor above but kept for compatibility
|
|
181
|
+
for name, proc in active_processes:
|
|
182
|
+
if proc.poll() is not None:
|
|
183
|
+
typer.secho(
|
|
184
|
+
f"Warning: Process {name} exited with code {proc.returncode}",
|
|
185
|
+
fg=typer.colors.RED,
|
|
186
|
+
)
|
|
187
|
+
active_processes.remove((name, proc))
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
typer.secho(f"Error: A system error occurred: {e}", fg=typer.colors.RED)
|
|
191
|
+
cleanup_and_exit(docker_composes)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cleanup_and_exit(docker_composes: List[Path]):
|
|
195
|
+
"""Stops all active processes and docker containers."""
|
|
196
|
+
for name, proc in active_processes:
|
|
197
|
+
typer.echo(f"Closing {name}...")
|
|
198
|
+
proc.terminate()
|
|
199
|
+
try:
|
|
200
|
+
proc.wait(timeout=5)
|
|
201
|
+
except subprocess.TimeoutExpired:
|
|
202
|
+
typer.echo(f"Force killing {name}...")
|
|
203
|
+
proc.kill()
|
|
204
|
+
|
|
205
|
+
for compose_path in docker_composes:
|
|
206
|
+
typer.echo(f"Stopping Docker Compose DB in {compose_path}...")
|
|
207
|
+
try:
|
|
208
|
+
subprocess.run(
|
|
209
|
+
["docker", "compose", "-f", "docker-compose-db.yml", "down", "-v"],
|
|
210
|
+
cwd=str(compose_path),
|
|
211
|
+
check=True,
|
|
212
|
+
stdout=subprocess.DEVNULL,
|
|
213
|
+
stderr=subprocess.DEVNULL,
|
|
214
|
+
)
|
|
215
|
+
except Exception:
|
|
216
|
+
typer.secho(f"Warning: Docker cleanup failed for {compose_path}", fg=typer.colors.RED)
|
|
217
|
+
|
|
218
|
+
typer.secho("Cleanup finished. Environment is clean.", fg=typer.colors.GREEN)
|
|
219
|
+
sys.exit(0)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project scanner and environment detector.
|
|
3
|
+
Identifies Spring Boot, Angular, Vue.js, React, NextJS, NestJS, NodeJS, FastAPI,
|
|
4
|
+
Django, Svelte, Go, and Docker components in a directory tree.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Directories to ignore during scanning
|
|
12
|
+
IGNORED_DIRECTORIES = {
|
|
13
|
+
"node_modules",
|
|
14
|
+
"target",
|
|
15
|
+
".git",
|
|
16
|
+
".angular",
|
|
17
|
+
"dist",
|
|
18
|
+
"build",
|
|
19
|
+
"venv",
|
|
20
|
+
".venv",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".next",
|
|
23
|
+
".svelte-kit",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_environment(root_path: str = "."):
|
|
28
|
+
"""
|
|
29
|
+
Scans the directory tree and its subfolders to identify components.
|
|
30
|
+
Returns the state and absolute paths of each component.
|
|
31
|
+
"""
|
|
32
|
+
root = Path(root_path).resolve()
|
|
33
|
+
env_state = {
|
|
34
|
+
"has_docker_compose": False,
|
|
35
|
+
"docker_path": None,
|
|
36
|
+
"has_spring": False,
|
|
37
|
+
"spring_path": None,
|
|
38
|
+
"has_angular": False,
|
|
39
|
+
"angular_path": None,
|
|
40
|
+
"has_vue": False,
|
|
41
|
+
"vue_path": None,
|
|
42
|
+
"has_react": False,
|
|
43
|
+
"react_path": None,
|
|
44
|
+
"has_nextjs": False,
|
|
45
|
+
"nextjs_path": None,
|
|
46
|
+
"has_nest": False,
|
|
47
|
+
"nest_path": None,
|
|
48
|
+
"has_nodejs": False,
|
|
49
|
+
"nodejs_path": None,
|
|
50
|
+
"has_fastapi": False,
|
|
51
|
+
"fastapi_path": None,
|
|
52
|
+
"has_django": False,
|
|
53
|
+
"django_path": None,
|
|
54
|
+
"has_svelte": False,
|
|
55
|
+
"svelte_path": None,
|
|
56
|
+
"has_go": False,
|
|
57
|
+
"go_path": None,
|
|
58
|
+
"project_root": str(root),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
62
|
+
# In-place modification of dirnames to prune the traversal
|
|
63
|
+
dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRECTORIES]
|
|
64
|
+
|
|
65
|
+
current_path = Path(dirpath)
|
|
66
|
+
filename_set = set(filenames)
|
|
67
|
+
|
|
68
|
+
# 1. Docker Compose detection
|
|
69
|
+
if "docker-compose-db.yml" in filename_set and not env_state["has_docker_compose"]:
|
|
70
|
+
env_state["has_docker_compose"] = True
|
|
71
|
+
env_state["docker_path"] = str(current_path)
|
|
72
|
+
|
|
73
|
+
# 2. Spring Boot detection
|
|
74
|
+
if ("pom.xml" in filename_set or "mvnw" in filename_set) and not env_state["has_spring"]:
|
|
75
|
+
env_state["has_spring"] = True
|
|
76
|
+
env_state["spring_path"] = str(current_path)
|
|
77
|
+
|
|
78
|
+
# 3. Angular detection
|
|
79
|
+
if "angular.json" in filename_set and not env_state["has_angular"]:
|
|
80
|
+
env_state["has_angular"] = True
|
|
81
|
+
env_state["angular_path"] = str(current_path)
|
|
82
|
+
|
|
83
|
+
# 4. Vite-based detection (Vue/React)
|
|
84
|
+
vue_markers = {"vite.config.ts", "vite.config.js"}
|
|
85
|
+
if (vue_markers & filename_set) and not any([env_state["has_vue"], env_state["has_react"]]):
|
|
86
|
+
# Distinguish by package.json
|
|
87
|
+
pkg_path = current_path / "package.json"
|
|
88
|
+
if pkg_path.exists():
|
|
89
|
+
try:
|
|
90
|
+
pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
91
|
+
deps = pkg.get("dependencies", {})
|
|
92
|
+
dev_deps = pkg.get("devDependencies", {})
|
|
93
|
+
all_deps = {**deps, **dev_deps}
|
|
94
|
+
if "react" in all_deps:
|
|
95
|
+
env_state["has_react"] = True
|
|
96
|
+
env_state["react_path"] = str(current_path)
|
|
97
|
+
else:
|
|
98
|
+
env_state["has_vue"] = True
|
|
99
|
+
env_state["vue_path"] = str(current_path)
|
|
100
|
+
except Exception:
|
|
101
|
+
env_state["has_vue"] = True
|
|
102
|
+
env_state["vue_path"] = str(current_path)
|
|
103
|
+
else:
|
|
104
|
+
env_state["has_vue"] = True
|
|
105
|
+
env_state["vue_path"] = str(current_path)
|
|
106
|
+
|
|
107
|
+
# 5. NestJS detection
|
|
108
|
+
if "nest-cli.json" in filename_set and not env_state["has_nest"]:
|
|
109
|
+
env_state["has_nest"] = True
|
|
110
|
+
env_state["nest_path"] = str(current_path)
|
|
111
|
+
|
|
112
|
+
# 6. NextJS detection
|
|
113
|
+
if any(f.startswith("next.config.") for f in filename_set) and not env_state["has_nextjs"]:
|
|
114
|
+
env_state["has_nextjs"] = True
|
|
115
|
+
env_state["nextjs_path"] = str(current_path)
|
|
116
|
+
|
|
117
|
+
# 7. Generic NodeJS detection (if not already caught)
|
|
118
|
+
if "package.json" in filename_set and not any(
|
|
119
|
+
[
|
|
120
|
+
env_state["has_angular"],
|
|
121
|
+
env_state["has_vue"],
|
|
122
|
+
env_state["has_react"],
|
|
123
|
+
env_state["has_nest"],
|
|
124
|
+
env_state["has_nextjs"],
|
|
125
|
+
]
|
|
126
|
+
):
|
|
127
|
+
env_state["has_nodejs"] = True
|
|
128
|
+
env_state["nodejs_path"] = str(current_path)
|
|
129
|
+
|
|
130
|
+
# 8. Python detection (FastAPI/Django)
|
|
131
|
+
if "requirements.txt" in filename_set:
|
|
132
|
+
req_path = current_path / "requirements.txt"
|
|
133
|
+
try:
|
|
134
|
+
req_content = req_path.read_text(encoding="utf-8").lower()
|
|
135
|
+
if "fastapi" in req_content and not env_state["has_fastapi"]:
|
|
136
|
+
env_state["has_fastapi"] = True
|
|
137
|
+
env_state["fastapi_path"] = str(current_path)
|
|
138
|
+
if "django" in req_content and not env_state["has_django"]:
|
|
139
|
+
env_state["has_django"] = True
|
|
140
|
+
env_state["django_path"] = str(current_path)
|
|
141
|
+
except (OSError, UnicodeDecodeError):
|
|
142
|
+
# Ignore unreadable requirements files during environment scanning
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# 9. Svelte detection
|
|
146
|
+
if "svelte.config.js" in filename_set and not env_state["has_svelte"]:
|
|
147
|
+
env_state["has_svelte"] = True
|
|
148
|
+
env_state["svelte_path"] = str(current_path)
|
|
149
|
+
|
|
150
|
+
# 10. Go detection
|
|
151
|
+
if "go.mod" in filename_set and not env_state["has_go"]:
|
|
152
|
+
env_state["has_go"] = True
|
|
153
|
+
env_state["go_path"] = str(current_path)
|
|
154
|
+
|
|
155
|
+
return env_state
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="form-container">
|
|
2
|
+
<h2>{{ '{{' }} isEditMode ? 'Modifier' : 'Nouveau' {{ '}}' }} {{ entity_name }}</h2>
|
|
3
|
+
|
|
4
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
5
|
+
{% for field in fields %}
|
|
6
|
+
<div class="form-group">
|
|
7
|
+
<label for="{{ field.name }}">{{ field.name | capitalize }}</label>
|
|
8
|
+
<input
|
|
9
|
+
id="{{ field.name }}"
|
|
10
|
+
type="{% if field.ts_type == 'number' %}number{% else %}text{% endif %}"
|
|
11
|
+
formControlName="{{ field.name }}"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
{% endfor %}
|
|
15
|
+
|
|
16
|
+
<div class="form-actions">
|
|
17
|
+
<button type="submit" [disabled]="form.invalid">Enregistrer</button>
|
|
18
|
+
<a routerLink="/{{ resource_name_lower }}s" class="btn-cancel">Annuler</a>
|
|
19
|
+
</div>
|
|
20
|
+
</form>
|
|
21
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Component, inject, OnInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
4
|
+
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
5
|
+
import { {{ entity_name }}Service } from '../../services/{{ resource_name_lower }}.service';
|
|
6
|
+
import { {{ entity_name }}Request } from '../../models/{{ resource_name_lower }}-request.model';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-{{ resource_name_lower }}-form',
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [CommonModule, ReactiveFormsModule, RouterModule],
|
|
12
|
+
templateUrl: './{{ resource_name_lower }}-form.component.html',
|
|
13
|
+
styleUrls: ['./{{ resource_name_lower }}-form.component.scss']
|
|
14
|
+
})
|
|
15
|
+
export class {{ entity_name }}FormComponent implements OnInit {
|
|
16
|
+
private fb = inject(FormBuilder);
|
|
17
|
+
private service = inject({{ entity_name }}Service);
|
|
18
|
+
private router = inject(Router);
|
|
19
|
+
private route = inject(ActivatedRoute);
|
|
20
|
+
|
|
21
|
+
form!: FormGroup;
|
|
22
|
+
isEditMode = false;
|
|
23
|
+
currentId?: number;
|
|
24
|
+
|
|
25
|
+
ngOnInit(): void {
|
|
26
|
+
this.initForm();
|
|
27
|
+
this.checkEditMode();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private initForm(): void {
|
|
31
|
+
this.form = this.fb.group({
|
|
32
|
+
{% for field in fields %}
|
|
33
|
+
{{ field.name }}: ['', Validators.required],
|
|
34
|
+
{% endfor %}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private checkEditMode(): void {
|
|
39
|
+
const id = this.route.snapshot.paramMap.get('id');
|
|
40
|
+
if (id) {
|
|
41
|
+
this.isEditMode = true;
|
|
42
|
+
this.currentId = +id;
|
|
43
|
+
this.service.getById(this.currentId).subscribe(data => {
|
|
44
|
+
this.form.patchValue(data);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onSubmit(): void {
|
|
50
|
+
if (this.form.invalid) return;
|
|
51
|
+
|
|
52
|
+
const request: {{ entity_name }}Request = this.form.value;
|
|
53
|
+
const action$ = this.isEditMode && this.currentId
|
|
54
|
+
? this.service.update(this.currentId, request)
|
|
55
|
+
: this.service.create(request);
|
|
56
|
+
|
|
57
|
+
action$.subscribe({
|
|
58
|
+
// Redirige vers la liste après succès
|
|
59
|
+
next: () => this.router.navigate(['/{{ resource_name_lower }}s']),
|
|
60
|
+
error: (err) => console.error('Erreur de sauvegarde', err)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div class="header-actions">
|
|
2
|
+
<h2>Liste des {{ entity_name }}s</h2>
|
|
3
|
+
<a routerLink="new" class="btn-primary">Nouveau {{ entity_name }}</a>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<table>
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>ID</th>
|
|
10
|
+
{% for field in fields %}
|
|
11
|
+
<th>{{ field.name | capitalize }}</th>
|
|
12
|
+
{% endfor %}
|
|
13
|
+
<th>Actions</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<tr *ngFor="let item of items">
|
|
18
|
+
<td>{{ '{{' }} item.id {{ '}}' }}</td>
|
|
19
|
+
{% for field in fields %}
|
|
20
|
+
<td>{{ '{{' }} item.{{ field.name }} {{ '}}' }}</td>
|
|
21
|
+
{% endfor %}
|
|
22
|
+
<td>
|
|
23
|
+
<a [routerLink]="['edit', item.id]" class="btn-edit">Modifier</a>
|
|
24
|
+
<button (click)="delete(item.id)" class="btn-delete">Supprimer</button>
|
|
25
|
+
</td>
|
|
26
|
+
</tr>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Component, inject, OnInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { RouterModule } from '@angular/router';
|
|
4
|
+
import { {{ entity_name }}Service } from '../../services/{{ resource_name_lower }}.service';
|
|
5
|
+
import { {{ entity_name }}Response } from '../../models/{{ resource_name_lower }}-response.model';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-{{ resource_name_lower }}-list',
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [CommonModule, RouterModule],
|
|
11
|
+
templateUrl: './{{ resource_name_lower }}-list.component.html',
|
|
12
|
+
styleUrls: ['./{{ resource_name_lower }}-list.component.scss']
|
|
13
|
+
})
|
|
14
|
+
export class {{ entity_name }}ListComponent implements OnInit {
|
|
15
|
+
private service = inject({{ entity_name }}Service);
|
|
16
|
+
items: {{ entity_name }}Response[] = [];
|
|
17
|
+
|
|
18
|
+
ngOnInit(): void {
|
|
19
|
+
this.loadItems();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loadItems(): void {
|
|
23
|
+
this.service.getAll().subscribe({
|
|
24
|
+
next: (data) => this.items = data,
|
|
25
|
+
error: (err) => console.error('Erreur lors du chargement', err)
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
delete(id: number): void {
|
|
30
|
+
if (confirm('Voulez-vous vraiment supprimer cet élément ?')) {
|
|
31
|
+
this.service.delete(id).subscribe(() => this.loadItems());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import { {{ entity_name }}ListComponent } from './pages/{{ resource_name_lower }}-list/{{ resource_name_lower }}-list.component';
|
|
3
|
+
import { {{ entity_name }}FormComponent } from './pages/{{ resource_name_lower }}-form/{{ resource_name_lower }}-form.component';
|
|
4
|
+
|
|
5
|
+
export const {{ uppercase_name }}_ROUTES: Routes = [
|
|
6
|
+
{ path: '', component: {{ entity_name }}ListComponent },
|
|
7
|
+
{ path: 'new', component: {{ entity_name }}FormComponent },
|
|
8
|
+
{ path: 'edit/:id', component: {{ entity_name }}FormComponent }
|
|
9
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import { environment } from '../../../../environments/environment';
|
|
5
|
+
import { {{ entity_name }}Request } from '../models/{{ resource_name_lower }}-request.model';
|
|
6
|
+
import { {{ entity_name }}Response } from '../models/{{ resource_name_lower }}-response.model';
|
|
7
|
+
|
|
8
|
+
@Injectable({
|
|
9
|
+
providedIn: 'root'
|
|
10
|
+
})
|
|
11
|
+
export class {{ entity_name }}Service {
|
|
12
|
+
private http = inject(HttpClient);
|
|
13
|
+
private apiUrl = `${environment.apiUrl}/{{ table_name }}`; // URL Spring Boot
|
|
14
|
+
|
|
15
|
+
getAll(): Observable<{{ entity_name }}Response[]> {
|
|
16
|
+
return this.http.get<{{ entity_name }}Response[]>(this.apiUrl);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getById(id: number): Observable<{{ entity_name }}Response> {
|
|
20
|
+
return this.http.get<{{ entity_name }}Response>(`${this.apiUrl}/${id}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
create(request: {{ entity_name }}Request): Observable<{{ entity_name }}Response> {
|
|
24
|
+
return this.http.post<{{ entity_name }}Response>(this.apiUrl, request);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update(id: number, request: {{ entity_name }}Request): Observable<{{ entity_name }}Response> {
|
|
28
|
+
return this.http.put<{{ entity_name }}Response>(`${this.apiUrl}/${id}`, request);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete(id: number): Observable<void> {
|
|
32
|
+
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
services:
|
|
2
|
+
{% for service in services %}
|
|
3
|
+
{{ service.service_name }}:
|
|
4
|
+
build:
|
|
5
|
+
context: {{ service.relative_context }}
|
|
6
|
+
dockerfile: Dockerfile
|
|
7
|
+
{% if service.kind == 'spring' and service.db %}
|
|
8
|
+
depends_on:
|
|
9
|
+
- {{ service.db.service_name }}
|
|
10
|
+
environment:
|
|
11
|
+
- SPRING_DATASOURCE_URL=jdbc:{{ service.db.type }}://{{ service.db.service_name }}:{{ service.db.internal_port }}/{{ service.db.name }}
|
|
12
|
+
- SPRING_DATASOURCE_USERNAME={{ service.db.user }}
|
|
13
|
+
- SPRING_DATASOURCE_PASSWORD={{ service.db.password }}
|
|
14
|
+
{% endif %}
|
|
15
|
+
{% if service.kind == 'angular' or service.kind == 'vue' %}
|
|
16
|
+
ports:
|
|
17
|
+
- "80:80"
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
{% endfor %}
|
|
21
|
+
{% for db in databases %}
|
|
22
|
+
{{ db.service_name }}:
|
|
23
|
+
image: {{ db.image }}
|
|
24
|
+
environment:
|
|
25
|
+
{% for key, value in db.env.items() %}
|
|
26
|
+
{{ key }}: {{ value }}
|
|
27
|
+
{% endfor %}
|
|
28
|
+
ports:
|
|
29
|
+
- "{{ db.port }}:{{ db.internal_port }}"
|
|
30
|
+
volumes:
|
|
31
|
+
- {{ db.volume_name }}:{{ db.volume_path }}
|
|
32
|
+
|
|
33
|
+
{% endfor %}
|
|
34
|
+
volumes:
|
|
35
|
+
{% for db in databases %}
|
|
36
|
+
{{ db.volume_name }}:
|
|
37
|
+
{% endfor %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Production Dockerfile for Django
|
|
2
|
+
# Generated by devctl
|
|
3
|
+
|
|
4
|
+
FROM python:3.11-slim
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Install system dependencies
|
|
9
|
+
RUN apt-get update && apk add --no-cache libpq-dev gcc
|
|
10
|
+
|
|
11
|
+
COPY requirements.txt .
|
|
12
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
13
|
+
|
|
14
|
+
COPY . .
|
|
15
|
+
|
|
16
|
+
EXPOSE 8000
|
|
17
|
+
|
|
18
|
+
# Using gunicorn for production
|
|
19
|
+
RUN pip install gunicorn
|
|
20
|
+
|
|
21
|
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "core.wsgi:application"]
|