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.
Files changed (92) hide show
  1. devctl/__init__.py +3 -0
  2. devctl/commands/__init__.py +3 -0
  3. devctl/commands/add.py +166 -0
  4. devctl/commands/deploy.py +61 -0
  5. devctl/commands/docker.py +65 -0
  6. devctl/commands/init.py +193 -0
  7. devctl/commands/run.py +67 -0
  8. devctl/generators/__init__.py +3 -0
  9. devctl/generators/angular.py +112 -0
  10. devctl/generators/django.py +61 -0
  11. devctl/generators/docker_scaffold.py +656 -0
  12. devctl/generators/fastapi.py +67 -0
  13. devctl/generators/go_fiber.py +61 -0
  14. devctl/generators/nestjs.py +49 -0
  15. devctl/generators/nextjs.py +53 -0
  16. devctl/generators/nodejs.py +109 -0
  17. devctl/generators/react.py +43 -0
  18. devctl/generators/scaffold_angular.py +163 -0
  19. devctl/generators/scaffold_django.py +79 -0
  20. devctl/generators/scaffold_fastapi.py +83 -0
  21. devctl/generators/scaffold_go.py +67 -0
  22. devctl/generators/scaffold_nestjs.py +52 -0
  23. devctl/generators/scaffold_nextjs.py +73 -0
  24. devctl/generators/scaffold_nodejs.py +81 -0
  25. devctl/generators/scaffold_react.py +80 -0
  26. devctl/generators/scaffold_spring.py +166 -0
  27. devctl/generators/scaffold_svelte.py +73 -0
  28. devctl/generators/scaffold_vue.py +111 -0
  29. devctl/generators/spring.py +221 -0
  30. devctl/generators/svelte.py +52 -0
  31. devctl/generators/vue.py +105 -0
  32. devctl/main.py +45 -0
  33. devctl/orchestrator/__init__.py +3 -0
  34. devctl/orchestrator/config_builder.py +64 -0
  35. devctl/orchestrator/runner.py +219 -0
  36. devctl/orchestrator/scanner.py +155 -0
  37. devctl/templates/angular/config/environment.development.ts.j2 +4 -0
  38. devctl/templates/angular/config/environment.ts.j2 +5 -0
  39. devctl/templates/angular/config/proxy.conf.json.j2 +8 -0
  40. devctl/templates/angular/feature/models/request.model.ts.j2 +5 -0
  41. devctl/templates/angular/feature/models/response.model.ts.j2 +6 -0
  42. devctl/templates/angular/feature/pages/form/form.component.html.j2 +21 -0
  43. devctl/templates/angular/feature/pages/form/form.component.scss.j2 +0 -0
  44. devctl/templates/angular/feature/pages/form/form.component.ts.j2 +63 -0
  45. devctl/templates/angular/feature/pages/list/list.component.html.j2 +28 -0
  46. devctl/templates/angular/feature/pages/list/list.component.scss.j2 +0 -0
  47. devctl/templates/angular/feature/pages/list/list.component.ts.j2 +34 -0
  48. devctl/templates/angular/feature/routes.ts.j2 +9 -0
  49. devctl/templates/angular/feature/services/service.ts.j2 +34 -0
  50. devctl/templates/docker/deploy.yml.j2 +37 -0
  51. devctl/templates/docker/django/Dockerfile.j2 +21 -0
  52. devctl/templates/docker/fastapi/Dockerfile.j2 +15 -0
  53. devctl/templates/docker/frontend/Dockerfile.j2 +31 -0
  54. devctl/templates/docker/go/Dockerfile.j2 +24 -0
  55. devctl/templates/docker/nestjs/Dockerfile.j2 +26 -0
  56. devctl/templates/docker/nextjs/Dockerfile.j2 +43 -0
  57. devctl/templates/docker/nodejs/Dockerfile.j2 +24 -0
  58. devctl/templates/docker/spring/Dockerfile.j2 +24 -0
  59. devctl/templates/docker/svelte/Dockerfile.j2 +24 -0
  60. devctl/templates/proxy.conf.json.j2 +0 -0
  61. devctl/templates/spring/Controller.java.j2 +50 -0
  62. devctl/templates/spring/Entity.java.j2 +22 -0
  63. devctl/templates/spring/Repository.java.j2 +9 -0
  64. devctl/templates/spring/Service.java.j2 +20 -0
  65. devctl/templates/spring/ServiceImpl.java.j2 +62 -0
  66. devctl/templates/spring/application.properties.j2 +19 -0
  67. devctl/templates/spring/config/ApplicationConfig.java.j2 +46 -0
  68. devctl/templates/spring/config/JwtAuthenticationFilter.java.j2 +54 -0
  69. devctl/templates/spring/config/JwtService.java.j2 +68 -0
  70. devctl/templates/spring/config/SecurityConfig.java.j2 +42 -0
  71. devctl/templates/spring/docker-compose.yml.j2 +29 -0
  72. devctl/templates/spring/dto/Request.java.j2 +20 -0
  73. devctl/templates/spring/dto/Response.java.j2 +22 -0
  74. devctl/templates/spring/mapper/Mapper.java.j2 +30 -0
  75. devctl/templates/vue/config/App.vue.j2 +35 -0
  76. devctl/templates/vue/config/main.ts.j2 +9 -0
  77. devctl/templates/vue/config/router.ts.j2 +18 -0
  78. devctl/templates/vue/config/vite.config.ts.j2 +16 -0
  79. devctl/templates/vue/feature/Form.vue.j2 +162 -0
  80. devctl/templates/vue/feature/List.vue.j2 +155 -0
  81. devctl/templates/vue/feature/models.ts.j2 +12 -0
  82. devctl/templates/vue/feature/routes.ts.j2 +19 -0
  83. devctl/templates/vue/feature/service.ts.j2 +44 -0
  84. devctl/utils/__init__.py +3 -0
  85. devctl/utils/dependencies.py +36 -0
  86. devctl/utils/env_loader.py +57 -0
  87. devctl-1.0.0.dist-info/METADATA +127 -0
  88. devctl-1.0.0.dist-info/RECORD +92 -0
  89. devctl-1.0.0.dist-info/WHEEL +5 -0
  90. devctl-1.0.0.dist-info/entry_points.txt +2 -0
  91. devctl-1.0.0.dist-info/licenses/LICENSE +21 -0
  92. devctl-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,67 @@
