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
package/cli/index.ts ADDED
@@ -0,0 +1,604 @@
1
+ #!/usr/bin/env bun
2
+ // CLI — Arckode Framework Tooling
3
+ // Punto de entrada para generación y análisis
4
+
5
+ import { mkdir, writeFile } from 'node:fs/promises'
6
+ import { join } from 'node:path'
7
+ import { generateModule, generateConnector } from './generate'
8
+ import { analyzeProject, printAnalysis, buildManifest } from './analyze'
9
+
10
+ async function main() {
11
+ const [, , cmd, ...args] = process.argv
12
+
13
+ switch (cmd) {
14
+ // ─── Crear proyecto ───
15
+ case 'new': {
16
+ const name = args[0]
17
+ if (!name) { console.log('Uso: bun arckode.ts new <nombre-proyecto> [--db=sqlite|mysql|postgres]'); return }
18
+
19
+ // Detectar DB adapter: --db=mysql | --db=postgres | default: sqlite
20
+ const dbFlag = args.find(a => a.startsWith('--db='))?.split('=')[1] ?? 'sqlite'
21
+ const db = ['sqlite', 'mysql', 'postgres'].includes(dbFlag) ? dbFlag : 'sqlite'
22
+
23
+ const base = join(process.cwd(), name)
24
+
25
+ // Crear estructura del proyecto
26
+ await mkdir(join(base, 'src', 'modules'), { recursive: true })
27
+ await mkdir(join(base, 'src', 'connectors'), { recursive: true })
28
+
29
+ // ─── CLAUDE.md ─────────────────────────────────────────────────────
30
+ await writeFile(join(base, 'CLAUDE.md'), [
31
+ `# CLAUDE.md — ${name}`,
32
+ `## Lee esto ANTES de escribir cualquier línea de código.`,
33
+ ``,
34
+ `### PASO 0 — Cargar contexto del sistema`,
35
+ `1. Leer \`arckode.json\` → saber qué módulos, rutas y modelos ya existen.`,
36
+ `2. Si no existe: \`bun run analyze --json\` para generarlo.`,
37
+ `3. Leer \`src/composition-root.ts\` → es la única fuente de verdad del sistema.`,
38
+ ``,
39
+ `### Reglas INMUTABLES (no se negocian, no se omiten)`,
40
+ ``,
41
+ `| # | Regla | Detectada por |`,
42
+ `|---|-------|--------------|`,
43
+ `| 1 | Módulos NO importan de otros módulos → usar conectores | \`arckode analyze\` |`,
44
+ `| 2 | index.ts es APPEND-ONLY → no eliminar exports | manual |`,
45
+ `| 3 | Conectores NO tienen lógica de negocio → solo delegan | \`arckode analyze\` |`,
46
+ `| 4 | Cada tabla pertenece a UN módulo | manual |`,
47
+ `| 5 | Si no está en composition-root.ts, no existe | \`arckode analyze\` |`,
48
+ `| 6 | Todo POST/PUT/PATCH requiere validateSchema() | \`arckode analyze\` |`,
49
+ `| 7 | Controller NO llama al ORM → llama al service | \`arckode analyze\` |`,
50
+ `| 8 | Service > 200 líneas → dividir | \`arckode analyze\` |`,
51
+ `| 9 | findById → verificar ownership inmediatamente (IDOR) | \`arckode analyze\` |`,
52
+ `| 10 | ORM dentro de un loop → N+1 garantizado, prohibido | \`arckode analyze\` |`,
53
+ `| 11 | Service recibe RepositoryAdapter<T>, no ORM directo | \`arckode analyze\` |`,
54
+ ``,
55
+ `### Protocolo de la IA (4 pasos obligatorios)`,
56
+ ``,
57
+ `\`\`\``,
58
+ `PASO 1 — IDENTIFICAR ALCANCE`,
59
+ ` ¿Toca 1 módulo? → Seguir`,
60
+ ` ¿Toca 1 conector? → Seguir`,
61
+ ` ¿Toca 2+ módulos? → DETENERSE. Dividir la tarea.`,
62
+ ``,
63
+ `PASO 2 — VERIFICAR REGLAS`,
64
+ ` ¿Importo de otro módulo? → NO`,
65
+ ` ¿Modifico index.ts? → Solo append`,
66
+ ` ¿Pongo lógica en el conector? → NO`,
67
+ ` ¿Comparto una tabla? → NO`,
68
+ ``,
69
+ `PASO 3 — GENERAR`,
70
+ ` Estructura obligatoria: index.ts, types.ts, sockets.ts,`,
71
+ ` actions/service.ts, actions/controller.ts,`,
72
+ ` validators/schema.ts, tests/service.test.ts`,
73
+ ``,
74
+ `PASO 4 — VERIFICAR`,
75
+ ` bun run analyze --json → violations: 0`,
76
+ ` bun test → 0 fallos`,
77
+ `\`\`\``,
78
+ ``,
79
+ `### DB Adapter activo: \`${db}\``,
80
+ ``,
81
+ `### Estructura del proyecto`,
82
+ `\`\`\``,
83
+ `src/`,
84
+ ` composition-root.ts ← TODO el sistema`,
85
+ ` modules/ ← módulos independientes (NO se importan entre sí)`,
86
+ ` connectors/ ← puentes entre módulos (sin lógica de negocio)`,
87
+ ` shared/helpers/ ← funciones puras sin estado`,
88
+ ` shared/services/ ← servicios con estado compartidos`,
89
+ `seeds/ ← datos de prueba`,
90
+ `migrations/ ← migraciones de BD`,
91
+ `\`\`\``,
92
+ ``,
93
+ `### Comandos útiles`,
94
+ `\`\`\`bash`,
95
+ `bun run dev # desarrollo con hot reload`,
96
+ `bun run analyze # verificar arquitectura (consola)`,
97
+ `bun run analyze --json # generar/actualizar arckode.json`,
98
+ `bun arckode make:module Nombre # generar módulo completo`,
99
+ `bun arckode make:connector nombre mod1 mod2 # conectar dos módulos`,
100
+ `bun test # correr todos los tests`,
101
+ `\`\`\``,
102
+ ].join('\n'))
103
+
104
+ // composition-root.ts
105
+ await writeFile(join(base, 'src', 'composition-root.ts'), [
106
+ `// composition-root.ts — ÚNICO archivo que describe TODO el sistema`,
107
+ `// La IA lee esto y sabe: módulos, conectores, dependencias.`,
108
+ `// ============================================================`,
109
+ ``,
110
+ `import {`,
111
+ ` ConfigStore, Container, Logger, ORM, Router,`,
112
+ ` NodeServer, MemoryCache, System, Auth, loadEnv,`,
113
+ `} from 'arckode-framework'`,
114
+ ...(db === 'sqlite' ? [
115
+ `import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'`,
116
+ ] : db === 'mysql' ? [
117
+ `import { MySQLAdapter } from 'arckode-framework/adapters/mysql'`,
118
+ ] : [
119
+ `import { PostgresAdapter } from 'arckode-framework/adapters/postgres'`,
120
+ ]),
121
+ `import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'`,
122
+ ``,
123
+ `// ─── 1. ENV + CONFIG ─────────────────────────`,
124
+ `// loadEnv() carga .env + .env.{NODE_ENV} — process.env tiene prioridad máxima`,
125
+ `const env = await loadEnv()`,
126
+ ``,
127
+ `const config = new ConfigStore()`,
128
+ `config.define({`,
129
+ ` PORT: { type: 'number', default: 3000 },`,
130
+ ...(db === 'sqlite' ? [
131
+ ` DB_PATH: { type: 'string', default: './data/db.sqlite' },`,
132
+ ] : db === 'mysql' ? [
133
+ ` DB_HOST: { type: 'string', required: true },`,
134
+ ` DB_USER: { type: 'string', required: true },`,
135
+ ` DB_PASSWORD:{ type: 'string', required: true },`,
136
+ ` DB_NAME: { type: 'string', required: true },`,
137
+ ` DB_PORT: { type: 'number', default: 3306 },`,
138
+ ] : [
139
+ ` DATABASE_URL: { type: 'string', required: true },`,
140
+ ]),
141
+ ` JWT_SECRET: { type: 'string', required: true },`,
142
+ ` LOG_LEVEL: { type: 'string', default: 'info' },`,
143
+ `}).load(env)`,
144
+ ``,
145
+ `// ─── 2. INFRAESTRUCTURA ──────────────────────`,
146
+ `const logger = new Logger('app', config.get('LOG_LEVEL') as any)`,
147
+ `const container = new Container()`,
148
+ ...(db === 'sqlite' ? [
149
+ `const dbAdapter = new SqliteAdapter({ path: config.get('DB_PATH') })`,
150
+ ] : db === 'mysql' ? [
151
+ `const dbAdapter = new MySQLAdapter({`,
152
+ ` host: config.get('DB_HOST'),`,
153
+ ` port: config.get<number>('DB_PORT'),`,
154
+ ` user: config.get('DB_USER'),`,
155
+ ` password: config.get('DB_PASSWORD'),`,
156
+ ` database: config.get('DB_NAME'),`,
157
+ `})`,
158
+ ] : [
159
+ `const dbAdapter = new PostgresAdapter({ connectionString: config.get('DATABASE_URL') })`,
160
+ ]),
161
+ `await dbAdapter.connect()`,
162
+ `const orm = new ORM(dbAdapter)`,
163
+ `const router = new Router()`,
164
+ `const http = new NodeServer(config.get<number>('PORT'), logger)`,
165
+ `const cache = new MemoryCache()`,
166
+ `const auth = new Auth(jwtTokenAdapter, config.get('JWT_SECRET'), logger)`,
167
+ ``,
168
+ `// ─── 3. MODELOS (cada módulo es dueño de su ModelDefinition) ─`,
169
+ `// import { MiModuloModel } from './modules/mi-modulo/types'`,
170
+ `// orm.define('MiModulo', MiModuloModel)`,
171
+ ``,
172
+ `// ─── 4. MIGRAR ───────────────────────────────`,
173
+ `await orm.migrate()`,
174
+ ``,
175
+ `// ─── 5. REGISTRAR ────────────────────────────`,
176
+ `container.register('config', () => config)`,
177
+ `container.register('logger', () => logger)`,
178
+ `container.register('db', () => dbAdapter, () => dbAdapter.close())`,
179
+ `container.register('orm', () => orm)`,
180
+ `container.register('cache', () => cache)`,
181
+ `container.init()`,
182
+ ``,
183
+ `// ─── 6. SISTEMA ───────────────────────────────`,
184
+ `const system = new System({`,
185
+ ` config, container, logger, orm, router, http, cache, auth,`,
186
+ `})`,
187
+ ``,
188
+ `// Registrar módulos — usar factory functions generadas por arckode make:module:`,
189
+ `// import { MiModuloModule } from './modules/mi-modulo'`,
190
+ `// system.addModule(MiModuloModule())`,
191
+ ``,
192
+ `// Conectar módulos:`,
193
+ `// system.addConnector('nombre', (ctx) => {`,
194
+ `// const modA = ctx.resolveModule('mod-a')`,
195
+ `// ctx.resolveModule('mod-b', { onEvento: async (data) => modA.accion(data) })`,
196
+ `// })`,
197
+ ``,
198
+ `// ─── 7. RUTAS ─────────────────────────────────`,
199
+ `router.get('/health', async () => ({`,
200
+ ` status: 200, body: { status: 'ok', uptime: process.uptime() }`,
201
+ `}))`,
202
+ ``,
203
+ `// ─── 8. SEEDS (solo en desarrollo) ────────────`,
204
+ `if (env.RUN_SEEDS === 'true') {`,
205
+ ` // await seeds.runAll()`,
206
+ `}`,
207
+ ``,
208
+ `// ─── 9. INICIAR ───────────────────────────────`,
209
+ `await system.start()`,
210
+ ``,
211
+ `process.on('SIGINT', async () => { await system.stop(); process.exit(0) })`,
212
+ `process.on('SIGTERM', async () => { await system.stop(); process.exit(0) })`,
213
+ ``,
214
+ ].join('\n'))
215
+
216
+ // package.json — dependencias según el DB elegido
217
+ const dbDeps: Record<string, string> = db === 'mysql'
218
+ ? { 'mysql2': '^3.0.0' }
219
+ : db === 'postgres'
220
+ ? { 'pg': '^8.0.0' }
221
+ : { 'better-sqlite3': '^11.0.0' }
222
+
223
+ const dbDevDeps: Record<string, string> = db === 'postgres'
224
+ ? { '@types/pg': '^8.0.0' }
225
+ : db === 'sqlite'
226
+ ? { '@types/better-sqlite3': '^7.6.0' }
227
+ : {}
228
+
229
+ await writeFile(join(base, 'package.json'), JSON.stringify({
230
+ name,
231
+ version: '1.0.0',
232
+ type: 'module',
233
+ scripts: {
234
+ dev: `bun --watch src/composition-root.ts`,
235
+ start: `bun run src/composition-root.ts`,
236
+ test: `bun test`,
237
+ 'test:watch': `bun test --watch`,
238
+ analyze: `bun run node_modules/arckode-framework/cli/index.ts analyze`,
239
+ 'analyze:json': `bun run node_modules/arckode-framework/cli/index.ts analyze --json`,
240
+ },
241
+ dependencies: {
242
+ 'arckode-framework': 'latest',
243
+ 'jsonwebtoken': '^9.0.0',
244
+ ...dbDeps,
245
+ },
246
+ devDependencies: {
247
+ '@types/jsonwebtoken': '^9.0.0',
248
+ ...dbDevDeps,
249
+ },
250
+ }, null, 2))
251
+
252
+ // .env según el DB elegido
253
+ const envContent = db === 'mysql'
254
+ ? `PORT=3000\nDB_HOST=localhost\nDB_PORT=3306\nDB_USER=root\nDB_PASSWORD=secret\nDB_NAME=${name}\nJWT_SECRET=cambiar-en-produccion\nLOG_LEVEL=info\n`
255
+ : db === 'postgres'
256
+ ? `PORT=3000\nDATABASE_URL=postgres://user:password@localhost:5432/${name}\nJWT_SECRET=cambiar-en-produccion\nLOG_LEVEL=info\n`
257
+ : `PORT=3000\nDB_PATH=./data/db.sqlite\nJWT_SECRET=cambiar-en-produccion\nLOG_LEVEL=info\n`
258
+
259
+ await writeFile(join(base, '.env'), envContent)
260
+
261
+ // .env.example
262
+ await writeFile(join(base, '.env.example'), `PORT=3000
263
+ DB_PATH=./data/db.sqlite
264
+ JWT_SECRET=cambiar-en-produccion
265
+ LOG_LEVEL=info
266
+ `)
267
+
268
+ // .gitignore
269
+ await writeFile(join(base, '.gitignore'), `node_modules/
270
+ dist/
271
+ data/
272
+ .env
273
+ `)
274
+
275
+ // Crear data/
276
+ await mkdir(join(base, 'data'), { recursive: true })
277
+
278
+ console.log(`\n✅ Proyecto "${name}" creado (DB: ${db})`)
279
+ console.log(`\n Siguientes pasos:`)
280
+ console.log(` cd ${name}`)
281
+ console.log(` bun install`)
282
+ console.log(` bun run dev`)
283
+ console.log(`\n La IA arranca leyendo CLAUDE.md → arckode.json`)
284
+ break
285
+ }
286
+
287
+ // ─── Generar módulo ───
288
+ case 'make:module': {
289
+ const name = args[0]
290
+ if (!name) { console.log('Uso: bun arckode.ts make:module <Nombre>'); return }
291
+
292
+ const basePath = join(process.cwd(), 'src')
293
+
294
+ // Preguntar campos (versión simple con defaults)
295
+ await generateModule({
296
+ name,
297
+ basePath,
298
+ description: `Módulo de ${name}`,
299
+ fields: {
300
+ nombre: { type: 'string', required: true },
301
+ activo: { type: 'boolean', required: false },
302
+ },
303
+ })
304
+ break
305
+ }
306
+
307
+ // ─── Generar conector ───
308
+ case 'make:connector': {
309
+ const name = args[0]
310
+ const modules = args.slice(1)
311
+ if (!name || modules.length < 2) {
312
+ console.log('Uso: bun arckode.ts make:connector <nombre> <mod1> <mod2> ...')
313
+ return
314
+ }
315
+
316
+ const basePath = join(process.cwd(), 'src')
317
+ await generateConnector({ name, basePath, modules })
318
+ break
319
+ }
320
+
321
+ // ─── Analizar arquitectura ───
322
+ case 'analyze': {
323
+ const basePath = process.cwd()
324
+ const result = await analyzeProject(basePath)
325
+
326
+ if (args.includes('--json')) {
327
+ const manifest = buildManifest(result)
328
+ const dest = join(basePath, 'arckode.json')
329
+ await writeFile(dest, JSON.stringify(manifest, null, 2))
330
+ const status = result.valid ? '✅ válido' : `❌ ${result.violations.length} violaciones`
331
+ console.log(`arckode.json generado — ${status}`)
332
+ } else {
333
+ printAnalysis(result)
334
+ }
335
+ break
336
+ }
337
+
338
+ // ─── Generar auth ───
339
+ case 'make:auth': {
340
+ const { makeAuth } = await import('./commands/make-auth')
341
+ await makeAuth(join(process.cwd(), 'src'))
342
+ break
343
+ }
344
+
345
+ // ─── Generar helper puro ───
346
+ case 'make:helper': {
347
+ const helperName = args[0]
348
+ if (!helperName) { console.log('Uso: bun arckode.ts make:helper <nombre>'); return }
349
+ const { makeHelper } = await import('./commands/make-helper')
350
+ await makeHelper(helperName, join(process.cwd(), 'src'))
351
+ break
352
+ }
353
+
354
+ // ─── Generar adapter de librería externa ───
355
+ case 'make:adapter': {
356
+ const adapterName = args[0]
357
+ const interfaceName = args[1]
358
+ if (!adapterName || !interfaceName) {
359
+ console.log('Uso: bun arckode.ts make:adapter <NombreAdapter> <NombreInterfaz>')
360
+ console.log('Ejemplo: bun arckode.ts make:adapter Stripe PaymentAdapter')
361
+ return
362
+ }
363
+ const { makeAdapter } = await import('./commands/make-adapter')
364
+ await makeAdapter(adapterName, interfaceName, join(process.cwd(), 'src'))
365
+ break
366
+ }
367
+
368
+ // ─── Generar seed ───
369
+ case 'make:seed': {
370
+ const seedName = args[0]
371
+ if (!seedName) { console.log('Uso: bun arckode.ts make:seed <Nombre>'); return }
372
+ const { makeSeed } = await import('./commands/make-seed')
373
+ await makeSeed(seedName, join(process.cwd(), 'src'))
374
+ break
375
+ }
376
+
377
+ // ─── Ejecutar seeds ───
378
+ case 'db:seed': {
379
+ const { dbSeed } = await import('./commands/db-seed')
380
+ await dbSeed(args[0])
381
+ break
382
+ }
383
+
384
+ // ─── Generar migración ───
385
+ case 'make:migration': {
386
+ const migName = args[0]
387
+ if (!migName) { console.log('Uso: bun arckode.ts make:migration <nombre>'); return }
388
+ const { makeMigration } = await import('./commands/make-migration')
389
+ await makeMigration(migName, join(process.cwd(), 'src'))
390
+ break
391
+ }
392
+
393
+ // ─── Nuevo frontend SPA ───
394
+ case 'new:frontend': {
395
+ const feName = args[0] ?? 'frontend'
396
+ const dest = join(process.cwd(), feName)
397
+ await mkdir(dest, { recursive: true })
398
+ // Copiar templates del frontend
399
+ const { cp } = await import('node:fs/promises')
400
+ const templateDir = join(import.meta.dirname, '..', 'templates', 'frontend')
401
+ await cp(templateDir, dest, { recursive: true })
402
+ console.log(`✅ Frontend SPA creado en ${dest}`)
403
+ console.log(` cd ${feName}`)
404
+ console.log(` npm install`)
405
+ console.log(` npm run dev`)
406
+ break
407
+ }
408
+
409
+ // ─── Generar módulo frontend ───
410
+ case 'make:page': {
411
+ const pageName = args[0]
412
+ if (!pageName) { console.log('Uso: bun arckode.ts make:page <Nombre>'); return }
413
+ const { makeFrontendModule } = await import('./commands/make-frontend-module')
414
+ await makeFrontendModule(pageName, join(process.cwd(), 'src'))
415
+ break
416
+ }
417
+
418
+ // ─── Generar API clients ───
419
+ case 'generate:api': {
420
+ const fePath = args[0] ?? join(process.cwd(), '..', 'frontend')
421
+ const { generateApiClient } = await import('./commands/generate-api-client')
422
+ await generateApiClient(process.cwd(), fePath)
423
+ break
424
+ }
425
+
426
+ // ─── Listar rutas (análisis estático) ───
427
+ case 'routes': {
428
+ const { readdir, readFile, stat } = await import('node:fs/promises')
429
+ const { existsSync } = await import('node:fs')
430
+ const modulesPath = join(process.cwd(), 'src', 'modules')
431
+
432
+ if (!existsSync(modulesPath)) {
433
+ console.log('❌ No se encuentra src/modules/')
434
+ break
435
+ }
436
+
437
+ const methods = ['get', 'post', 'put', 'patch', 'delete'] as const
438
+ const methodColors: Record<string, string> = {
439
+ get: '\x1b[32m',
440
+ post: '\x1b[33m',
441
+ put: '\x1b[34m',
442
+ patch: '\x1b[35m',
443
+ delete: '\x1b[31m',
444
+ }
445
+ const reset = '\x1b[0m'
446
+
447
+ // Recolecta todos los .ts de un directorio recursivamente
448
+ async function collectTsFiles(dir: string): Promise<string[]> {
449
+ const files: string[] = []
450
+ if (!existsSync(dir)) return files
451
+ for (const entry of await readdir(dir)) {
452
+ const full = join(dir, entry)
453
+ const s = await stat(full)
454
+ if (s.isDirectory()) files.push(...await collectTsFiles(full))
455
+ else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts') && !entry.endsWith('.d.ts')) files.push(full)
456
+ }
457
+ return files
458
+ }
459
+
460
+ // Extrae rutas de un bloque de código fuente — deduplica por método+path
461
+ function extractRoutes(src: string, seen: Set<string>): string[] {
462
+ const found: string[] = []
463
+ for (const method of methods) {
464
+ // Soporta comillas simples, dobles y backticks sin interpolación
465
+ const re = new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`\${}]+)['"\`]`, 'g')
466
+ let match: RegExpExecArray | null
467
+ while ((match = re.exec(src)) !== null) {
468
+ const key = `${method}:${match[1]}`
469
+ if (seen.has(key)) continue
470
+ seen.add(key)
471
+ const color = methodColors[method]
472
+ found.push(` ${color}${method.toUpperCase().padEnd(7)}${reset} ${match[1]}`)
473
+ }
474
+ }
475
+ return found
476
+ }
477
+
478
+ console.log('\n📋 Rutas registradas (análisis estático)\n')
479
+
480
+ const moduleDirs = await readdir(modulesPath)
481
+ let total = 0
482
+ const globalSeen = new Set<string>()
483
+
484
+ for (const mod of moduleDirs) {
485
+ const modDir = join(modulesPath, mod)
486
+ const s = await stat(modDir).catch(() => null)
487
+ if (!s?.isDirectory()) continue
488
+
489
+ const files = await collectTsFiles(modDir)
490
+ const seen = new Set<string>()
491
+ const routes: string[] = []
492
+
493
+ for (const file of files) {
494
+ const src = await readFile(file, 'utf-8')
495
+ routes.push(...extractRoutes(src, seen))
496
+ }
497
+
498
+ // Marcar en el set global para no duplicar con composition-root
499
+ for (const key of seen) globalSeen.add(key)
500
+
501
+ if (routes.length > 0) {
502
+ console.log(`[${mod}]`)
503
+ routes.forEach(r => console.log(r))
504
+ console.log()
505
+ total += routes.length
506
+ }
507
+ }
508
+
509
+ // Rutas globales en composition-root (no registradas por ningún módulo)
510
+ const crPath = join(process.cwd(), 'src', 'composition-root.ts')
511
+ if (existsSync(crPath)) {
512
+ const src = await readFile(crPath, 'utf-8')
513
+ const globalRoutes: string[] = []
514
+ for (const method of methods) {
515
+ // Soporta comillas simples, dobles y backticks sin interpolación
516
+ const re = new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`\${}]+)['"\`]`, 'g')
517
+ let match: RegExpExecArray | null
518
+ while ((match = re.exec(src)) !== null) {
519
+ const key = `${method}:${match[1]}`
520
+ if (globalSeen.has(key)) continue
521
+ globalSeen.add(key)
522
+ const color = methodColors[method]
523
+ globalRoutes.push(` ${color}${method.toUpperCase().padEnd(7)}${reset} ${match[1]}`)
524
+ }
525
+ }
526
+ if (globalRoutes.length > 0) {
527
+ console.log('[global]')
528
+ globalRoutes.forEach(r => console.log(r))
529
+ console.log()
530
+ total += globalRoutes.length
531
+ }
532
+ }
533
+
534
+ console.log(`Total: ${total} ruta(s)`)
535
+ break
536
+ }
537
+
538
+ // ─── Ejecutar migraciones ───
539
+ case 'db:migrate': {
540
+ const direction = args[0] === 'down' ? 'down' : 'up'
541
+ const { dbMigrate } = await import('./commands/db-migrate')
542
+ await dbMigrate(direction)
543
+ break
544
+ }
545
+
546
+ default: {
547
+ console.log(`
548
+ ╔═══════════════════════════════════════════╗
549
+ ║ Arckode Framework — Tooling ║
550
+ ║ Para IA · Modular · SOLID · Predictible ║
551
+ ╚═══════════════════════════════════════════╝
552
+
553
+ BACKEND:
554
+ arckode new <proyecto> Crear proyecto backend completo
555
+ arckode make:auth Generar autenticación (login, register, JWT)
556
+ arckode make:module <Nombre> Generar módulo completo (+ migration + seed + tests)
557
+ arckode make:connector <n> <m1> <m2> Conectar módulos
558
+ arckode make:seed <Nombre> Generar seed
559
+ arckode make:migration <nombre> Generar migración
560
+ arckode make:helper <nombre> Generar helper puro en shared/helpers/
561
+ arckode make:adapter <Adapter> <Interfaz> Generar adapter de librería externa
562
+
563
+ FRONTEND (SPA separado):
564
+ arckode new:frontend [nombre] Crear proyecto frontend Vue 3 + Vite + TypeScript
565
+ arckode make:page <Nombre> Generar módulo frontend (api, composable, components, page, router)
566
+ arckode generate:api [frontend] Generar API clients desde modelos del backend
567
+
568
+ MONOLITO:
569
+ Usar new y después new:frontend dentro del proyecto. El servidor sirve API + frontend.
570
+
571
+ BASE DE DATOS:
572
+ arckode db:migrate Ejecutar migraciones pendientes (src/migrations/)
573
+ arckode db:migrate down Revertir la última migración aplicada
574
+ arckode db:seed Listar o ejecutar seeds disponibles
575
+
576
+ ANÁLISIS:
577
+ arckode analyze Analizar arquitectura (detecta violaciones)
578
+ arckode routes Listar todas las rutas (análisis estático)
579
+
580
+ Ejemplo completo (SPA separado):
581
+ arckode new mi-api # Backend
582
+ cd mi-api
583
+ arckode make:auth # Auth
584
+ arckode make:module Productos # CRUD
585
+ arckode make:connector pedidos productos
586
+ bun run src/composition-root.ts # Iniciar API
587
+
588
+ # En otra terminal:
589
+ arckode new:frontend frontend # Frontend SPA
590
+ cd frontend
591
+ npm install && npm run dev # Iniciar en :5173
592
+
593
+ Ejemplo completo (Monolito):
594
+ arckode nuevo monito-app
595
+ cd monito-app
596
+ arckode make:auth
597
+ arckode make:module Productos
598
+ # El composition-root ya incluye serveStatic para el frontend
599
+ `)
600
+ }
601
+ }
602
+ }
603
+
604
+ main().catch(console.error)