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
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)
|