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.
Files changed (52) hide show
  1. package/README.md +546 -0
  2. package/adapters/__tests__/mysql.test.ts +283 -0
  3. package/adapters/jwt.ts +18 -0
  4. package/adapters/mysql.ts +98 -0
  5. package/adapters/postgres.ts +52 -0
  6. package/adapters/redis-cache.ts +64 -0
  7. package/adapters/sqlite.ts +73 -0
  8. package/adapters/vendor.d.ts +48 -0
  9. package/bin/arckode.js +7 -0
  10. package/cli/analyze.ts +506 -0
  11. package/cli/commands/db-migrate.ts +121 -0
  12. package/cli/commands/db-seed.ts +54 -0
  13. package/cli/commands/generate-api-client.ts +106 -0
  14. package/cli/commands/make-adapter.ts +132 -0
  15. package/cli/commands/make-auth.ts +297 -0
  16. package/cli/commands/make-frontend-module.ts +271 -0
  17. package/cli/commands/make-helper.ts +65 -0
  18. package/cli/commands/make-migration.ts +30 -0
  19. package/cli/commands/make-seed.ts +29 -0
  20. package/cli/generate.ts +132 -0
  21. package/cli/index.ts +604 -0
  22. package/cli/stubs/frontend-stub.ts +294 -0
  23. package/cli/stubs/fullstack-stub.ts +46 -0
  24. package/cli/stubs/module-stub.ts +469 -0
  25. package/kernel/__tests__/adapters.test.ts +101 -0
  26. package/kernel/__tests__/analyzer.test.ts +282 -0
  27. package/kernel/__tests__/framework.test.ts +617 -0
  28. package/kernel/__tests__/middlewares.test.ts +174 -0
  29. package/kernel/__tests__/static.test.ts +94 -0
  30. package/kernel/framework.ts +1851 -0
  31. package/kernel/middlewares.ts +179 -0
  32. package/kernel/static.ts +76 -0
  33. package/kernel/testing.ts +237 -0
  34. package/modules/events/index.ts +99 -0
  35. package/modules/mail/index.ts +51 -0
  36. package/modules/mail/smtp-adapter.ts +42 -0
  37. package/modules/queue/index.ts +78 -0
  38. package/modules/storage/index.ts +40 -0
  39. package/modules/storage/local-adapter.ts +41 -0
  40. package/modules/ws/__tests__/ws.test.ts +114 -0
  41. package/modules/ws/index.ts +136 -0
  42. package/package.json +99 -0
  43. package/skills/auth/SKILL.md +243 -0
  44. package/skills/cli/SKILL.md +258 -0
  45. package/skills/config/SKILL.md +253 -0
  46. package/skills/connectors/SKILL.md +259 -0
  47. package/skills/helpers/SKILL.md +206 -0
  48. package/skills/middlewares/SKILL.md +282 -0
  49. package/skills/orm/SKILL.md +260 -0
  50. package/skills/realtime/SKILL.md +307 -0
  51. package/skills/services/SKILL.md +206 -0
  52. 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
+ }
@@ -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
+ }