arckode-framework 1.0.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.
- package/README.md +546 -0
- package/adapters/__tests__/mysql.test.ts +283 -0
- package/adapters/jwt.ts +18 -0
- package/adapters/mysql.ts +98 -0
- package/adapters/postgres.ts +52 -0
- package/adapters/redis-cache.ts +64 -0
- package/adapters/sqlite.ts +73 -0
- package/adapters/vendor.d.ts +48 -0
- package/bin/arckode.js +7 -0
- package/cli/analyze.ts +506 -0
- package/cli/commands/db-migrate.ts +121 -0
- package/cli/commands/db-seed.ts +54 -0
- package/cli/commands/generate-api-client.ts +106 -0
- package/cli/commands/make-adapter.ts +132 -0
- package/cli/commands/make-auth.ts +297 -0
- package/cli/commands/make-frontend-module.ts +271 -0
- package/cli/commands/make-helper.ts +65 -0
- package/cli/commands/make-migration.ts +30 -0
- package/cli/commands/make-seed.ts +29 -0
- package/cli/generate.ts +132 -0
- package/cli/index.ts +604 -0
- package/cli/stubs/frontend-stub.ts +294 -0
- package/cli/stubs/fullstack-stub.ts +46 -0
- package/cli/stubs/module-stub.ts +469 -0
- package/kernel/__tests__/adapters.test.ts +101 -0
- package/kernel/__tests__/analyzer.test.ts +282 -0
- package/kernel/__tests__/framework.test.ts +617 -0
- package/kernel/__tests__/middlewares.test.ts +174 -0
- package/kernel/__tests__/static.test.ts +94 -0
- package/kernel/framework.ts +1851 -0
- package/kernel/middlewares.ts +179 -0
- package/kernel/static.ts +76 -0
- package/kernel/testing.ts +237 -0
- package/modules/events/index.ts +99 -0
- package/modules/mail/index.ts +51 -0
- package/modules/mail/smtp-adapter.ts +42 -0
- package/modules/queue/index.ts +78 -0
- package/modules/storage/index.ts +40 -0
- package/modules/storage/local-adapter.ts +41 -0
- package/modules/ws/__tests__/ws.test.ts +114 -0
- package/modules/ws/index.ts +136 -0
- package/package.json +99 -0
- package/skills/auth/SKILL.md +243 -0
- package/skills/cli/SKILL.md +258 -0
- package/skills/config/SKILL.md +253 -0
- package/skills/connectors/SKILL.md +259 -0
- package/skills/helpers/SKILL.md +206 -0
- package/skills/middlewares/SKILL.md +282 -0
- package/skills/orm/SKILL.md +260 -0
- package/skills/realtime/SKILL.md +307 -0
- package/skills/services/SKILL.md +206 -0
- package/skills/testing/SKILL.md +257 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// cli/commands/make-frontend-module.ts — Genera módulo frontend completo
|
|
2
|
+
// Crea: API client, composable, componentes (list + form), página, router
|
|
3
|
+
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
export async function makeFrontendModule(name: string, basePath: string) {
|
|
8
|
+
const pascal = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
|
9
|
+
const kebab = name.toLowerCase()
|
|
10
|
+
const modulePath = join(basePath, 'modules', kebab)
|
|
11
|
+
|
|
12
|
+
await mkdir(join(modulePath, 'api'), { recursive: true })
|
|
13
|
+
await mkdir(join(modulePath, 'composables'), { recursive: true })
|
|
14
|
+
await mkdir(join(modulePath, 'components'), { recursive: true })
|
|
15
|
+
await mkdir(join(modulePath, 'pages'), { recursive: true })
|
|
16
|
+
await mkdir(join(modulePath, 'router'), { recursive: true })
|
|
17
|
+
|
|
18
|
+
// index.ts
|
|
19
|
+
await writeFile(join(modulePath, 'index.ts'), `// modules/${kebab}/index.ts — PUERTA PÚBLICA
|
|
20
|
+
export { use${pascal} } from './composables/use${pascal}'
|
|
21
|
+
export { default as ${pascal}Page } from './pages/${pascal}Page.vue'
|
|
22
|
+
export { default as ${pascal}List } from './components/${pascal}List.vue'
|
|
23
|
+
export { default as ${pascal}Form } from './components/${pascal}Form.vue'
|
|
24
|
+
export { ${kebab}Routes } from './router'
|
|
25
|
+
export { ${kebab}Api } from './api/${kebab}.api'
|
|
26
|
+
export type { ${pascal}DTO, Create${pascal}DTO } from './types'
|
|
27
|
+
|
|
28
|
+
export function create${pascal}Module() {
|
|
29
|
+
return { routes: ${kebab}Routes }
|
|
30
|
+
}
|
|
31
|
+
`)
|
|
32
|
+
|
|
33
|
+
// types.ts
|
|
34
|
+
await writeFile(join(modulePath, 'types.ts'), `// modules/${kebab}/types.ts
|
|
35
|
+
|
|
36
|
+
export interface ${pascal}DTO {
|
|
37
|
+
id: string
|
|
38
|
+
nombre: string
|
|
39
|
+
activo: boolean
|
|
40
|
+
createdAt: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Create${pascal}DTO {
|
|
44
|
+
nombre: string
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
|
|
48
|
+
// api.ts
|
|
49
|
+
await writeFile(join(modulePath, `api/${kebab}.api.ts`), `// modules/${kebab}/api/${kebab}.api.ts
|
|
50
|
+
|
|
51
|
+
import type { ${pascal}DTO, Create${pascal}DTO } from '../types'
|
|
52
|
+
|
|
53
|
+
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
|
|
54
|
+
|
|
55
|
+
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
|
|
56
|
+
const token = localStorage.getItem('arckode_token')
|
|
57
|
+
const res = await fetch(\`\${BASE_URL}\${path}\`, {
|
|
58
|
+
...opts,
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
...(token ? { Authorization: \`Bearer \${token}\` } : {}),
|
|
62
|
+
...opts.headers,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'Error')
|
|
66
|
+
return res.json()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PaginatedResponse<T> {
|
|
70
|
+
data: T[]
|
|
71
|
+
pagination: { page: number; limit: number; total: number; totalPages: number }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const ${kebab}Api = {
|
|
75
|
+
list: (params?: Record<string, string>) => {
|
|
76
|
+
const q = params ? \`?\${new URLSearchParams(params)}\` : ''
|
|
77
|
+
return request<PaginatedResponse<${pascal}DTO>>(\`/${kebab}\${q}\`)
|
|
78
|
+
},
|
|
79
|
+
create: (data: Create${pascal}DTO) =>
|
|
80
|
+
request<${pascal}DTO>('/${kebab}', { method: 'POST', body: JSON.stringify(data) }),
|
|
81
|
+
delete: (id: string) =>
|
|
82
|
+
request<void>(\`/${kebab}/\${id}\`, { method: 'DELETE' }),
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
|
|
86
|
+
// composable
|
|
87
|
+
await writeFile(join(modulePath, `composables/use${pascal}.ts`), `// modules/${kebab}/composables/use${pascal}.ts
|
|
88
|
+
|
|
89
|
+
import { ref, computed } from 'vue'
|
|
90
|
+
import { ${kebab}Api } from '../api/${kebab}.api'
|
|
91
|
+
import type { ${pascal}DTO, Create${pascal}DTO } from '../types'
|
|
92
|
+
|
|
93
|
+
export function use${pascal}() {
|
|
94
|
+
const items = ref<${pascal}DTO[]>([])
|
|
95
|
+
const loading = ref(false)
|
|
96
|
+
const error = ref<string | null>(null)
|
|
97
|
+
const hasItems = computed(() => items.value.length > 0)
|
|
98
|
+
|
|
99
|
+
async function list() {
|
|
100
|
+
loading.value = true; error.value = null
|
|
101
|
+
try {
|
|
102
|
+
const r = await ${kebab}Api.list()
|
|
103
|
+
items.value = r.data
|
|
104
|
+
} catch (e) { error.value = (e as Error).message; throw e }
|
|
105
|
+
finally { loading.value = false }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function create(data: Create${pascal}DTO) {
|
|
109
|
+
loading.value = true
|
|
110
|
+
try {
|
|
111
|
+
const item = await ${kebab}Api.create(data)
|
|
112
|
+
items.value.push(item)
|
|
113
|
+
return item
|
|
114
|
+
} catch (e) { error.value = (e as Error).message; throw e }
|
|
115
|
+
finally { loading.value = false }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function remove(id: string) {
|
|
119
|
+
loading.value = true
|
|
120
|
+
try {
|
|
121
|
+
await ${kebab}Api.delete(id)
|
|
122
|
+
items.value = items.value.filter(i => i.id !== id)
|
|
123
|
+
} catch (e) { error.value = (e as Error).message; throw e }
|
|
124
|
+
finally { loading.value = false }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { items, loading, error, hasItems, list, create, remove }
|
|
128
|
+
}
|
|
129
|
+
`)
|
|
130
|
+
|
|
131
|
+
// components/List.vue
|
|
132
|
+
await writeFile(join(modulePath, 'components', `${pascal}List.vue`), `<script setup lang="ts">
|
|
133
|
+
import { onMounted } from 'vue'
|
|
134
|
+
import { use${pascal} } from '../composables/use${pascal}'
|
|
135
|
+
|
|
136
|
+
const { items, loading, error, hasItems, list, remove } = use${pascal}()
|
|
137
|
+
onMounted(() => list())
|
|
138
|
+
|
|
139
|
+
function confirmarEliminar(id: string, nombre: string) {
|
|
140
|
+
if (confirm(\`¿Eliminar "\${nombre}"?\`)) remove(id)
|
|
141
|
+
}
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<template>
|
|
145
|
+
<div class="list">
|
|
146
|
+
<div v-if="loading" class="loading">Cargando...</div>
|
|
147
|
+
<div v-else-if="error" class="error">{{ error }}</div>
|
|
148
|
+
<div v-else-if="!hasItems" class="empty">No hay ${kebab} registrados</div>
|
|
149
|
+
<table v-else class="table">
|
|
150
|
+
<thead><tr><th>Nombre</th><th>Estado</th><th>Acciones</th></tr></thead>
|
|
151
|
+
<tbody>
|
|
152
|
+
<tr v-for="item in items" :key="item.id">
|
|
153
|
+
<td>{{ item.nombre }}</td>
|
|
154
|
+
<td><span :class="['badge', item.activo ? 'activo' : 'inactivo']">{{ item.activo ? 'Activo' : 'Inactivo' }}</span></td>
|
|
155
|
+
<td><button @click="confirmarEliminar(item.id, item.nombre)" class="btn-sm btn-danger">Eliminar</button></td>
|
|
156
|
+
</tr>
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
.table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; }
|
|
164
|
+
.table th { background: #f8f9fa; padding: 12px; text-align: left; font-size: 0.85em; color: #666; }
|
|
165
|
+
.table td { padding: 12px; border-top: 1px solid #eee; }
|
|
166
|
+
.badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; }
|
|
167
|
+
.badge.activo { background: #f0fdf4; color: #27ae60; }
|
|
168
|
+
.badge.inactivo { background: #fdf0ef; color: #e74c3c; }
|
|
169
|
+
.btn-sm { padding: 4px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85em; }
|
|
170
|
+
.btn-danger { background: #fee; color: #c0392b; }
|
|
171
|
+
.loading, .error, .empty { padding: 40px; text-align: center; }
|
|
172
|
+
</style>
|
|
173
|
+
`)
|
|
174
|
+
|
|
175
|
+
// components/Form.vue
|
|
176
|
+
await writeFile(join(modulePath, 'components', `${pascal}Form.vue`), `<script setup lang="ts">
|
|
177
|
+
import { ref } from 'vue'
|
|
178
|
+
import { use${pascal} } from '../composables/use${pascal}'
|
|
179
|
+
|
|
180
|
+
const emit = defineEmits<{ saved: []; canceled: [] }>()
|
|
181
|
+
const { create } = use${pascal}()
|
|
182
|
+
const nombre = ref('')
|
|
183
|
+
const submitting = ref(false)
|
|
184
|
+
const formError = ref<string | null>(null)
|
|
185
|
+
|
|
186
|
+
async function handleSubmit() {
|
|
187
|
+
if (!nombre.value.trim()) { formError.value = 'El nombre es requerido'; return }
|
|
188
|
+
submitting.value = true; formError.value = null
|
|
189
|
+
try {
|
|
190
|
+
await create({ nombre: nombre.value })
|
|
191
|
+
nombre.value = ''
|
|
192
|
+
emit('saved')
|
|
193
|
+
} catch (e) { formError.value = (e as Error).message }
|
|
194
|
+
finally { submitting.value = false }
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
199
|
+
<form @submit.prevent="handleSubmit" class="form">
|
|
200
|
+
<h3>Nuevo ${pascal}</h3>
|
|
201
|
+
<div v-if="formError" class="alert alert-error">{{ formError }}</div>
|
|
202
|
+
<div class="field">
|
|
203
|
+
<label>Nombre</label>
|
|
204
|
+
<input v-model="nombre" type="text" placeholder="Nombre" required />
|
|
205
|
+
</div>
|
|
206
|
+
<div class="actions">
|
|
207
|
+
<button type="submit" :disabled="submitting" class="btn-primary">{{ submitting ? 'Guardando...' : 'Guardar' }}</button>
|
|
208
|
+
<button type="button" @click="emit('canceled')" class="btn-secondary">Cancelar</button>
|
|
209
|
+
</div>
|
|
210
|
+
</form>
|
|
211
|
+
</template>
|
|
212
|
+
|
|
213
|
+
<style scoped>
|
|
214
|
+
.form { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
|
|
215
|
+
.field { margin-bottom: 16px; }
|
|
216
|
+
label { display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em; }
|
|
217
|
+
input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
|
|
218
|
+
.actions { display: flex; gap: 8px; }
|
|
219
|
+
.btn-primary { background: #27ae60; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
|
|
220
|
+
.btn-secondary { background: #95a5a6; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
|
|
221
|
+
.alert { padding: 8px 12px; border-radius: 4px; margin-bottom: 12px; }
|
|
222
|
+
.alert-error { background: #fdf0ef; color: #e74c3c; }
|
|
223
|
+
</style>
|
|
224
|
+
`)
|
|
225
|
+
|
|
226
|
+
// Page.vue
|
|
227
|
+
await writeFile(join(modulePath, 'pages', `${pascal}Page.vue`), `<script setup lang="ts">
|
|
228
|
+
import { ref } from 'vue'
|
|
229
|
+
import ${pascal}List from '../components/${pascal}List.vue'
|
|
230
|
+
import ${pascal}Form from '../components/${pascal}Form.vue'
|
|
231
|
+
|
|
232
|
+
const showForm = ref(false)
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<div class="page">
|
|
237
|
+
<div class="header">
|
|
238
|
+
<h1>${pascal}s</h1>
|
|
239
|
+
<button @click="showForm = !showForm" class="btn-primary">{{ showForm ? 'Cancelar' : 'Nuevo' }}</button>
|
|
240
|
+
</div>
|
|
241
|
+
<${pascal}Form v-if="showForm" @saved="showForm = false" @canceled="showForm = false" />
|
|
242
|
+
<${pascal}List />
|
|
243
|
+
</div>
|
|
244
|
+
</template>
|
|
245
|
+
|
|
246
|
+
<style scoped>
|
|
247
|
+
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
248
|
+
.btn-primary { background: #27ae60; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
|
|
249
|
+
</style>
|
|
250
|
+
`)
|
|
251
|
+
|
|
252
|
+
// router
|
|
253
|
+
await writeFile(join(modulePath, 'router', 'index.ts'), `// modules/${kebab}/router/index.ts
|
|
254
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
255
|
+
|
|
256
|
+
export const ${kebab}Routes: RouteRecordRaw[] = [
|
|
257
|
+
{
|
|
258
|
+
path: '/${kebab}',
|
|
259
|
+
name: '${kebab}',
|
|
260
|
+
component: () => import('../pages/${pascal}Page.vue'),
|
|
261
|
+
meta: { requiresAuth: true },
|
|
262
|
+
},
|
|
263
|
+
]
|
|
264
|
+
`)
|
|
265
|
+
|
|
266
|
+
console.log(`✅ Módulo frontend "${pascal}" creado en modules/${kebab}`)
|
|
267
|
+
console.log(` Registralo en main.ts:`)
|
|
268
|
+
console.log(` import { create${pascal}Module, ${kebab}Routes } from './modules/${kebab}'`)
|
|
269
|
+
console.log(` const ${kebab} = create${pascal}Module()`)
|
|
270
|
+
console.log(` routes: [...${kebab}Routes]`)
|
|
271
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// cli/commands/make-helper.ts — Generador de helpers puros
|
|
2
|
+
|
|
3
|
+
import { mkdir, writeFile, access } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
function toKebab(str: string): string {
|
|
7
|
+
return str
|
|
8
|
+
.replace(/([A-Z])/g, '-$1')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/^-/, '')
|
|
11
|
+
.replace(/[-_\s]+/g, '-')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toCamel(str: string): string {
|
|
15
|
+
return str
|
|
16
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
17
|
+
.replace(/^(.)/, c => c.toLowerCase())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function helperStub(name: string): string {
|
|
21
|
+
const kebab = toKebab(name)
|
|
22
|
+
const camel = toCamel(name)
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
`// shared/helpers/${kebab}.ts`,
|
|
26
|
+
`// Helpers puros — sin estado, sin efectos secundarios, sin dependencias externas`,
|
|
27
|
+
`// Importar directo desde cualquier módulo: import { ... } from '../../shared/helpers/${kebab}'`,
|
|
28
|
+
``,
|
|
29
|
+
`/**`,
|
|
30
|
+
` * Ejemplo: renombrá esta función con algo descriptivo del dominio.`,
|
|
31
|
+
` * Patrón A — Helper puro (ver ARCHITECTURE-REFERENCE.md)`,
|
|
32
|
+
` */`,
|
|
33
|
+
`export function ${camel}Example(input: string): string {`,
|
|
34
|
+
` return input.trim()`,
|
|
35
|
+
`}`,
|
|
36
|
+
``,
|
|
37
|
+
].join('\n')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function makeHelper(name: string, basePath: string): Promise<void> {
|
|
41
|
+
const kebab = toKebab(name)
|
|
42
|
+
const helpersPath = join(basePath, 'shared', 'helpers')
|
|
43
|
+
|
|
44
|
+
await mkdir(helpersPath, { recursive: true })
|
|
45
|
+
|
|
46
|
+
const filePath = join(helpersPath, `${kebab}.ts`)
|
|
47
|
+
|
|
48
|
+
// No sobreescribir si ya existe
|
|
49
|
+
try {
|
|
50
|
+
await access(filePath)
|
|
51
|
+
console.log(`⚠ El helper "${kebab}.ts" ya existe. Editalo directamente.`)
|
|
52
|
+
return
|
|
53
|
+
} catch {
|
|
54
|
+
// no existe → crear
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await writeFile(filePath, helperStub(name), 'utf-8')
|
|
58
|
+
|
|
59
|
+
console.log(`✅ Helper "${name}" creado en shared/helpers/${kebab}.ts`)
|
|
60
|
+
console.log(``)
|
|
61
|
+
console.log(` Importar desde cualquier módulo:`)
|
|
62
|
+
console.log(` import { ${toCamel(name)}Example } from '../../shared/helpers/${kebab}'`)
|
|
63
|
+
console.log(``)
|
|
64
|
+
console.log(` Renombrá la función de ejemplo y agregá la lógica real.`)
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// cli/commands/make-migration.ts — Generador de migraciones
|
|
2
|
+
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { ModuleStubParams } from '../stubs/module-stub'
|
|
6
|
+
import { migrationStub } from '../stubs/module-stub'
|
|
7
|
+
|
|
8
|
+
export async function makeMigration(name: string, basePath: string) {
|
|
9
|
+
const pascal = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
|
10
|
+
const kebab = name.toLowerCase()
|
|
11
|
+
|
|
12
|
+
const params: ModuleStubParams = {
|
|
13
|
+
name: pascal,
|
|
14
|
+
module: kebab,
|
|
15
|
+
fields: [
|
|
16
|
+
{ name: 'id', type: 'string', required: true },
|
|
17
|
+
{ name: 'nombre', type: 'string', required: true },
|
|
18
|
+
{ name: 'activo', type: 'boolean', required: false },
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const migrationsPath = join(basePath, 'migrations')
|
|
23
|
+
await mkdir(migrationsPath, { recursive: true })
|
|
24
|
+
|
|
25
|
+
const content = migrationStub(params)
|
|
26
|
+
const filename = `${Date.now()}_create_${kebab}.ts`
|
|
27
|
+
|
|
28
|
+
await writeFile(join(migrationsPath, filename), content, 'utf-8')
|
|
29
|
+
console.log(`✅ Migración creada: migrations/${filename}`)
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// cli/commands/make-seed.ts — Generador de seeds
|
|
2
|
+
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { ModuleStubParams } from '../stubs/module-stub'
|
|
6
|
+
import { seedStub } from '../stubs/module-stub'
|
|
7
|
+
|
|
8
|
+
export async function makeSeed(name: string, basePath: string) {
|
|
9
|
+
const pascal = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
|
10
|
+
const kebab = name.toLowerCase()
|
|
11
|
+
|
|
12
|
+
const params: ModuleStubParams = {
|
|
13
|
+
name: pascal,
|
|
14
|
+
module: kebab,
|
|
15
|
+
fields: [
|
|
16
|
+
{ name: 'nombre', type: 'string', required: true },
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const seedPath = join(basePath, 'seeds')
|
|
21
|
+
await mkdir(seedPath, { recursive: true })
|
|
22
|
+
|
|
23
|
+
const content = seedStub(params)
|
|
24
|
+
.replace(`'${pascal} de ejemplo'`, `'${pascal} #1'`)
|
|
25
|
+
.replace(`'${pascal} de ejemplo 2'`, `'${pascal} #2'`)
|
|
26
|
+
|
|
27
|
+
await writeFile(join(seedPath, `${kebab}.ts`), content, 'utf-8')
|
|
28
|
+
console.log(`✅ Seed "${pascal}" creado en seeds/${kebab}.ts`)
|
|
29
|
+
}
|
package/cli/generate.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// cli/generate.ts — Generador de código para IA
|
|
2
|
+
// SOLID: cada generator produce código 100% correcto con la arquitectura.
|
|
3
|
+
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import type { ModuleStubParams } from './stubs/module-stub'
|
|
7
|
+
import {
|
|
8
|
+
indexStub, typesStub, socketsStub,
|
|
9
|
+
serviceStub, controllerStub, validatorStub,
|
|
10
|
+
testStub, migrationStub, seedStub,
|
|
11
|
+
} from './stubs/module-stub'
|
|
12
|
+
|
|
13
|
+
type StringCase = 'pascal' | 'camel' | 'kebab'
|
|
14
|
+
|
|
15
|
+
function toCase(str: string, target: StringCase): string {
|
|
16
|
+
const p = str.replace(/[-_]/g, ' ').replace(/\w+/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).replace(/\s/g, '')
|
|
17
|
+
if (target === 'pascal') return p
|
|
18
|
+
if (target === 'camel') return p.charAt(0).toLowerCase() + p.slice(1)
|
|
19
|
+
return p.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function detectFields(name: string): { name: string; type: 'string' | 'number' | 'boolean'; required: boolean }[] {
|
|
23
|
+
return [
|
|
24
|
+
{ name: 'nombre', type: 'string', required: true },
|
|
25
|
+
{ name: 'activo', type: 'boolean', required: false },
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Generador de módulo completo ───
|
|
30
|
+
export async function generateModule(params: {
|
|
31
|
+
name: string
|
|
32
|
+
basePath: string
|
|
33
|
+
description?: string
|
|
34
|
+
fields?: Record<string, { type: 'string' | 'number' | 'boolean'; required?: boolean }>
|
|
35
|
+
}): Promise<void> {
|
|
36
|
+
const { name, basePath } = params
|
|
37
|
+
const pascal = toCase(name, 'pascal')
|
|
38
|
+
const kebab = toCase(name, 'kebab')
|
|
39
|
+
|
|
40
|
+
const userFields = params.fields ?? detectFields(name)
|
|
41
|
+
const fieldList = Object.entries(userFields).map(([n, f]) => ({
|
|
42
|
+
name: n,
|
|
43
|
+
type: f.type as 'string' | 'number' | 'boolean',
|
|
44
|
+
required: f.required ?? false,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const stubParams: ModuleStubParams = {
|
|
48
|
+
name: pascal,
|
|
49
|
+
module: kebab,
|
|
50
|
+
fields: fieldList,
|
|
51
|
+
softDelete: false,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const modulePath = join(basePath, 'modules', kebab)
|
|
55
|
+
await mkdir(join(modulePath, 'actions'), { recursive: true })
|
|
56
|
+
await mkdir(join(modulePath, 'validators'), { recursive: true })
|
|
57
|
+
await mkdir(join(modulePath, 'tests'), { recursive: true })
|
|
58
|
+
|
|
59
|
+
await writeFile(join(modulePath, 'index.ts'), indexStub(stubParams), 'utf-8')
|
|
60
|
+
await writeFile(join(modulePath, 'types.ts'), typesStub(stubParams), 'utf-8')
|
|
61
|
+
await writeFile(join(modulePath, 'sockets.ts'), socketsStub(stubParams), 'utf-8')
|
|
62
|
+
await writeFile(join(modulePath, 'actions/service.ts'), serviceStub(stubParams), 'utf-8')
|
|
63
|
+
await writeFile(join(modulePath, 'actions/controller.ts'), controllerStub(stubParams), 'utf-8')
|
|
64
|
+
await writeFile(join(modulePath, 'validators/schema.ts'), validatorStub(stubParams), 'utf-8')
|
|
65
|
+
await writeFile(join(modulePath, 'tests/service.test.ts'), testStub(stubParams), 'utf-8')
|
|
66
|
+
|
|
67
|
+
// Migration
|
|
68
|
+
const migrationsPath = join(basePath, 'migrations')
|
|
69
|
+
await mkdir(migrationsPath, { recursive: true })
|
|
70
|
+
await writeFile(join(migrationsPath, `${Date.now()}_create_${kebab}.ts`), migrationStub(stubParams), 'utf-8')
|
|
71
|
+
|
|
72
|
+
// Seed
|
|
73
|
+
const seedsPath = join(basePath, 'seeds')
|
|
74
|
+
await mkdir(seedsPath, { recursive: true })
|
|
75
|
+
await writeFile(join(seedsPath, `${kebab}.ts`), seedStub(stubParams), 'utf-8')
|
|
76
|
+
|
|
77
|
+
console.log(`✅ Módulo "${pascal}" creado en modules/${kebab}`)
|
|
78
|
+
console.log(` Archivos: index, types, sockets, service, controller, validators, tests`)
|
|
79
|
+
console.log(` Migración: migrations/..._create_${kebab}.ts`)
|
|
80
|
+
console.log(` Seed: seeds/${kebab}.ts`)
|
|
81
|
+
console.log('')
|
|
82
|
+
console.log(` Registralo en composition-root.ts:`)
|
|
83
|
+
console.log(` import { ${pascal}Module } from './modules/${kebab}'`)
|
|
84
|
+
console.log(` import { OrmRepository } from 'arckode-framework'`)
|
|
85
|
+
console.log(` import type { ${pascal}DTO } from './modules/${kebab}/types'`)
|
|
86
|
+
console.log(` orm.define('${pascal}', { table: '${kebab}', fields: { nombre: { type: 'string' } }, timestamps: true })`)
|
|
87
|
+
console.log(` const ${kebab}Repo = new OrmRepository<${pascal}DTO>(orm, '${pascal}')`)
|
|
88
|
+
console.log(` system.addModule(${pascal}Module) // pasar ${kebab}Repo al módulo`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Generador de conector ───
|
|
92
|
+
export async function generateConnector(params: {
|
|
93
|
+
name: string
|
|
94
|
+
basePath: string
|
|
95
|
+
modules: string[]
|
|
96
|
+
description?: string
|
|
97
|
+
}): Promise<void> {
|
|
98
|
+
const { name, basePath, modules, description = '' } = params
|
|
99
|
+
const camel = toCase(name, 'camel')
|
|
100
|
+
|
|
101
|
+
const connectorPath = join(basePath, 'connectors')
|
|
102
|
+
await mkdir(connectorPath, { recursive: true })
|
|
103
|
+
|
|
104
|
+
await writeFile(join(connectorPath, `${camel}.ts`), [
|
|
105
|
+
`// connectors/${camel}.ts — Conector entre módulos`,
|
|
106
|
+
`// Responsabilidad ÚNICA: conectar módulos SIN modificarlos`,
|
|
107
|
+
`// SOLID: cada conector conecta módulos, no contiene lógica de negocio`,
|
|
108
|
+
``,
|
|
109
|
+
`// Conecta: ${modules.join(' → ')}`,
|
|
110
|
+
description ? `// Descripción: ${description}` : '',
|
|
111
|
+
``,
|
|
112
|
+
`import type { ConnectorContext } from 'arckode-framework'`,
|
|
113
|
+
``,
|
|
114
|
+
`export function ${camel}(ctx: ConnectorContext): void {`,
|
|
115
|
+
modules.map(m => ` const ${toCase(m, 'camel')} = ctx.resolveModule<any>('${m}')`).join('\n'),
|
|
116
|
+
``,
|
|
117
|
+
modules.slice(1).map(m => {
|
|
118
|
+
const prev = toCase(modules[0] ?? '', 'camel')
|
|
119
|
+
const curr = toCase(m, 'camel')
|
|
120
|
+
return ` // ${prev} → ${curr}: conectar eventos`
|
|
121
|
+
}).join('\n'),
|
|
122
|
+
`}`,
|
|
123
|
+
``,
|
|
124
|
+
].join('\n'))
|
|
125
|
+
|
|
126
|
+
console.log(`✅ Conector "${camel}" creado en ${connectorPath} (conecta: ${modules.join(', ')})`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mapFieldType(type: string): string {
|
|
130
|
+
const map: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean' }
|
|
131
|
+
return map[type] ?? 'unknown'
|
|
132
|
+
}
|