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,22 @@
|
|
|
1
|
+
package {{ base_package }}.dto.response;
|
|
2
|
+
|
|
3
|
+
import lombok.AllArgsConstructor;
|
|
4
|
+
import lombok.Builder;
|
|
5
|
+
import lombok.Data;
|
|
6
|
+
import lombok.NoArgsConstructor;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Data Transfer Object for sending {{ entity_name }} details in a response.
|
|
10
|
+
*/
|
|
11
|
+
@Data
|
|
12
|
+
@Builder
|
|
13
|
+
@NoArgsConstructor
|
|
14
|
+
@AllArgsConstructor
|
|
15
|
+
public class {{ entity_name }}Response {
|
|
16
|
+
|
|
17
|
+
private Long id;
|
|
18
|
+
|
|
19
|
+
{% for field in fields %}
|
|
20
|
+
private {{ field.java_type }} {{ field.name }};
|
|
21
|
+
{% endfor %}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package {{ base_package }}.mapper;
|
|
2
|
+
|
|
3
|
+
import {{ base_package }}.entity.{{ entity_name }}Entity;
|
|
4
|
+
import {{ base_package }}.dto.request.{{ entity_name }}Request;
|
|
5
|
+
import {{ base_package }}.dto.response.{{ entity_name }}Response;
|
|
6
|
+
import org.mapstruct.Mapper;
|
|
7
|
+
import org.mapstruct.MappingTarget;
|
|
8
|
+
import org.mapstruct.ReportingPolicy;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mapper interface for converting between {{ entity_name }} Entity and DTOs.
|
|
12
|
+
*/
|
|
13
|
+
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
14
|
+
public interface {{ entity_name }}Mapper {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts a {{ entity_name }}Entity entity to a {{ entity_name }}Response DTO.
|
|
18
|
+
*/
|
|
19
|
+
{{ entity_name }}Response toResponse({{ entity_name }}Entity entity);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a {{ entity_name }}Request DTO to a {{ entity_name }}Entity entity.
|
|
23
|
+
*/
|
|
24
|
+
{{ entity_name }}Entity toEntity({{ entity_name }}Request request);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Updates an existing {{ entity_name }}Entity entity from a {{ entity_name }}Request DTO.
|
|
28
|
+
*/
|
|
29
|
+
void updateEntityFromRequest({{ entity_name }}Request request, @MappingTarget {{ entity_name }}Entity entity);
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Le layout principal de l'application
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<div id="app">
|
|
7
|
+
<header>
|
|
8
|
+
<nav>
|
|
9
|
+
<router-link to="/">Accueil</router-link>
|
|
10
|
+
</nav>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<main class="container">
|
|
14
|
+
<router-view />
|
|
15
|
+
</main>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<style scoped>
|
|
20
|
+
header {
|
|
21
|
+
padding: 1rem;
|
|
22
|
+
background-color: #f8f9fa;
|
|
23
|
+
border-bottom: 1px solid #e9ecef;
|
|
24
|
+
margin-bottom: 2rem;
|
|
25
|
+
}
|
|
26
|
+
nav {
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: 1rem;
|
|
29
|
+
}
|
|
30
|
+
.container {
|
|
31
|
+
max-width: 1200px;
|
|
32
|
+
margin: 0 auto;
|
|
33
|
+
padding: 0 1rem;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
// --- DEBUT DES ROUTES DEVCTL ---
|
|
4
|
+
// Les routes générées par 'devctl add resource' seront importées ici
|
|
5
|
+
// --- FIN DES ROUTES DEVCTL ---
|
|
6
|
+
|
|
7
|
+
const router = createRouter({
|
|
8
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
9
|
+
routes: [
|
|
10
|
+
{
|
|
11
|
+
path: '/',
|
|
12
|
+
name: 'home',
|
|
13
|
+
component: () => import('../components/HelloWorld.vue') // La page d'accueil par défaut de Vite
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default router
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
// https://vitejs.dev/config/
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
server: {
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': {
|
|
10
|
+
target: 'http://localhost:8080',
|
|
11
|
+
changeOrigin: true,
|
|
12
|
+
secure: false,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
4
|
+
import { {{ entity_name }}Service } from '../services/{{ entity_name }}Service'
|
|
5
|
+
import type { {{ entity_name }}Request } from '../models/{{ entity_name }}Models'
|
|
6
|
+
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
const isEdit = ref(false)
|
|
10
|
+
const id = ref<number | null>(null)
|
|
11
|
+
|
|
12
|
+
const form = ref<{{ entity_name }}Request>({
|
|
13
|
+
{% for field in fields -%}
|
|
14
|
+
{{ field.name }}: {% if field.ts_type == 'number' %}0{% elif field.ts_type == 'boolean' %}false{% else %}''{% endif %},
|
|
15
|
+
{% endfor -%}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const loading = ref(false)
|
|
19
|
+
const error = ref('')
|
|
20
|
+
|
|
21
|
+
onMounted(async () => {
|
|
22
|
+
const paramId = route.params.id
|
|
23
|
+
if (paramId) {
|
|
24
|
+
isEdit.value = true
|
|
25
|
+
id.value = parseInt(paramId as string)
|
|
26
|
+
loading.value = true
|
|
27
|
+
try {
|
|
28
|
+
const data = await {{ entity_name }}Service.getById(id.value)
|
|
29
|
+
form.value = {
|
|
30
|
+
{% for field in fields -%}
|
|
31
|
+
{{ field.name }}: data.{{ field.name }},
|
|
32
|
+
{% endfor -%}
|
|
33
|
+
}
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
error.value = err.message || 'Failed to load item.'
|
|
36
|
+
} finally {
|
|
37
|
+
loading.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const save = async () => {
|
|
43
|
+
loading.value = true
|
|
44
|
+
error.value = ''
|
|
45
|
+
try {
|
|
46
|
+
if (isEdit.value && id.value !== null) {
|
|
47
|
+
await {{ entity_name }}Service.update(id.value, form.value)
|
|
48
|
+
} else {
|
|
49
|
+
await {{ entity_name }}Service.create(form.value)
|
|
50
|
+
}
|
|
51
|
+
router.push('/{{ resource_name_lower }}s')
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
error.value = err.message || 'Failed to save item.'
|
|
54
|
+
} finally {
|
|
55
|
+
loading.value = false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cancel = () => {
|
|
60
|
+
router.push('/{{ resource_name_lower }}s')
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div class="form-container">
|
|
66
|
+
<h1>{{"{{"}} isEdit ? 'Edit' : 'Create' {{"}}"}} {{ entity_name }}</h1>
|
|
67
|
+
|
|
68
|
+
<div v-if="error" class="error-message">{{ error }}</div>
|
|
69
|
+
|
|
70
|
+
<form @submit.prevent="save" class="form">
|
|
71
|
+
{% for field in fields -%}
|
|
72
|
+
<div class="form-group">
|
|
73
|
+
<label for="{{ field.name }}">{{ field.name | capitalize }}</label>
|
|
74
|
+
{% if field.ts_type == 'boolean' -%}
|
|
75
|
+
<input type="checkbox" id="{{ field.name }}" v-model="form.{{ field.name }}" />
|
|
76
|
+
{%- elif field.ts_type == 'number' -%}
|
|
77
|
+
<input type="number" id="{{ field.name }}" v-model.number="form.{{ field.name }}" required />
|
|
78
|
+
{%- else -%}
|
|
79
|
+
<input type="text" id="{{ field.name }}" v-model="form.{{ field.name }}" required />
|
|
80
|
+
{%- endif %}
|
|
81
|
+
</div>
|
|
82
|
+
{% endfor -%}
|
|
83
|
+
|
|
84
|
+
<div class="form-actions">
|
|
85
|
+
<button type="button" @click="cancel" class="btn btn-secondary" :disabled="loading">Cancel</button>
|
|
86
|
+
<button type="submit" class="btn btn-primary" :disabled="loading">
|
|
87
|
+
{{"{{"}} loading ? 'Saving...' : 'Save' {{"}}"}}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
.form-container {
|
|
96
|
+
max-width: 600px;
|
|
97
|
+
margin: 0 auto;
|
|
98
|
+
padding: 1.5rem;
|
|
99
|
+
background: white;
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
102
|
+
}
|
|
103
|
+
.form {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
gap: 1.25rem;
|
|
107
|
+
margin-top: 1.5rem;
|
|
108
|
+
}
|
|
109
|
+
.form-group {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 0.5rem;
|
|
113
|
+
}
|
|
114
|
+
.form-group label {
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
color: #374151;
|
|
117
|
+
}
|
|
118
|
+
.form-group input[type="text"],
|
|
119
|
+
.form-group input[type="number"] {
|
|
120
|
+
padding: 0.5rem;
|
|
121
|
+
border: 1px solid #d1d5db;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
}
|
|
124
|
+
.form-group input[type="checkbox"] {
|
|
125
|
+
width: fit-content;
|
|
126
|
+
}
|
|
127
|
+
.form-actions {
|
|
128
|
+
display: flex;
|
|
129
|
+
justify-content: flex-end;
|
|
130
|
+
gap: 0.75rem;
|
|
131
|
+
margin-top: 1rem;
|
|
132
|
+
}
|
|
133
|
+
.btn {
|
|
134
|
+
padding: 0.5rem 1rem;
|
|
135
|
+
border: none;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
}
|
|
140
|
+
.btn-primary {
|
|
141
|
+
background-color: #4f46e5;
|
|
142
|
+
color: white;
|
|
143
|
+
}
|
|
144
|
+
.btn-primary:disabled {
|
|
145
|
+
background-color: #a5b4fc;
|
|
146
|
+
}
|
|
147
|
+
.btn-secondary {
|
|
148
|
+
background-color: #6b7280;
|
|
149
|
+
color: white;
|
|
150
|
+
}
|
|
151
|
+
.btn-secondary:disabled {
|
|
152
|
+
background-color: #e5e7eb;
|
|
153
|
+
color: #9ca3af;
|
|
154
|
+
}
|
|
155
|
+
.error-message {
|
|
156
|
+
padding: 1rem;
|
|
157
|
+
background-color: #fee2e2;
|
|
158
|
+
color: #ef4444;
|
|
159
|
+
border-radius: 4px;
|
|
160
|
+
margin-bottom: 1rem;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { {{ entity_name }}Service } from '../services/{{ entity_name }}Service'
|
|
5
|
+
import type { {{ entity_name }}Response } from '../models/{{ entity_name }}Models'
|
|
6
|
+
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const items = ref<{{ entity_name }}Response[]>([])
|
|
9
|
+
const loading = ref(false)
|
|
10
|
+
const error = ref('')
|
|
11
|
+
|
|
12
|
+
const loadItems = async () => {
|
|
13
|
+
loading.value = true
|
|
14
|
+
error.value = ''
|
|
15
|
+
try {
|
|
16
|
+
items.value = await {{ entity_name }}Service.getAll()
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
error.value = err.message || 'An error occurred while loading data.'
|
|
19
|
+
} finally {
|
|
20
|
+
loading.value = false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deleteItem = async (id: number) => {
|
|
25
|
+
if (!confirm('Are you sure you want to delete this item?')) return
|
|
26
|
+
try {
|
|
27
|
+
await {{ entity_name }}Service.delete(id)
|
|
28
|
+
await loadItems()
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
alert(err.message || 'Failed to delete item.')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const editItem = (id: number) => {
|
|
35
|
+
router.push(`/{{ resource_name_lower }}s/${id}/edit`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const createNew = () => {
|
|
39
|
+
router.push('/{{ resource_name_lower }}s/new')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onMounted(() => {
|
|
43
|
+
loadItems()
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div class="list-container">
|
|
49
|
+
<div class="list-header">
|
|
50
|
+
<h1>List of {{ entity_name }}s</h1>
|
|
51
|
+
<button @click="createNew" class="btn btn-primary">Create New</button>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div v-if="loading" class="loading">Loading...</div>
|
|
55
|
+
<div v-else-if="error" class="error-message">{{ error }}</div>
|
|
56
|
+
<div v-else class="table-responsive">
|
|
57
|
+
<table class="table">
|
|
58
|
+
<thead>
|
|
59
|
+
<tr>
|
|
60
|
+
<th>ID</th>
|
|
61
|
+
{% for field in fields -%}
|
|
62
|
+
<th>{{ field.name | capitalize }}</th>
|
|
63
|
+
{% endfor -%}
|
|
64
|
+
<th>Actions</th>
|
|
65
|
+
</tr>
|
|
66
|
+
</thead>
|
|
67
|
+
<tbody>
|
|
68
|
+
<tr v-for="item in items" :key="item.id">
|
|
69
|
+
<td>{{"{{"}} item.id {{"}}"}}</td>
|
|
70
|
+
{% for field in fields -%}
|
|
71
|
+
<td>{{"{{"}} item.{{ field.name }} {{"}}"}}</td>
|
|
72
|
+
{% endfor -%}
|
|
73
|
+
<td class="actions">
|
|
74
|
+
<button @click="editItem(item.id)" class="btn btn-sm btn-secondary">Edit</button>
|
|
75
|
+
<button @click="deleteItem(item.id)" class="btn btn-sm btn-danger">Delete</button>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr v-if="items.length === 0">
|
|
79
|
+
<td :colspan="{{ fields | length + 2 }}" class="text-center">No items found.</td>
|
|
80
|
+
</tr>
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.list-container {
|
|
89
|
+
padding: 1.5rem;
|
|
90
|
+
background: white;
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
93
|
+
}
|
|
94
|
+
.list-header {
|
|
95
|
+
display: flex;
|
|
96
|
+
justify-content: space-between;
|
|
97
|
+
align-items: center;
|
|
98
|
+
margin-bottom: 1.5rem;
|
|
99
|
+
}
|
|
100
|
+
.btn {
|
|
101
|
+
padding: 0.5rem 1rem;
|
|
102
|
+
border: none;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
}
|
|
107
|
+
.btn-primary {
|
|
108
|
+
background-color: #4f46e5;
|
|
109
|
+
color: white;
|
|
110
|
+
}
|
|
111
|
+
.btn-secondary {
|
|
112
|
+
background-color: #6b7280;
|
|
113
|
+
color: white;
|
|
114
|
+
}
|
|
115
|
+
.btn-danger {
|
|
116
|
+
background-color: #ef4444;
|
|
117
|
+
color: white;
|
|
118
|
+
}
|
|
119
|
+
.btn-sm {
|
|
120
|
+
padding: 0.25rem 0.5rem;
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
}
|
|
123
|
+
.table-responsive {
|
|
124
|
+
overflow-x: auto;
|
|
125
|
+
}
|
|
126
|
+
.table {
|
|
127
|
+
width: 100%;
|
|
128
|
+
border-collapse: collapse;
|
|
129
|
+
margin-top: 1rem;
|
|
130
|
+
}
|
|
131
|
+
.table th, .table td {
|
|
132
|
+
padding: 0.75rem;
|
|
133
|
+
text-align: left;
|
|
134
|
+
border-bottom: 1px solid #e5e7eb;
|
|
135
|
+
}
|
|
136
|
+
.table th {
|
|
137
|
+
background-color: #f9fafb;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
}
|
|
140
|
+
.actions {
|
|
141
|
+
display: flex;
|
|
142
|
+
gap: 0.5rem;
|
|
143
|
+
}
|
|
144
|
+
.loading, .error-message {
|
|
145
|
+
padding: 2rem;
|
|
146
|
+
text-align: center;
|
|
147
|
+
font-size: 1.125rem;
|
|
148
|
+
}
|
|
149
|
+
.error-message {
|
|
150
|
+
color: #ef4444;
|
|
151
|
+
}
|
|
152
|
+
.text-center {
|
|
153
|
+
text-align: center;
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface {{ entity_name }}Request {
|
|
2
|
+
{% for field in fields -%}
|
|
3
|
+
{{ field.name }}: {{ field.ts_type }};
|
|
4
|
+
{% endfor -%}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface {{ entity_name }}Response {
|
|
8
|
+
id: number;
|
|
9
|
+
{% for field in fields -%}
|
|
10
|
+
{{ field.name }}: {{ field.ts_type }};
|
|
11
|
+
{% endfor -%}
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
export const {{ resource_name_lower }}Routes: RouteRecordRaw[] = [
|
|
4
|
+
{
|
|
5
|
+
path: '/{{ resource_name_lower }}s',
|
|
6
|
+
name: '{{ resource_name_lower }}-list',
|
|
7
|
+
component: () => import('./views/{{ entity_name }}List.vue')
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
path: '/{{ resource_name_lower }}s/new',
|
|
11
|
+
name: '{{ resource_name_lower }}-new',
|
|
12
|
+
component: () => import('./views/{{ entity_name }}Form.vue')
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: '/{{ resource_name_lower }}s/:id/edit',
|
|
16
|
+
name: '{{ resource_name_lower }}-edit',
|
|
17
|
+
component: () => import('./views/{{ entity_name }}Form.vue')
|
|
18
|
+
}
|
|
19
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { {{ entity_name }}Request, {{ entity_name }}Response } from '../models/{{ entity_name }}Models'
|
|
2
|
+
|
|
3
|
+
export class {{ entity_name }}Service {
|
|
4
|
+
private static baseUrl = '/api/{{ resource_name_lower }}s'
|
|
5
|
+
|
|
6
|
+
static async getAll(): Promise<{{ entity_name }}Response[]> {
|
|
7
|
+
const res = await fetch(this.baseUrl)
|
|
8
|
+
if (!res.ok) throw new Error('Failed to fetch {{ resource_name_lower }}s')
|
|
9
|
+
return res.json()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static async getById(id: number): Promise<{{ entity_name }}Response> {
|
|
13
|
+
const res = await fetch(`${this.baseUrl}/${id}`)
|
|
14
|
+
if (!res.ok) throw new Error('Failed to fetch {{ resource_name_lower }}')
|
|
15
|
+
return res.json()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static async create(data: {{ entity_name }}Request): Promise<{{ entity_name }}Response> {
|
|
19
|
+
const res = await fetch(this.baseUrl, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(data)
|
|
23
|
+
})
|
|
24
|
+
if (!res.ok) throw new Error('Failed to create {{ resource_name_lower }}')
|
|
25
|
+
return res.json()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async update(id: number, data: {{ entity_name }}Request): Promise<{{ entity_name }}Response> {
|
|
29
|
+
const res = await fetch(`${this.baseUrl}/${id}`, {
|
|
30
|
+
method: 'PUT',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify(data)
|
|
33
|
+
})
|
|
34
|
+
if (!res.ok) throw new Error('Failed to update {{ resource_name_lower }}')
|
|
35
|
+
return res.json()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async delete(id: number): Promise<void> {
|
|
39
|
+
const res = await fetch(`${this.baseUrl}/${id}`, {
|
|
40
|
+
method: 'DELETE'
|
|
41
|
+
})
|
|
42
|
+
if (!res.ok) throw new Error('Failed to delete {{ resource_name_lower }}')
|
|
43
|
+
}
|
|
44
|
+
}
|
devctl/utils/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for checking system dependencies.
|
|
3
|
+
Verifies if required tools like Docker, NPM, and Java are installed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_tool(tool_name: str, required_for: str = "this operation"):
|
|
12
|
+
"""
|
|
13
|
+
Check if a tool is available in the system PATH.
|
|
14
|
+
If not, print a friendly error message and exit.
|
|
15
|
+
"""
|
|
16
|
+
if shutil.which(tool_name) is None:
|
|
17
|
+
typer.secho(
|
|
18
|
+
f"\nError: '{tool_name}' is not installed or not in your PATH.",
|
|
19
|
+
fg=typer.colors.RED,
|
|
20
|
+
bold=True,
|
|
21
|
+
)
|
|
22
|
+
typer.echo(f"It is required for {required_for}.")
|
|
23
|
+
|
|
24
|
+
# Provide helpful hints for common tools
|
|
25
|
+
hints = {
|
|
26
|
+
"docker": "Visit https://docs.docker.com/get-docker/",
|
|
27
|
+
"npm": "Install Node.js from https://nodejs.org/",
|
|
28
|
+
"java": "Install OpenJDK (e.g., version 17) from your package manager.",
|
|
29
|
+
"mvnw": "Ensure you are in a Spring Boot project root with the 'mvnw' wrapper.",
|
|
30
|
+
"ng": "Install Angular CLI using 'npm install -g @angular/cli'.",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if tool_name in hints:
|
|
34
|
+
typer.echo(f"Hint: {hints[tool_name]}")
|
|
35
|
+
|
|
36
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility for loading environment variables from .env files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_env_file(project_path: Path) -> Dict[str, str]:
|
|
11
|
+
"""
|
|
12
|
+
Reads a .env file in the given path and returns a dictionary of variables.
|
|
13
|
+
Does not override existing environment variables in os.environ by default
|
|
14
|
+
when used for process injection, but provides the mapping for merge.
|
|
15
|
+
"""
|
|
16
|
+
env_vars = {}
|
|
17
|
+
env_path = project_path / ".env"
|
|
18
|
+
|
|
19
|
+
if not env_path.exists():
|
|
20
|
+
return env_vars
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
24
|
+
for line in f:
|
|
25
|
+
line = line.strip()
|
|
26
|
+
# Skip comments and empty lines
|
|
27
|
+
if not line or line.startswith("#"):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if "=" in line:
|
|
31
|
+
key, value = line.split("=", 1)
|
|
32
|
+
# Clean key and value
|
|
33
|
+
key = key.strip()
|
|
34
|
+
value = value.strip()
|
|
35
|
+
|
|
36
|
+
# Remove quotes if present
|
|
37
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
38
|
+
value.startswith("'") and value.endswith("'")
|
|
39
|
+
):
|
|
40
|
+
value = value[1:-1]
|
|
41
|
+
|
|
42
|
+
env_vars[key] = value
|
|
43
|
+
except Exception:
|
|
44
|
+
# Fail silently to not break the runner if .env is malformed
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return env_vars
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_project_env(project_path: Path) -> Dict[str, str]:
|
|
51
|
+
"""
|
|
52
|
+
Returns a full environment dictionary merging system env and .env file.
|
|
53
|
+
"""
|
|
54
|
+
env = os.environ.copy()
|
|
55
|
+
dot_env = load_env_file(project_path)
|
|
56
|
+
env.update(dot_env)
|
|
57
|
+
return env
|