1
+ """
2
+ Generators for FastAPI projects.
3
+ Includes boilerplate generation with Uvicorn and Pydantic.
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+
9
+ import typer
10
+
11
+
12
+ def generate_fastapi_boilerplate(project_name: str) -> bool:
13
+ """
14
+ Generates a new FastAPI project.
15
+ """
16
+ typer.secho(f"Generating FastAPI project '{project_name}'...", fg=typer.colors.CYAN)
17
+ safe_name = project_name.lower().replace("_", "-")
18
+ project_path = os.path.join(os.getcwd(), safe_name)
19
+
20
+ try:
21
+ os.makedirs(project_path, exist_ok=True)
22
+
23
+ # 1. Create main.py
24
+ main_py = """from fastapi import FastAPI
25
+
26
+ app = FastAPI(title="devctl FastAPI project")
27
+
28
+ @app.get("/")
29
+ def read_root():
30
+ return {"Hello": "from devctl FastAPI!"}
31
+
32
+ @app.get("/items/{item_id}")
33
+ def read_item(item_id: int, q: str = None):
34
+ return {"item_id": item_id, "q": q}
35
+ """
36
+ with open(os.path.join(project_path, "main.py"), "w") as f:
37
+ f.write(main_py)
38
+
39
+ # 2. Create requirements.txt
40
+ requirements = """fastapi
41
+ uvicorn[standard]
42
+ pydantic
43
+ """
44
+ with open(os.path.join(project_path, "requirements.txt"), "w") as f:
45
+ f.write(requirements)
46
+
47
+ # 3. Create virtual environment
48
+ typer.secho("Creating virtual environment...", fg=typer.colors.CYAN)
49
+ subprocess.run(["python3", "-m", "venv", ".venv"], cwd=project_path, check=True)
50
+
51
+ # 4. Install dependencies
52
+ typer.secho("Installing dependencies (fastapi, uvicorn)...", fg=typer.colors.CYAN)
53
+ # Note: on Linux it's .venv/bin/pip
54
+ pip_path = os.path.join(".venv", "bin", "pip")
55
+ subprocess.run(
56
+ [pip_path, "install", "-r", "requirements.txt"],
57
+ cwd=project_path,
58
+ check=True,
59
+ stdout=subprocess.DEVNULL,
60
+ )
61
+
62
+ typer.secho(f"FastAPI project '{safe_name}' successfully generated!", fg=typer.colors.GREEN)
63
+ return True
64
+
65
+ except Exception as e:
66
+ typer.secho(f"Error: FastAPI initialization failed: {e}", fg=typer.colors.RED)
67
+ return False
@@ -0,0 +1,61 @@
1
+ """
2
+ Generators for Go (Fiber) projects.
3
+ Includes boilerplate generation with Fiber framework.
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+
9
+ import typer
10
+
11
+
12
+ def generate_go_boilerplate(project_name: str) -> bool:
13
+ """
14
+ Generates a new Go + Fiber project.
15
+ """
16
+ typer.secho(f"Generating Go project '{project_name}'...", fg=typer.colors.CYAN)
17
+ project_path = os.path.join(os.getcwd(), project_name)
18
+
19
+ try:
20
+ os.makedirs(project_path, exist_ok=True)
21
+
22
+ # 1. Go mod init
23
+ typer.secho("Initializing Go module...", fg=typer.colors.CYAN)
24
+ subprocess.run(["go", "mod", "init", project_name], cwd=project_path, check=True)
25
+
26
+ # 2. Install Fiber
27
+ typer.secho("Installing Fiber framework...", fg=typer.colors.CYAN)
28
+ subprocess.run(
29
+ ["go", "get", "github.com/gofiber/fiber/v2"],
30
+ cwd=project_path,
31
+ check=True,
32
+ stdout=subprocess.DEVNULL,
33
+ )
34
+
35
+ # 3. Create main.go
36
+ main_go = """package main
37
+
38
+ import (
39
+ "log"
40
+ "github.com/gofiber/fiber/v2"
41
+ )
42
+
43
+ func main() {
44
+ app := fiber.New()
45
+
46
+ app.Get("/", func(c *fiber.Ctx) error {
47
+ return c.SendString("Hello from devctl Go/Fiber!")
48
+ })
49
+
50
+ log.Fatal(app.Listen(":3000"))
51
+ }
52
+ """
53
+ with open(os.path.join(project_path, "main.go"), "w") as f:
54
+ f.write(main_go)
55
+
56
+ typer.secho(f"Go project '{project_name}' successfully generated!", fg=typer.colors.GREEN)
57
+ return True
58
+
59
+ except Exception as e:
60
+ typer.secho(f"Error: Go initialization failed: {e}", fg=typer.colors.RED)
61
+ return False
@@ -0,0 +1,49 @@
1
+ """
2
+ Generators for NestJS projects.
3
+ Includes boilerplate generation via Nest CLI.
4
+ """
5
+
6
+ import subprocess
7
+
8
+ import typer
9
+
10
+
11
+ def generate_nest_boilerplate(project_name: str) -> bool:
12
+ """
13
+ Generates a new NestJS project using the Nest CLI via npx.
14
+ """
15
+ typer.secho(f"🔄 Generating NestJS project '{project_name}'...", fg=typer.colors.CYAN)
16
+ safe_name = project_name.lower().replace("_", "-")
17
+
18
+ try:
19
+ # Use npx to run Nest CLI without requiring global installation
20
+ # --package-manager npm: ensures npm is used
21
+ # --strict: enables strict mode
22
+ # --skip-git: devctl might be in a git repo already
23
+ typer.secho("Scaffolding NestJS project (this may take a minute)...", fg=typer.colors.CYAN)
24
+ subprocess.run(
25
+ [
26
+ "npx",
27
+ "-p",
28
+ "@nestjs/cli",
29
+ "nest",
30
+ "new",
31
+ safe_name,
32
+ "--package-manager",
33
+ "npm",
34
+ "--skip-git",
35
+ ],
36
+ check=True,
37
+ )
38
+
39
+ typer.secho(
40
+ f"✅ NestJS project '{safe_name}' successfully generated!", fg=typer.colors.GREEN
41
+ )
42
+ return True
43
+
44
+ except subprocess.CalledProcessError as e:
45
+ typer.secho(f"❌ Nest CLI process failed with code: {e.returncode}", fg=typer.colors.RED)
46
+ return False
47
+ except FileNotFoundError:
48
+ typer.secho("❌ Error: 'npx' or 'npm' not found in path.", fg=typer.colors.RED)
49
+ return False
@@ -0,0 +1,53 @@
1
+ """
2
+ Generators for NextJS projects.
3
+ Includes boilerplate generation via create-next-app.
4
+ """
5
+
6
+ import subprocess
7
+
8
+ import typer
9
+
10
+
11
+ def generate_nextjs_boilerplate(project_name: str) -> bool:
12
+ """
13
+ Generates a new NextJS project using create-next-app via npx.
14
+ """
15
+ typer.secho(f"🔄 Generating NextJS project '{project_name}'...", fg=typer.colors.CYAN)
16
+ safe_name = project_name.lower().replace("_", "-")
17
+
18
+ try:
19
+ typer.secho("Scaffolding NextJS project (this may take a minute)...", fg=typer.colors.CYAN)
20
+ # --ts: TypeScript
21
+ # --eslint: ESLint
22
+ # --tailwind: Tailwind CSS
23
+ # --src-dir: Use src/ directory
24
+ # --app: Use App Router
25
+ # --import-alias: alias for imports
26
+ subprocess.run(
27
+ [
28
+ "npx",
29
+ "create-next-app@latest",
30
+ safe_name,
31
+ "--ts",
32
+ "--eslint",
33
+ "--tailwind",
34
+ "--src-dir",
35
+ "--app",
36
+ "--import-alias",
37
+ "@/*",
38
+ "--use-npm",
39
+ ],
40
+ check=True,
41
+ )
42
+
43
+ typer.secho(
44
+ f"✅ NextJS project '{safe_name}' successfully generated!", fg=typer.colors.GREEN
45
+ )
46
+ return True
47
+
48
+ except subprocess.CalledProcessError as e:
49
+ typer.secho(f"❌ NextJS creation failed with code: {e.returncode}", fg=typer.colors.RED)
50
+ return False
51
+ except Exception as e:
52
+ typer.secho(f"❌ NextJS initialization failed: {e}", fg=typer.colors.RED)
53
+ return False
@@ -0,0 +1,109 @@
1
+ """
2
+ Generators for NodeJS (Express) projects.
3
+ Includes boilerplate generation with TypeScript and Express.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+
10
+ import typer
11
+
12
+
13
+ def generate_nodejs_boilerplate(project_name: str) -> bool:
14
+ """
15
+ Generates a new NodeJS + Express + TypeScript project.
16
+ """
17
+ typer.secho(f"🔄 Generating NodeJS/Express project '{project_name}'...", fg=typer.colors.CYAN)
18
+ safe_name = project_name.lower().replace("_", "-")
19
+ project_path = os.path.join(os.getcwd(), safe_name)
20
+
21
+ try:
22
+ os.makedirs(project_path, exist_ok=True)
23
+
24
+ # 1. Initialize package.json
25
+ typer.secho("Initializing package.json...", fg=typer.colors.CYAN)
26
+ subprocess.run(
27
+ ["npm", "init", "-y"], cwd=project_path, check=True, stdout=subprocess.DEVNULL
28
+ )
29
+
30
+ # 2. Install dependencies
31
+ typer.secho(
32
+ "Installing dependencies (express, typescript, ts-node, nodemon)...",
33
+ fg=typer.colors.CYAN,
34
+ )
35
+ subprocess.run(
36
+ ["npm", "install", "express", "dotenv"],
37
+ cwd=project_path,
38
+ check=True,
39
+ stdout=subprocess.DEVNULL,
40
+ )
41
+ subprocess.run(
42
+ [
43
+ "npm",
44
+ "install",
45
+ "-D",
46
+ "typescript",
47
+ "@types/node",
48
+ "@types/express",
49
+ "ts-node",
50
+ "nodemon",
51
+ "rimraf",
52
+ ],
53
+ cwd=project_path,
54
+ check=True,
55
+ stdout=subprocess.DEVNULL,
56
+ )
57
+
58
+ # 3. Initialize TypeScript
59
+ typer.secho("Configuring TypeScript...", fg=typer.colors.CYAN)
60
+ subprocess.run(
61
+ ["npx", "tsc", "--init"], cwd=project_path, check=True, stdout=subprocess.DEVNULL
62
+ )
63
+
64
+ # 4. Create folder structure
65
+ os.makedirs(os.path.join(project_path, "src"), exist_ok=True)
66
+
67
+ # 5. Create basic index.ts
68
+ index_ts = """import express, { Request, Response } from 'express';
69
+ import dotenv from 'dotenv';
70
+
71
+ dotenv.config();
72
+
73
+ const app = express();
74
+ const port = process.env.PORT || 3000;
75
+
76
+ app.use(express.json());
77
+
78
+ app.get('/', (req: Request, res: Response) => {
79
+ res.send('Hello from devctl NodeJS/Express!');
80
+ });
81
+
82
+ app.listen(port, () => {
83
+ console.log(`[server]: Server is running at http://localhost:${port}`);
84
+ });
85
+ """
86
+ with open(os.path.join(project_path, "src", "index.ts"), "w") as f:
87
+ f.write(index_ts)
88
+
89
+ # 6. Update package.json scripts
90
+ with open(os.path.join(project_path, "package.json"), "r") as f:
91
+ pkg = json.load(f)
92
+
93
+ pkg["scripts"] = {
94
+ "start": "node dist/index.js",
95
+ "build": "rimraf dist && tsc",
96
+ "dev": "nodemon src/index.ts",
97
+ }
98
+
99
+ with open(os.path.join(project_path, "package.json"), "w") as f:
100
+ json.dump(pkg, f, indent=2)
101
+
102
+ typer.secho(
103
+ f"NodeJS/Express project '{safe_name}' successfully generated!", fg=typer.colors.GREEN
104
+ )
105
+ return True
106
+
107
+ except Exception as e:
108
+ typer.secho(f"Error: NodeJS/Express initialization failed: {e}", fg=typer.colors.RED)
109
+ return False
@@ -0,0 +1,43 @@
1
+ """
2
+ Generators for ReactJS projects via Vite.
3
+ Includes boilerplate generation and basic configuration.
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+
9
+ import typer
10
+
11
+
12
+ def generate_react_boilerplate(project_name: str) -> bool:
13
+ """
14
+ Generates a new React + TypeScript project via Vite.
15
+ """
16
+ typer.secho(f"Generating ReactJS frontend '{project_name}' via Vite...", fg=typer.colors.CYAN)
17
+ safe_name = project_name.lower().replace("_", "-")
18
+
19
+ try:
20
+ typer.secho("Scaffolding React project...", fg=typer.colors.CYAN)
21
+ subprocess.run(
22
+ ["npm", "create", "vite@latest", safe_name, "--", "--template", "react-ts"],
23
+ check=True,
24
+ )
25
+
26
+ project_full_path = os.path.join(os.getcwd(), safe_name)
27
+
28
+ typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN)
29
+ subprocess.run(["npm", "install"], cwd=project_full_path, check=True)
30
+
31
+ typer.secho(
32
+ f"ReactJS frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN
33
+ )
34
+ return True
35
+
36
+ except subprocess.CalledProcessError as e:
37
+ typer.secho(
38
+ f"Error: React/Vite process failed with code: {e.returncode}", fg=typer.colors.RED
39
+ )
40
+ return False
41
+ except Exception as e:
42
+ typer.secho(f"Error: React/Vite initialization failed: {e}", fg=typer.colors.RED)
43
+ return False
@@ -0,0 +1,163 @@
1
+ """
2
+ Angular resource scaffolding generator.
3
+ Handles the creation of features including models, services, and components.
4
+ """
5
+
6
+ import os
7
+
8
+ import typer
9
+ from jinja2 import Environment, FileSystemLoader
10
+
11
+ from devctl.orchestrator.scanner import detect_environment
12
+
13
+ # Dictionary mapping CLI types to TypeScript types
14
+ TS_TYPE_MAP = {
15
+ "string": "string",
16
+ "int": "number",
17
+ "integer": "number",
18
+ "double": "number",
19
+ "float": "number",
20
+ "boolean": "boolean",
21
+ "date": "string", # In JSON, dates are typically transmitted as ISO strings
22
+ }
23
+
24
+
25
+ def parse_ts_fields(fields_str: str):
26
+ """Transforms terminal fields into TypeScript types."""
27
+ if not fields_str:
28
+ return []
29
+ parsed = []
30
+ for raw in [f.strip() for f in fields_str.split(",") if f.strip()]:
31
+ if ":" not in raw:
32
+ continue
33
+ name, field_type = raw.split(":", 1)
34
+ ts_type = TS_TYPE_MAP.get(field_type.lower().strip(), "string")
35
+ parsed.append({"name": name.strip(), "ts_type": ts_type})
36
+ return parsed
37
+
38
+
39
+ def generate_angular_resource(resource_name: str, fields_str: str, root_path: str = "."):
40
+ """
41
+ Orchestrates the creation of a complete Angular feature.
42
+ """
43
+ env_state = detect_environment(root_path)
44
+
45
+ if not env_state["has_angular"]:
46
+ typer.secho("Error: No Angular project detected here.", fg=typer.colors.RED)
47
+ raise typer.Exit(code=1)
48
+
49
+ angular_root = env_state["angular_path"]
50
+ resource_lower = resource_name.lower()
51
+ entity_name = resource_name.capitalize()
52
+
53
+ # Feature target directory: src/app/features/resource_name
54
+ feature_dir = os.path.join(angular_root, "src", "app", "features", resource_lower)
55
+
56
+ # Components to generate
57
+ components = [
58
+ # Models
59
+ {
60
+ "dir": "models",
61
+ "suffix": "-request.model",
62
+ "ext": ".ts",
63
+ "template": "models/request.model.ts.j2",
64
+ },
65
+ {
66
+ "dir": "models",
67
+ "suffix": "-response.model",
68
+ "ext": ".ts",
69
+ "template": "models/response.model.ts.j2",
70
+ },
71
+ # Service
72
+ {
73
+ "dir": "services",
74
+ "suffix": ".service",
75
+ "ext": ".ts",
76
+ "template": "services/service.ts.j2",
77
+ },
78
+ # Routes (at feature root)
79
+ {"dir": "", "suffix": ".routes", "ext": ".ts", "template": "routes.ts.j2"},
80
+ # List Component
81
+ {
82
+ "dir": f"pages/{resource_lower}-list",
83
+ "suffix": "-list.component",
84
+ "ext": ".ts",
85
+ "template": "pages/list/list.component.ts.j2",
86
+ },
87
+ {
88
+ "dir": f"pages/{resource_lower}-list",
89
+ "suffix": "-list.component",
90
+ "ext": ".html",
91
+ "template": "pages/list/list.component.html.j2",
92
+ },
93
+ {
94
+ "dir": f"pages/{resource_lower}-list",
95
+ "suffix": "-list.component",
96
+ "ext": ".scss",
97
+ "template": "pages/list/list.component.scss.j2",
98
+ },
99
+ # Form Component
100
+ {
101
+ "dir": f"pages/{resource_lower}-form",
102
+ "suffix": "-form.component",
103
+ "ext": ".ts",
104
+ "template": "pages/form/form.component.ts.j2",
105
+ },
106
+ {
107
+ "dir": f"pages/{resource_lower}-form",
108
+ "suffix": "-form.component",
109
+ "ext": ".html",
110
+ "template": "pages/form/form.component.html.j2",
111
+ },
112
+ {
113
+ "dir": f"pages/{resource_lower}-form",
114
+ "suffix": "-form.component",
115
+ "ext": ".scss",
116
+ "template": "pages/form/form.component.scss.j2",
117
+ },
118
+ ]
119
+
120
+ templates_dir = os.path.join(os.path.dirname(__file__), "..", "templates", "angular", "feature")
121
+ env = Environment(loader=FileSystemLoader(templates_dir))
122
+
123
+ typer.secho(f"Generating Angular feature '{entity_name}'...", fg=typer.colors.CYAN)
124
+
125
+ # Template data
126
+ context = {
127
+ "entity_name": entity_name,
128
+ "resource_name_lower": resource_lower,
129
+ "table_name": f"{resource_lower}s",
130
+ "uppercase_name": resource_name.upper(),
131
+ "fields": parse_ts_fields(fields_str),
132
+ }
133
+
134
+ for comp in components:
135
+ # Create target directory
136
+ target_dir = os.path.join(feature_dir, os.path.normpath(comp["dir"]))
137
+ os.makedirs(target_dir, exist_ok=True)
138
+
139
+ target_file_name = f"{resource_lower}{comp['suffix']}{comp['ext']}"
140
+
141
+ try:
142
+ template = env.get_template(comp["template"])
143
+ content = template.render(context)
144
+
145
+ with open(os.path.join(target_dir, target_file_name), "w", encoding="utf-8") as f:
146
+ f.write(content)
147
+
148
+ display_dir = comp["dir"] if comp["dir"] else "feature root"
149
+ typer.echo(f" - Created: {display_dir}/{target_file_name}")
150
+
151
+ except Exception as e:
152
+ # Tip: if no .scss.j2 template exists, create an empty file automatically
153
+ if comp["ext"] == ".scss":
154
+ with open(os.path.join(target_dir, target_file_name), "w") as f:
155
+ f.write("")
156
+ typer.echo(f" - Created (empty): {comp['dir']}/{target_file_name}")
157
+ else:
158
+ typer.secho(
159
+ f"Warning: Failed to generate {comp['template']}: {e}",
160
+ fg=typer.colors.YELLOW,
161
+ )
162
+
163
+ typer.secho(f"{entity_name} feature successfully generated!", fg=typer.colors.GREEN)
@@ -0,0 +1,79 @@
1
+ """
2
+ Django resource scaffolding generator.
3
+ Handles the creation of models, serializers, and views.
4
+ """
5
+
6
+ import os
7
+
8
+ import typer
9
+
10
+ from devctl.orchestrator.scanner import detect_environment
11
+
12
+
13
+ def generate_django_resource(resource_name: str, fields_str: str, root_path: str = "."):
14
+ """
15
+ Scaffolds a Django resource.
16
+ """
17
+ env_state = detect_environment(root_path)
18
+
19
+ if not env_state["has_django"]:
20
+ typer.secho("❌ Error: No Django project detected here.", fg=typer.colors.RED)
21
+ raise typer.Exit(code=1)
22
+
23
+ django_root = env_state["django_path"]
24
+ resource_name.lower()
25
+ entity_name = resource_name.capitalize()
26
+
27
+ # Structure: core/models.py, core/serializers.py, core/views.py
28
+ # For simplicity, we inject into the 'core' app created during init
29
+ core_dir = os.path.join(django_root, "core")
30
+ if not os.path.exists(core_dir):
31
+ os.makedirs(core_dir, exist_ok=True)
32
+
33
+ typer.secho(f"⚙️ Generating Django resource '{entity_name}'...", fg=typer.colors.CYAN)
34
+
35
+ # 1. Append Model
36
+ model_snippet = f"""
37
+ class {entity_name}(models.Model):
38
+ # Fields: {fields_str}
39
+ created_at = models.DateTimeField(auto_now_add=True)
40
+ updated_at = models.DateTimeField(auto_now=True)
41
+
42
+ def __str__(self):
43
+ return f"{entity_name} {{self.id}}"
44
+ """
45
+ with open(os.path.join(core_dir, "models.py"), "a") as f:
46
+ f.write(model_snippet)
47
+
48
+ # 2. Append Serializer
49
+ serializer_path = os.path.join(core_dir, "serializers.py")
50
+ if not os.path.exists(serializer_path):
51
+ with open(serializer_path, "w") as f:
52
+ f.write("from rest_framework import serializers\nfrom .models import *\n")
53
+
54
+ serializer_snippet = f"""
55
+ class {entity_name}Serializer(serializers.ModelSerializer):
56
+ class Meta:
57
+ model = {entity_name}
58
+ fields = '__all__'
59
+ """
60
+ with open(serializer_path, "a") as f:
61
+ f.write(serializer_snippet)
62
+
63
+ # 3. Append View
64
+ view_snippet = f"""
65
+ from rest_framework import viewsets
66
+ from .models import {entity_name}
67
+ from .serializers import {entity_name}Serializer
68
+
69
+ class {entity_name}ViewSet(viewsets.ModelViewSet):
70
+ queryset = {entity_name}.objects.all()
71
+ serializer_class = {entity_name}Serializer
72
+ """
73
+ with open(os.path.join(core_dir, "views.py"), "a") as f:
74
+ f.write(view_snippet)
75
+
76
+ typer.secho(f"✅ {entity_name} Django feature successfully generated!", fg=typer.colors.GREEN)
77
+ typer.echo(" - Updated: core/models.py")
78
+ typer.echo(" - Updated: core/serializers.py")
79
+ typer.echo(" - Updated: core/views.py")