multiapi_cli 0.1.0
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.
- checksums.yaml +7 -0
- data/README.md +33 -0
- data/exe/multiapi +6 -0
- data/lib/multiapi_cli/version.rb +3 -0
- data/lib/multiapi_cli.rb +1041 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4c0b2ae858627c36ab5bd20d0ad9209a6d2c3e23202c9730e71a6d6eda5b0486
|
4
|
+
data.tar.gz: ae75a299c282a737b66319f49b26e2c3a5eb97b1c48e937d53ba7051a7281ac7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d85a70142782c93d66f07a983ac74229a10abdf4345a715376568d6241dbd97c64a4c865d918c3fa49ffc12bdb68d55ae505aeefc4eee76099b8076cb5954841
|
7
|
+
data.tar.gz: 110ffcff5e93bdf7f53475ec42265c51f294b3b93754eda00b24b31bab7b1ca6ee7b30da8d4888f147cf729366393690460ad0bb7922904a6e6243e8c0f9755e
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Multiapi CLI
|
2
|
+
|
3
|
+
Uma CLI para gerenciar projetos Multiapi, integrando geradores do Rails e Nuxt.
|
4
|
+
|
5
|
+
## Instalação
|
6
|
+
|
7
|
+
```bash
|
8
|
+
cd cli
|
9
|
+
bundle install
|
10
|
+
bundle exec rake install
|
11
|
+
```
|
12
|
+
|
13
|
+
## Uso
|
14
|
+
|
15
|
+
```bash
|
16
|
+
multiapi generate
|
17
|
+
```
|
18
|
+
|
19
|
+
A CLI irá perguntar interativamente:
|
20
|
+
- O que você deseja criar:
|
21
|
+
- Recurso Completo (API + Frontend)
|
22
|
+
- Apenas API
|
23
|
+
- Apenas Frontend
|
24
|
+
- O nome do recurso que você quer criar
|
25
|
+
|
26
|
+
## Desenvolvimento
|
27
|
+
|
28
|
+
Para desenvolver a CLI:
|
29
|
+
|
30
|
+
1. Clone o repositório
|
31
|
+
2. Execute `bundle install`
|
32
|
+
3. Faça suas alterações
|
33
|
+
4. Execute `bundle exec rake install` para testar localmente
|
data/exe/multiapi
ADDED
data/lib/multiapi_cli.rb
ADDED
@@ -0,0 +1,1041 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "tty-prompt"
|
3
|
+
require "tty-spinner"
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module MultiapiCli
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class CLI < Thor
|
11
|
+
desc "generate COMPONENT", "Gera um novo componente na estrutura Multiapi"
|
12
|
+
def generate
|
13
|
+
prompt = TTY::Prompt.new
|
14
|
+
|
15
|
+
type = prompt.select("O que você deseja criar?") do |menu|
|
16
|
+
menu.choice "Recurso Completo (API + Frontend)", :full_resource
|
17
|
+
menu.choice "Apenas API", :api_only
|
18
|
+
menu.choice "Apenas Frontend", :frontend_only
|
19
|
+
end
|
20
|
+
|
21
|
+
name = prompt.ask("Nome do recurso:", required: true)
|
22
|
+
|
23
|
+
scope = prompt.select("Qual o escopo do recurso?") do |menu|
|
24
|
+
menu.choice "Admin - Área Administrativa", :admin
|
25
|
+
menu.choice "User - Área do Usuário", :user
|
26
|
+
end
|
27
|
+
|
28
|
+
case type
|
29
|
+
when :full_resource
|
30
|
+
generate_full_resource(name, scope)
|
31
|
+
when :api_only
|
32
|
+
generate_api_resource(name, scope)
|
33
|
+
when :frontend_only
|
34
|
+
generate_frontend_resource(name, scope)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def generate_app_vue_content
|
41
|
+
<<~VUE
|
42
|
+
<template>
|
43
|
+
<NuxtLayout>
|
44
|
+
<NuxtPage />
|
45
|
+
</NuxtLayout>
|
46
|
+
</template>
|
47
|
+
VUE
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_layout_content(scope)
|
51
|
+
<<~VUE
|
52
|
+
<template>
|
53
|
+
<div class="#{scope}-layout">
|
54
|
+
<header class="#{scope}-header">
|
55
|
+
<!-- Add your header content here -->
|
56
|
+
</header>
|
57
|
+
|
58
|
+
<main class="#{scope}-main">
|
59
|
+
<slot />
|
60
|
+
</main>
|
61
|
+
|
62
|
+
<footer class="#{scope}-footer">
|
63
|
+
<!-- Add your footer content here -->
|
64
|
+
</footer>
|
65
|
+
</div>
|
66
|
+
</template>
|
67
|
+
|
68
|
+
<script setup lang="ts">
|
69
|
+
// Add your layout logic here
|
70
|
+
</script>
|
71
|
+
|
72
|
+
<style scoped>
|
73
|
+
.#{scope}-layout {
|
74
|
+
min-height: 100vh;
|
75
|
+
display: flex;
|
76
|
+
flex-direction: column;
|
77
|
+
}
|
78
|
+
|
79
|
+
.#{scope}-main {
|
80
|
+
flex: 1;
|
81
|
+
padding: 20px;
|
82
|
+
}
|
83
|
+
</style>
|
84
|
+
VUE
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_page_content(name, scope)
|
88
|
+
resource_name = name.downcase
|
89
|
+
plural_name = resource_name.pluralize
|
90
|
+
component_name = "#{scope.capitalize}#{plural_name.camelize}Index"
|
91
|
+
|
92
|
+
<<~VUE
|
93
|
+
<template>
|
94
|
+
<div class="#{plural_name}-page">
|
95
|
+
<#{component_name} />
|
96
|
+
</div>
|
97
|
+
</template>
|
98
|
+
|
99
|
+
<script setup lang="ts">
|
100
|
+
import { definePageMeta } from '#imports'
|
101
|
+
import #{component_name} from '../../components/#{scope}/#{plural_name}/index.vue'
|
102
|
+
import { use#{plural_name.camelize}Store } from '../../stores/#{scope}/#{plural_name}'
|
103
|
+
|
104
|
+
definePageMeta({
|
105
|
+
layout: '#{scope}'
|
106
|
+
})
|
107
|
+
</script>
|
108
|
+
|
109
|
+
<style scoped>
|
110
|
+
.#{plural_name}-page {
|
111
|
+
padding: 0;
|
112
|
+
}
|
113
|
+
</style>
|
114
|
+
VUE
|
115
|
+
end
|
116
|
+
|
117
|
+
def generate_component_content(name, scope)
|
118
|
+
resource_name = name.downcase
|
119
|
+
plural_name = resource_name.pluralize
|
120
|
+
singular_name = resource_name.singularize
|
121
|
+
|
122
|
+
<<~VUE
|
123
|
+
<template>
|
124
|
+
<div class="#{plural_name}-component">
|
125
|
+
<Card v-if="store.loading" class="w-full">
|
126
|
+
<CardHeader>
|
127
|
+
<CardTitle>Carregando...</CardTitle>
|
128
|
+
</CardHeader>
|
129
|
+
<CardContent class="flex items-center justify-center">
|
130
|
+
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
131
|
+
</CardContent>
|
132
|
+
</Card>
|
133
|
+
|
134
|
+
<Alert v-else-if="store.error" variant="destructive">
|
135
|
+
<AlertTitle>Erro</AlertTitle>
|
136
|
+
<AlertDescription>{{ store.error }}</AlertDescription>
|
137
|
+
</Alert>
|
138
|
+
|
139
|
+
<div v-else class="space-y-4">
|
140
|
+
<div class="flex items-center justify-between">
|
141
|
+
<h1 class="text-2xl font-bold">#{plural_name.titleize}</h1>
|
142
|
+
<Button @click="showCreateModal = true" variant="default">
|
143
|
+
<PlusIcon class="mr-2 h-4 w-4" />
|
144
|
+
Novo #{singular_name.titleize}
|
145
|
+
</Button>
|
146
|
+
</div>
|
147
|
+
|
148
|
+
<Card class="w-full">
|
149
|
+
<Table>
|
150
|
+
<TableHeader>
|
151
|
+
<TableRow>
|
152
|
+
<TableHead v-for="key in Object.keys(store.get#{plural_name.camelize}[0] || {})" :key="key">
|
153
|
+
{{ key }}
|
154
|
+
</TableHead>
|
155
|
+
<TableHead>Ações</TableHead>
|
156
|
+
</TableRow>
|
157
|
+
</TableHeader>
|
158
|
+
<TableBody>
|
159
|
+
<TableRow v-for="item in store.get#{plural_name.camelize}" :key="item.id">
|
160
|
+
<TableCell v-for="(value, key) in item" :key="key">
|
161
|
+
{{ value }}
|
162
|
+
</TableCell>
|
163
|
+
<TableCell>
|
164
|
+
<div class="flex space-x-2">
|
165
|
+
<Button @click="openEditModal(item)" variant="outline" size="sm">
|
166
|
+
<PencilIcon class="h-4 w-4" />
|
167
|
+
</Button>
|
168
|
+
<Button @click="confirmDelete(item)" variant="destructive" size="sm">
|
169
|
+
<TrashIcon class="h-4 w-4" />
|
170
|
+
</Button>
|
171
|
+
</div>
|
172
|
+
</TableCell>
|
173
|
+
</TableRow>
|
174
|
+
</TableBody>
|
175
|
+
</Table>
|
176
|
+
</Card>
|
177
|
+
|
178
|
+
<Dialog :open="showCreateModal" @update:open="showCreateModal = $event">
|
179
|
+
<DialogContent>
|
180
|
+
<DialogHeader>
|
181
|
+
<DialogTitle>Criar Novo #{singular_name.titleize}</DialogTitle>
|
182
|
+
</DialogHeader>
|
183
|
+
<#{scope}#{singular_name.camelize}Create
|
184
|
+
@success="handleCreateSuccess"
|
185
|
+
@cancel="showCreateModal = false"
|
186
|
+
/>
|
187
|
+
</DialogContent>
|
188
|
+
</Dialog>
|
189
|
+
|
190
|
+
<Dialog :open="showEditModal" @update:open="showEditModal = $event">
|
191
|
+
<DialogContent>
|
192
|
+
<DialogHeader>
|
193
|
+
<DialogTitle>Editar #{singular_name.titleize}</DialogTitle>
|
194
|
+
</DialogHeader>
|
195
|
+
<#{scope}#{singular_name.camelize}Edit
|
196
|
+
:id="selectedItem?.id"
|
197
|
+
@success="handleEditSuccess"
|
198
|
+
@cancel="showEditModal = false"
|
199
|
+
/>
|
200
|
+
</DialogContent>
|
201
|
+
</Dialog>
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
</template>
|
205
|
+
|
206
|
+
<script setup lang="ts">
|
207
|
+
import { onMounted, ref } from 'vue'
|
208
|
+
import { use#{plural_name.camelize}Store } from '../../../stores/#{scope}/#{plural_name}'
|
209
|
+
import #{scope}#{singular_name.camelize}Create from './create.vue'
|
210
|
+
import #{scope}#{singular_name.camelize}Edit from './edit.vue'
|
211
|
+
import { Button } from '@/components/ui/button'
|
212
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
213
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
214
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
215
|
+
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
216
|
+
import { PlusIcon, PencilIcon, TrashIcon } from 'lucide-vue-next'
|
217
|
+
|
218
|
+
const store = use#{plural_name.camelize}Store()
|
219
|
+
const showCreateModal = ref(false)
|
220
|
+
const showEditModal = ref(false)
|
221
|
+
const selectedItem = ref(null)
|
222
|
+
|
223
|
+
onMounted(async () => {
|
224
|
+
await store.fetch#{plural_name.camelize}()
|
225
|
+
})
|
226
|
+
|
227
|
+
const openEditModal = (item) => {
|
228
|
+
selectedItem.value = item
|
229
|
+
showEditModal.value = true
|
230
|
+
}
|
231
|
+
|
232
|
+
const handleCreateSuccess = async () => {
|
233
|
+
showCreateModal.value = false
|
234
|
+
await store.fetch#{plural_name.camelize}()
|
235
|
+
}
|
236
|
+
|
237
|
+
const handleEditSuccess = async () => {
|
238
|
+
showEditModal.value = false
|
239
|
+
await store.fetch#{plural_name.camelize}()
|
240
|
+
}
|
241
|
+
|
242
|
+
const confirmDelete = async (item) => {
|
243
|
+
if (confirm(`Deseja realmente excluir este #{singular_name}?`)) {
|
244
|
+
try {
|
245
|
+
await store.delete#{singular_name.camelize}(item.id)
|
246
|
+
await store.fetch#{plural_name.camelize}()
|
247
|
+
} catch (error) {
|
248
|
+
console.error('Erro ao excluir #{singular_name}:', error)
|
249
|
+
}
|
250
|
+
}
|
251
|
+
}
|
252
|
+
</script>
|
253
|
+
|
254
|
+
<style scoped>
|
255
|
+
.#{plural_name}-component {
|
256
|
+
padding: 0;
|
257
|
+
}
|
258
|
+
|
259
|
+
.loading {
|
260
|
+
display: flex;
|
261
|
+
flex-direction: column;
|
262
|
+
align-items: center;
|
263
|
+
justify-content: center;
|
264
|
+
padding: 40px;
|
265
|
+
}
|
266
|
+
|
267
|
+
.spinner {
|
268
|
+
width: 40px;
|
269
|
+
height: 40px;
|
270
|
+
border: 4px solid #f3f3f3;
|
271
|
+
border-top: 4px solid #3498db;
|
272
|
+
border-radius: 50%;
|
273
|
+
animation: spin 1s linear infinite;
|
274
|
+
}
|
275
|
+
|
276
|
+
@keyframes spin {
|
277
|
+
0% { transform: rotate(0deg); }
|
278
|
+
100% { transform: rotate(360deg); }
|
279
|
+
}
|
280
|
+
|
281
|
+
.error {
|
282
|
+
color: #dc3545;
|
283
|
+
padding: 20px;
|
284
|
+
text-align: center;
|
285
|
+
}
|
286
|
+
|
287
|
+
.header {
|
288
|
+
display: flex;
|
289
|
+
justify-content: space-between;
|
290
|
+
align-items: center;
|
291
|
+
margin-bottom: 20px;
|
292
|
+
}
|
293
|
+
|
294
|
+
.items {
|
295
|
+
display: grid;
|
296
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
297
|
+
gap: 20px;
|
298
|
+
}
|
299
|
+
|
300
|
+
.item {
|
301
|
+
border: 1px solid #ddd;
|
302
|
+
border-radius: 8px;
|
303
|
+
padding: 15px;
|
304
|
+
}
|
305
|
+
|
306
|
+
.item-content {
|
307
|
+
margin-bottom: 15px;
|
308
|
+
}
|
309
|
+
|
310
|
+
.field {
|
311
|
+
margin-bottom: 8px;
|
312
|
+
}
|
313
|
+
|
314
|
+
.item-actions {
|
315
|
+
display: flex;
|
316
|
+
gap: 10px;
|
317
|
+
justify-content: flex-end;
|
318
|
+
}
|
319
|
+
|
320
|
+
.modal {
|
321
|
+
position: fixed;
|
322
|
+
top: 0;
|
323
|
+
left: 0;
|
324
|
+
width: 100%;
|
325
|
+
height: 100%;
|
326
|
+
display: flex;
|
327
|
+
align-items: center;
|
328
|
+
justify-content: center;
|
329
|
+
z-index: 1000;
|
330
|
+
}
|
331
|
+
|
332
|
+
.modal-overlay {
|
333
|
+
position: absolute;
|
334
|
+
top: 0;
|
335
|
+
left: 0;
|
336
|
+
width: 100%;
|
337
|
+
height: 100%;
|
338
|
+
background: rgba(0, 0, 0, 0.5);
|
339
|
+
}
|
340
|
+
|
341
|
+
.modal-content {
|
342
|
+
position: relative;
|
343
|
+
background: white;
|
344
|
+
border-radius: 8px;
|
345
|
+
width: 90%;
|
346
|
+
max-width: 500px;
|
347
|
+
z-index: 1001;
|
348
|
+
}
|
349
|
+
|
350
|
+
.btn-primary {
|
351
|
+
background: #3498db;
|
352
|
+
color: white;
|
353
|
+
border: none;
|
354
|
+
padding: 8px 16px;
|
355
|
+
border-radius: 4px;
|
356
|
+
cursor: pointer;
|
357
|
+
}
|
358
|
+
|
359
|
+
.btn-secondary {
|
360
|
+
background: #2ecc71;
|
361
|
+
color: white;
|
362
|
+
border: none;
|
363
|
+
padding: 8px 16px;
|
364
|
+
border-radius: 4px;
|
365
|
+
cursor: pointer;
|
366
|
+
}
|
367
|
+
|
368
|
+
.btn-danger {
|
369
|
+
background: #e74c3c;
|
370
|
+
color: white;
|
371
|
+
border: none;
|
372
|
+
padding: 8px 16px;
|
373
|
+
border-radius: 4px;
|
374
|
+
cursor: pointer;
|
375
|
+
}
|
376
|
+
|
377
|
+
button:disabled {
|
378
|
+
opacity: 0.7;
|
379
|
+
cursor: not-allowed;
|
380
|
+
}
|
381
|
+
</style>
|
382
|
+
VUE
|
383
|
+
end
|
384
|
+
|
385
|
+
def resource_exists_in_api?(name)
|
386
|
+
# Verifica se existe o controller
|
387
|
+
File.exist?(File.join(api_path, "app/controllers/#{name.downcase.pluralize}_controller.rb")) ||
|
388
|
+
# Verifica se existe o model
|
389
|
+
File.exist?(File.join(api_path, "app/models/#{name.downcase.singularize}.rb"))
|
390
|
+
end
|
391
|
+
|
392
|
+
def resource_exists_in_frontend?(name, scope)
|
393
|
+
# Verifica se existe a página no diretório correto
|
394
|
+
File.exist?(File.join(frontend_path, "pages/#{scope}/#{name.downcase.pluralize}.vue")) ||
|
395
|
+
# Verifica se existe o componente no diretório correto
|
396
|
+
File.exist?(File.join(frontend_path, "components/#{scope}/#{name.downcase.pluralize}/index.vue"))
|
397
|
+
end
|
398
|
+
|
399
|
+
def api_path
|
400
|
+
File.expand_path("../../api", __dir__)
|
401
|
+
end
|
402
|
+
|
403
|
+
def frontend_path
|
404
|
+
File.expand_path("../../web", __dir__)
|
405
|
+
end
|
406
|
+
|
407
|
+
def generate_store_content(name, scope)
|
408
|
+
resource_name = name.downcase
|
409
|
+
plural_name = resource_name.pluralize
|
410
|
+
singular_name = resource_name.singularize
|
411
|
+
|
412
|
+
# Template do store usando Pinia
|
413
|
+
<<~STORE
|
414
|
+
import { defineStore } from 'pinia'
|
415
|
+
import { useApi } from '@/services/api'
|
416
|
+
|
417
|
+
export const use#{plural_name.camelize}Store = defineStore('#{scope}_#{plural_name}', {
|
418
|
+
state: () => ({
|
419
|
+
#{plural_name}: [],
|
420
|
+
#{singular_name}: null,
|
421
|
+
loading: false,
|
422
|
+
error: null
|
423
|
+
}),
|
424
|
+
|
425
|
+
actions: {
|
426
|
+
async fetch#{plural_name.camelize}() {
|
427
|
+
const api = useApi()
|
428
|
+
this.loading = true
|
429
|
+
try {
|
430
|
+
const response = await api.get('/#{plural_name}')
|
431
|
+
this.#{plural_name} = response.data
|
432
|
+
this.error = null
|
433
|
+
} catch (err) {
|
434
|
+
this.error = err.message
|
435
|
+
console.error('Error fetching #{plural_name}:', err)
|
436
|
+
} finally {
|
437
|
+
this.loading = false
|
438
|
+
}
|
439
|
+
},
|
440
|
+
|
441
|
+
async fetch#{singular_name.camelize}(id) {
|
442
|
+
const api = useApi()
|
443
|
+
this.loading = true
|
444
|
+
try {
|
445
|
+
const response = await api.get(`/#{plural_name}/${id}`)
|
446
|
+
this.#{singular_name} = response.data
|
447
|
+
this.error = null
|
448
|
+
} catch (err) {
|
449
|
+
this.error = err.message
|
450
|
+
console.error('Error fetching #{singular_name}:', err)
|
451
|
+
} finally {
|
452
|
+
this.loading = false
|
453
|
+
}
|
454
|
+
},
|
455
|
+
|
456
|
+
async create#{singular_name.camelize}(data) {
|
457
|
+
const api = useApi()
|
458
|
+
this.loading = true
|
459
|
+
try {
|
460
|
+
const response = await api.post('/#{plural_name}', data)
|
461
|
+
this.#{plural_name}.push(response.data)
|
462
|
+
this.error = null
|
463
|
+
return response.data
|
464
|
+
} catch (err) {
|
465
|
+
this.error = err.message
|
466
|
+
console.error('Error creating #{singular_name}:', err)
|
467
|
+
throw err
|
468
|
+
} finally {
|
469
|
+
this.loading = false
|
470
|
+
}
|
471
|
+
},
|
472
|
+
|
473
|
+
async update#{singular_name.camelize}(id, data) {
|
474
|
+
const api = useApi()
|
475
|
+
this.loading = true
|
476
|
+
try {
|
477
|
+
const response = await api.put(`/#{plural_name}/${id}`, data)
|
478
|
+
const index = this.#{plural_name}.findIndex(item => item.id === id)
|
479
|
+
if (index !== -1) {
|
480
|
+
this.#{plural_name}[index] = response.data
|
481
|
+
}
|
482
|
+
this.error = null
|
483
|
+
return response.data
|
484
|
+
} catch (err) {
|
485
|
+
this.error = err.message
|
486
|
+
console.error('Error updating #{singular_name}:', err)
|
487
|
+
throw err
|
488
|
+
} finally {
|
489
|
+
this.loading = false
|
490
|
+
}
|
491
|
+
},
|
492
|
+
|
493
|
+
async delete#{singular_name.camelize}(id) {
|
494
|
+
const api = useApi()
|
495
|
+
this.loading = true
|
496
|
+
try {
|
497
|
+
await api.delete(`/#{plural_name}/${id}`)
|
498
|
+
this.#{plural_name} = this.#{plural_name}.filter(item => item.id !== id)
|
499
|
+
this.error = null
|
500
|
+
} catch (err) {
|
501
|
+
this.error = err.message
|
502
|
+
console.error('Error deleting #{singular_name}:', err)
|
503
|
+
throw err
|
504
|
+
} finally {
|
505
|
+
this.loading = false
|
506
|
+
}
|
507
|
+
}
|
508
|
+
},
|
509
|
+
|
510
|
+
getters: {
|
511
|
+
get#{plural_name.camelize}: state => state.#{plural_name},
|
512
|
+
get#{singular_name.camelize}: state => state.#{singular_name},
|
513
|
+
isLoading: state => state.loading,
|
514
|
+
getError: state => state.error
|
515
|
+
}
|
516
|
+
})
|
517
|
+
STORE
|
518
|
+
end
|
519
|
+
|
520
|
+
def generate_full_resource(name, scope)
|
521
|
+
spinner = TTY::Spinner.new("[:spinner] Verificando e gerando recurso #{name} no escopo #{scope}...")
|
522
|
+
spinner.auto_spin
|
523
|
+
|
524
|
+
api_exists = resource_exists_in_api?(name)
|
525
|
+
frontend_exists = resource_exists_in_frontend?(name, scope)
|
526
|
+
|
527
|
+
if api_exists && frontend_exists
|
528
|
+
spinner.error("Recurso #{name} já existe tanto na API quanto no Frontend!")
|
529
|
+
return
|
530
|
+
end
|
531
|
+
|
532
|
+
unless api_exists
|
533
|
+
if File.directory?(api_path)
|
534
|
+
# Verifica se é o model User, que é um caso especial
|
535
|
+
if name.downcase == "user"
|
536
|
+
puts "! Model User já existe, pulando geração do model..."
|
537
|
+
# Gera apenas o controller e rotas para o User
|
538
|
+
system("cd #{api_path} && rails generate controller #{name.pluralize}")
|
539
|
+
else
|
540
|
+
# Para outros recursos, gera o scaffold completo
|
541
|
+
system("cd #{api_path} && rails generate scaffold #{name}")
|
542
|
+
end
|
543
|
+
puts "✓ API gerada com sucesso!"
|
544
|
+
else
|
545
|
+
puts "✗ Diretório da API não encontrado em #{api_path}"
|
546
|
+
end
|
547
|
+
else
|
548
|
+
puts "! API já existe, pulando geração..."
|
549
|
+
end
|
550
|
+
|
551
|
+
unless frontend_exists
|
552
|
+
if File.directory?(frontend_path)
|
553
|
+
# Cria diretórios necessários
|
554
|
+
FileUtils.mkdir_p(File.join(frontend_path, "layouts"))
|
555
|
+
FileUtils.mkdir_p(File.join(frontend_path, "pages/#{scope}"))
|
556
|
+
FileUtils.mkdir_p(File.join(frontend_path, "components/#{scope}/#{name.downcase.pluralize}"))
|
557
|
+
FileUtils.mkdir_p(File.join(frontend_path, "stores/#{scope}"))
|
558
|
+
|
559
|
+
# Gera os arquivos manualmente
|
560
|
+
# Gera a página
|
561
|
+
page_path = File.join(frontend_path, "pages/#{scope}/#{name.downcase.pluralize}.vue")
|
562
|
+
File.write(page_path, generate_page_content(name, scope))
|
563
|
+
|
564
|
+
# Gera os componentes
|
565
|
+
components_dir = File.join(frontend_path, "components/#{scope}/#{name.downcase.pluralize}")
|
566
|
+
|
567
|
+
# Componente principal (index.vue)
|
568
|
+
File.write(File.join(components_dir, "index.vue"), generate_component_content(name, scope))
|
569
|
+
|
570
|
+
# Componente de criação (create.vue)
|
571
|
+
create_path = File.join(components_dir, "create.vue")
|
572
|
+
File.write(create_path, generate_create_form_content(name, scope))
|
573
|
+
|
574
|
+
# Componente de edição (edit.vue)
|
575
|
+
edit_path = File.join(components_dir, "edit.vue")
|
576
|
+
File.write(edit_path, generate_edit_form_content(name, scope))
|
577
|
+
|
578
|
+
# Gera o store
|
579
|
+
store_path = File.join(frontend_path, "stores/#{scope}/#{name.downcase.pluralize}.ts")
|
580
|
+
File.write(store_path, generate_store_content(name, scope))
|
581
|
+
|
582
|
+
puts "✓ Frontend gerado com sucesso no escopo #{scope}!"
|
583
|
+
puts "✓ Componentes gerados:"
|
584
|
+
puts " - #{page_path}"
|
585
|
+
puts " - #{components_dir}/index.vue"
|
586
|
+
puts " - #{components_dir}/create.vue"
|
587
|
+
puts " - #{components_dir}/edit.vue"
|
588
|
+
puts " - #{store_path}"
|
589
|
+
else
|
590
|
+
puts "✗ Diretório do Frontend não encontrado em #{frontend_path}"
|
591
|
+
end
|
592
|
+
else
|
593
|
+
puts "! Frontend já existe no escopo #{scope}, pulando geração..."
|
594
|
+
end
|
595
|
+
|
596
|
+
spinner.success("Processo finalizado!")
|
597
|
+
end
|
598
|
+
|
599
|
+
def generate_api_resource(name, scope)
|
600
|
+
spinner = TTY::Spinner.new("[:spinner] Verificando e gerando API #{name}...")
|
601
|
+
spinner.auto_spin
|
602
|
+
|
603
|
+
if resource_exists_in_api?(name)
|
604
|
+
spinner.error("API #{name} já existe!")
|
605
|
+
return
|
606
|
+
end
|
607
|
+
|
608
|
+
if File.directory?(api_path)
|
609
|
+
# Verifica se é o model User, que é um caso especial
|
610
|
+
if name.downcase == "user"
|
611
|
+
puts "! Model User já existe, pulando geração do model..."
|
612
|
+
# Gera apenas o controller e rotas para o User
|
613
|
+
system("cd #{api_path} && rails generate controller #{name.pluralize}")
|
614
|
+
else
|
615
|
+
# Para outros recursos, gera o scaffold completo
|
616
|
+
system("cd #{api_path} && rails generate scaffold #{name}")
|
617
|
+
end
|
618
|
+
spinner.success("API #{name} gerada com sucesso!")
|
619
|
+
else
|
620
|
+
spinner.error("Diretório da API não encontrado em #{api_path}")
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
def extract_controller_params(name)
|
625
|
+
controller_path = File.join(api_path, "app/controllers/#{name.downcase.pluralize}_controller.rb")
|
626
|
+
return [] unless File.exist?(controller_path)
|
627
|
+
|
628
|
+
content = File.read(controller_path)
|
629
|
+
|
630
|
+
# Procura pelo método strong parameters (normalmente chamado de resource_params)
|
631
|
+
if content =~ /def\s+(?:#{name.downcase.singularize}_params|resource_params|permitted_params).*?params\.require\(:#{name.downcase.singularize}\)\.permit\((.*?)\)/m
|
632
|
+
params_string = $1
|
633
|
+
# Extrai os parâmetros permitidos
|
634
|
+
params = params_string.scan(/:(\w+)/).flatten
|
635
|
+
|
636
|
+
# Mapeia os parâmetros para seus tipos baseado em convenções comuns
|
637
|
+
params.map do |param|
|
638
|
+
field_type = case param
|
639
|
+
when /(date|time)/i
|
640
|
+
'datetime-local'
|
641
|
+
when /email/i
|
642
|
+
'email'
|
643
|
+
when /password/i
|
644
|
+
'password'
|
645
|
+
when /phone/i
|
646
|
+
'tel'
|
647
|
+
when /price|cost|value|amount/i
|
648
|
+
'number'
|
649
|
+
when /description|content|text/i
|
650
|
+
'textarea'
|
651
|
+
when /status|type|category|kind/i
|
652
|
+
'select'
|
653
|
+
when /active|enabled|published|featured/i
|
654
|
+
'checkbox'
|
655
|
+
else
|
656
|
+
'text'
|
657
|
+
end
|
658
|
+
|
659
|
+
{
|
660
|
+
name: param,
|
661
|
+
label: param.humanize,
|
662
|
+
type: field_type,
|
663
|
+
placeholder: "Digite #{param.humanize.downcase}",
|
664
|
+
required: !(param =~ /description|notes|optional/)
|
665
|
+
}
|
666
|
+
end
|
667
|
+
else
|
668
|
+
# Se não encontrar os parâmetros, retorna campos padrão
|
669
|
+
[
|
670
|
+
{
|
671
|
+
name: 'name',
|
672
|
+
label: 'Nome',
|
673
|
+
type: 'text',
|
674
|
+
placeholder: 'Digite o nome',
|
675
|
+
required: true
|
676
|
+
}
|
677
|
+
]
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
def generate_create_form_content(name, scope)
|
682
|
+
resource_name = name.downcase
|
683
|
+
plural_name = resource_name.pluralize
|
684
|
+
singular_name = resource_name.singularize
|
685
|
+
|
686
|
+
fields = extract_controller_params(name)
|
687
|
+
field_definitions = fields.map do |field|
|
688
|
+
<<~FIELD
|
689
|
+
{
|
690
|
+
name: '#{field[:name]}',
|
691
|
+
label: '#{field[:label]}',
|
692
|
+
type: '#{field[:type]}',
|
693
|
+
placeholder: '#{field[:placeholder]}',
|
694
|
+
required: #{field[:required]}
|
695
|
+
}
|
696
|
+
FIELD
|
697
|
+
end.join(",\n ")
|
698
|
+
|
699
|
+
schema_validations = fields.map do |field|
|
700
|
+
validation = if field[:required]
|
701
|
+
"#{field[:name]}: z.string().min(1, '#{field[:label]} é obrigatório')"
|
702
|
+
else
|
703
|
+
"#{field[:name]}: z.string().optional()"
|
704
|
+
end
|
705
|
+
|
706
|
+
validation
|
707
|
+
end.join(",\n ")
|
708
|
+
|
709
|
+
<<~VUE
|
710
|
+
<template>
|
711
|
+
<Form @submit="onSubmit" :validation-schema="schema" v-slot="{ errors }">
|
712
|
+
<div class="space-y-4">
|
713
|
+
<FormField
|
714
|
+
v-for="field in fields"
|
715
|
+
:key="field.name"
|
716
|
+
:name="field.name"
|
717
|
+
v-slot="{ field: formField, errorMessage }"
|
718
|
+
>
|
719
|
+
<FormItem>
|
720
|
+
<FormLabel>{{ field.label }}</FormLabel>
|
721
|
+
<FormControl>
|
722
|
+
<template v-if="field.type === 'textarea'">
|
723
|
+
<Textarea
|
724
|
+
v-bind="formField"
|
725
|
+
:placeholder="field.placeholder"
|
726
|
+
:required="field.required"
|
727
|
+
/>
|
728
|
+
</template>
|
729
|
+
<template v-else-if="field.type === 'select'">
|
730
|
+
<Select v-bind="formField">
|
731
|
+
<SelectTrigger>
|
732
|
+
<SelectValue :placeholder="field.placeholder" />
|
733
|
+
</SelectTrigger>
|
734
|
+
<SelectContent>
|
735
|
+
<SelectGroup>
|
736
|
+
<SelectLabel>{{ field.label }}</SelectLabel>
|
737
|
+
<SelectItem v-for="option in field.options" :key="option.value" :value="option.value">
|
738
|
+
{{ option.label }}
|
739
|
+
</SelectItem>
|
740
|
+
</SelectGroup>
|
741
|
+
</SelectContent>
|
742
|
+
</Select>
|
743
|
+
</template>
|
744
|
+
<template v-else-if="field.type === 'checkbox'">
|
745
|
+
<Checkbox v-bind="formField" />
|
746
|
+
</template>
|
747
|
+
<template v-else>
|
748
|
+
<Input
|
749
|
+
v-bind="formField"
|
750
|
+
:type="field.type"
|
751
|
+
:placeholder="field.placeholder"
|
752
|
+
:required="field.required"
|
753
|
+
/>
|
754
|
+
</template>
|
755
|
+
</FormControl>
|
756
|
+
<FormMessage>{{ errorMessage }}</FormMessage>
|
757
|
+
</FormItem>
|
758
|
+
</FormField>
|
759
|
+
|
760
|
+
<div class="flex justify-end space-x-2">
|
761
|
+
<Button type="button" variant="outline" @click="$emit('cancel')">
|
762
|
+
Cancelar
|
763
|
+
</Button>
|
764
|
+
<Button type="submit" :disabled="isSubmitting">
|
765
|
+
<div v-if="isSubmitting" class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
766
|
+
Criar
|
767
|
+
</Button>
|
768
|
+
</div>
|
769
|
+
</div>
|
770
|
+
</Form>
|
771
|
+
</template>
|
772
|
+
|
773
|
+
<script setup lang="ts">
|
774
|
+
import { ref } from 'vue'
|
775
|
+
import { Form } from 'vee-validate'
|
776
|
+
import * as z from 'zod'
|
777
|
+
import { toFormValidator } from '@vee-validate/zod'
|
778
|
+
import { use#{plural_name.camelize}Store } from '../../../stores/#{scope}/#{plural_name}'
|
779
|
+
import { Button } from '@/components/ui/button'
|
780
|
+
import { Input } from '@/components/ui/input'
|
781
|
+
import { Textarea } from '@/components/ui/textarea'
|
782
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
|
783
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
784
|
+
import { Form as FormRoot, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
785
|
+
|
786
|
+
const store = use#{plural_name.camelize}Store()
|
787
|
+
const isSubmitting = ref(false)
|
788
|
+
|
789
|
+
const fields = [
|
790
|
+
#{field_definitions}
|
791
|
+
]
|
792
|
+
|
793
|
+
const schema = toFormValidator(
|
794
|
+
z.object({
|
795
|
+
#{schema_validations}
|
796
|
+
})
|
797
|
+
)
|
798
|
+
|
799
|
+
const onSubmit = async (values) => {
|
800
|
+
try {
|
801
|
+
isSubmitting.value = true
|
802
|
+
await store.create#{singular_name.camelize}(values)
|
803
|
+
$emit('success')
|
804
|
+
} catch (error) {
|
805
|
+
console.error('Erro ao criar #{singular_name}:', error)
|
806
|
+
} finally {
|
807
|
+
isSubmitting.value = false
|
808
|
+
}
|
809
|
+
}
|
810
|
+
</script>
|
811
|
+
VUE
|
812
|
+
end
|
813
|
+
|
814
|
+
def generate_edit_form_content(name, scope)
|
815
|
+
resource_name = name.downcase
|
816
|
+
plural_name = resource_name.pluralize
|
817
|
+
singular_name = resource_name.singularize
|
818
|
+
|
819
|
+
fields = extract_controller_params(name)
|
820
|
+
field_definitions = fields.map do |field|
|
821
|
+
<<~FIELD
|
822
|
+
{
|
823
|
+
name: '#{field[:name]}',
|
824
|
+
label: '#{field[:label]}',
|
825
|
+
type: '#{field[:type]}',
|
826
|
+
placeholder: '#{field[:placeholder]}',
|
827
|
+
required: #{field[:required]}
|
828
|
+
}
|
829
|
+
FIELD
|
830
|
+
end.join(",\n ")
|
831
|
+
|
832
|
+
schema_validations = fields.map do |field|
|
833
|
+
validation = if field[:required]
|
834
|
+
"#{field[:name]}: z.string().min(1, '#{field[:label]} é obrigatório')"
|
835
|
+
else
|
836
|
+
"#{field[:name]}: z.string().optional()"
|
837
|
+
end
|
838
|
+
|
839
|
+
validation
|
840
|
+
end.join(",\n ")
|
841
|
+
|
842
|
+
<<~VUE
|
843
|
+
<template>
|
844
|
+
<div v-if="isLoading" class="flex items-center justify-center p-4">
|
845
|
+
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
846
|
+
</div>
|
847
|
+
|
848
|
+
<Form
|
849
|
+
v-else
|
850
|
+
@submit="onSubmit"
|
851
|
+
:validation-schema="schema"
|
852
|
+
:initial-values="formData"
|
853
|
+
v-slot="{ errors }"
|
854
|
+
>
|
855
|
+
<div class="space-y-4">
|
856
|
+
<FormField
|
857
|
+
v-for="field in fields"
|
858
|
+
:key="field.name"
|
859
|
+
:name="field.name"
|
860
|
+
v-slot="{ field: formField, errorMessage }"
|
861
|
+
>
|
862
|
+
<FormItem>
|
863
|
+
<FormLabel>{{ field.label }}</FormLabel>
|
864
|
+
<FormControl>
|
865
|
+
<template v-if="field.type === 'textarea'">
|
866
|
+
<Textarea
|
867
|
+
v-bind="formField"
|
868
|
+
:placeholder="field.placeholder"
|
869
|
+
:required="field.required"
|
870
|
+
/>
|
871
|
+
</template>
|
872
|
+
<template v-else-if="field.type === 'select'">
|
873
|
+
<Select v-bind="formField">
|
874
|
+
<SelectTrigger>
|
875
|
+
<SelectValue :placeholder="field.placeholder" />
|
876
|
+
</SelectTrigger>
|
877
|
+
<SelectContent>
|
878
|
+
<SelectGroup>
|
879
|
+
<SelectLabel>{{ field.label }}</SelectLabel>
|
880
|
+
<SelectItem v-for="option in field.options" :key="option.value" :value="option.value">
|
881
|
+
{{ option.label }}
|
882
|
+
</SelectItem>
|
883
|
+
</SelectGroup>
|
884
|
+
</SelectContent>
|
885
|
+
</Select>
|
886
|
+
</template>
|
887
|
+
<template v-else-if="field.type === 'checkbox'">
|
888
|
+
<Checkbox v-bind="formField" />
|
889
|
+
</template>
|
890
|
+
<template v-else>
|
891
|
+
<Input
|
892
|
+
v-bind="formField"
|
893
|
+
:type="field.type"
|
894
|
+
:placeholder="field.placeholder"
|
895
|
+
:required="field.required"
|
896
|
+
/>
|
897
|
+
</template>
|
898
|
+
</FormControl>
|
899
|
+
<FormMessage>{{ errorMessage }}</FormMessage>
|
900
|
+
</FormItem>
|
901
|
+
</FormField>
|
902
|
+
|
903
|
+
<div class="flex justify-end space-x-2">
|
904
|
+
<Button type="button" variant="outline" @click="$emit('cancel')">
|
905
|
+
Cancelar
|
906
|
+
</Button>
|
907
|
+
<Button type="submit" :disabled="isSubmitting">
|
908
|
+
<div v-if="isSubmitting" class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
909
|
+
Salvar
|
910
|
+
</Button>
|
911
|
+
</div>
|
912
|
+
</div>
|
913
|
+
</Form>
|
914
|
+
</template>
|
915
|
+
|
916
|
+
<script setup lang="ts">
|
917
|
+
import { ref, onMounted } from 'vue'
|
918
|
+
import { Form } from 'vee-validate'
|
919
|
+
import * as z from 'zod'
|
920
|
+
import { toFormValidator } from '@vee-validate/zod'
|
921
|
+
import { use#{plural_name.camelize}Store } from '../../../stores/#{scope}/#{plural_name}'
|
922
|
+
import { Button } from '@/components/ui/button'
|
923
|
+
import { Input } from '@/components/ui/input'
|
924
|
+
import { Textarea } from '@/components/ui/textarea'
|
925
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
|
926
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
927
|
+
import { Form as FormRoot, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
928
|
+
|
929
|
+
const props = defineProps<{
|
930
|
+
id: string | number
|
931
|
+
}>()
|
932
|
+
|
933
|
+
const store = use#{plural_name.camelize}Store()
|
934
|
+
const isSubmitting = ref(false)
|
935
|
+
const isLoading = ref(true)
|
936
|
+
const formData = ref({})
|
937
|
+
|
938
|
+
const fields = [
|
939
|
+
#{field_definitions}
|
940
|
+
]
|
941
|
+
|
942
|
+
const schema = toFormValidator(
|
943
|
+
z.object({
|
944
|
+
#{schema_validations}
|
945
|
+
})
|
946
|
+
)
|
947
|
+
|
948
|
+
onMounted(async () => {
|
949
|
+
try {
|
950
|
+
const data = await store.fetch#{singular_name.camelize}(props.id)
|
951
|
+
formData.value = data
|
952
|
+
} catch (error) {
|
953
|
+
console.error('Erro ao carregar #{singular_name}:', error)
|
954
|
+
} finally {
|
955
|
+
isLoading.value = false
|
956
|
+
}
|
957
|
+
})
|
958
|
+
|
959
|
+
const onSubmit = async (values) => {
|
960
|
+
try {
|
961
|
+
isSubmitting.value = true
|
962
|
+
await store.update#{singular_name.camelize}(props.id, values)
|
963
|
+
$emit('success')
|
964
|
+
} catch (error) {
|
965
|
+
console.error('Erro ao atualizar #{singular_name}:', error)
|
966
|
+
} finally {
|
967
|
+
isSubmitting.value = false
|
968
|
+
}
|
969
|
+
}
|
970
|
+
</script>
|
971
|
+
VUE
|
972
|
+
end
|
973
|
+
|
974
|
+
def generate_frontend_resource(name, scope)
|
975
|
+
spinner = TTY::Spinner.new("[:spinner] Verificando e gerando Frontend #{name} no escopo #{scope}...")
|
976
|
+
spinner.auto_spin
|
977
|
+
|
978
|
+
begin
|
979
|
+
if resource_exists_in_frontend?(name, scope)
|
980
|
+
spinner.error("Frontend #{name} já existe no escopo #{scope}!")
|
981
|
+
return
|
982
|
+
end
|
983
|
+
|
984
|
+
if File.directory?(frontend_path)
|
985
|
+
# Atualiza app.vue se não existir
|
986
|
+
app_vue_path = File.join(frontend_path, "app.vue")
|
987
|
+
unless File.exist?(app_vue_path)
|
988
|
+
File.write(app_vue_path, generate_app_vue_content)
|
989
|
+
end
|
990
|
+
|
991
|
+
# Cria diretórios necessários
|
992
|
+
FileUtils.mkdir_p(File.join(frontend_path, "layouts"))
|
993
|
+
FileUtils.mkdir_p(File.join(frontend_path, "pages/#{scope}"))
|
994
|
+
FileUtils.mkdir_p(File.join(frontend_path, "components/#{scope}/#{name.downcase.pluralize}"))
|
995
|
+
FileUtils.mkdir_p(File.join(frontend_path, "stores/#{scope}"))
|
996
|
+
|
997
|
+
# Gera o layout do escopo se não existir
|
998
|
+
layout_path = File.join(frontend_path, "layouts/#{scope}.vue")
|
999
|
+
unless File.exist?(layout_path)
|
1000
|
+
File.write(layout_path, generate_layout_content(scope))
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
# Gera a página
|
1004
|
+
page_path = File.join(frontend_path, "pages/#{scope}/#{name.downcase.pluralize}.vue")
|
1005
|
+
File.write(page_path, generate_page_content(name, scope))
|
1006
|
+
|
1007
|
+
# Gera os componentes
|
1008
|
+
components_dir = File.join(frontend_path, "components/#{scope}/#{name.downcase.pluralize}")
|
1009
|
+
|
1010
|
+
# Componente principal (index.vue)
|
1011
|
+
File.write(File.join(components_dir, "index.vue"), generate_component_content(name, scope))
|
1012
|
+
|
1013
|
+
# Componente de criação (create.vue)
|
1014
|
+
create_path = File.join(components_dir, "create.vue")
|
1015
|
+
File.write(create_path, generate_create_form_content(name, scope))
|
1016
|
+
|
1017
|
+
# Componente de edição (edit.vue)
|
1018
|
+
edit_path = File.join(components_dir, "edit.vue")
|
1019
|
+
File.write(edit_path, generate_edit_form_content(name, scope))
|
1020
|
+
|
1021
|
+
# Gera o store
|
1022
|
+
store_path = File.join(frontend_path, "stores/#{scope}/#{name.downcase.pluralize}.ts")
|
1023
|
+
File.write(store_path, generate_store_content(name, scope))
|
1024
|
+
|
1025
|
+
spinner.success("Frontend #{name} gerado com sucesso no escopo #{scope}!")
|
1026
|
+
puts "✓ Componentes gerados:"
|
1027
|
+
puts " - #{page_path}"
|
1028
|
+
puts " - #{components_dir}/index.vue"
|
1029
|
+
puts " - #{components_dir}/create.vue"
|
1030
|
+
puts " - #{components_dir}/edit.vue"
|
1031
|
+
puts " - #{store_path}"
|
1032
|
+
else
|
1033
|
+
spinner.error("Diretório do Frontend não encontrado em #{frontend_path}")
|
1034
|
+
end
|
1035
|
+
rescue => e
|
1036
|
+
spinner.error("Erro ao gerar frontend: #{e.message}")
|
1037
|
+
puts e.backtrace
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multiapi_cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Seu Nome
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-03-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: tty-prompt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.23.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.23.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tty-spinner
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '7.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '7.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '13.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
description: Uma CLI que integra geradores do Rails e Nuxt para projetos Multiapi
|
98
|
+
email:
|
99
|
+
- seu.email@exemplo.com
|
100
|
+
executables:
|
101
|
+
- multiapi
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- README.md
|
106
|
+
- exe/multiapi
|
107
|
+
- lib/multiapi_cli.rb
|
108
|
+
- lib/multiapi_cli/version.rb
|
109
|
+
homepage: https://github.com/seu-usuario/multiapi
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 3.0.0
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.5.11
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: CLI para gerenciar projetos Multiapi
|
132
|
+
test_files: []
|