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 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require_relative "../lib/multiapi_cli"
5
+
6
+ MultiapiCli::CLI.start(ARGV)
@@ -0,0 +1,3 @@
1
+ module MultiapiCli
2
+ VERSION = "0.1.0"
3
+ end
@@ -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: []