arckode-framework 1.3.2 → 1.4.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/adapters/jwt.ts +6 -4
- package/adapters/mysql.ts +7 -2
- package/adapters/postgres.ts +37 -0
- package/adapters/sqlite.ts +7 -1
- package/adapters/vendor.d.ts +48 -0
- package/cli/analyze/checks.ts +333 -0
- package/cli/analyze/index.ts +44 -0
- package/cli/analyze/report.ts +107 -0
- package/cli/analyze/types.ts +46 -0
- package/cli/analyze/utils.ts +36 -0
- package/cli/analyze.ts +2 -647
- package/cli/commands/db-migrate.ts +213 -89
- package/cli/commands/db-seed.ts +97 -32
- package/cli/commands/db-utils.ts +192 -0
- package/cli/commands/new.ts +175 -0
- package/cli/commands/routes.ts +94 -0
- package/cli/index.ts +57 -404
- package/cli/stubs/module/core.ts +162 -0
- package/cli/stubs/module/data.ts +171 -0
- package/cli/stubs/module/index.ts +5 -0
- package/cli/stubs/module/service.ts +198 -0
- package/cli/stubs/module/types.ts +12 -0
- package/cli/stubs/module-stub.ts +2 -552
- package/kernel/auth.ts +114 -0
- package/kernel/cache.ts +37 -0
- package/kernel/config.ts +129 -0
- package/kernel/container.ts +64 -0
- package/kernel/db/orm-migrate.ts +136 -0
- package/kernel/db/orm-repository.ts +45 -0
- package/kernel/db/orm-utils.ts +93 -0
- package/kernel/db/orm.ts +254 -0
- package/kernel/db/transactor.ts +17 -0
- package/kernel/db/types.ts +72 -0
- package/kernel/errors.ts +102 -0
- package/kernel/framework.default.ts +41 -0
- package/kernel/framework.ts +8 -2144
- package/kernel/http/router.ts +131 -0
- package/kernel/http/server.ts +303 -0
- package/kernel/http/types.ts +56 -0
- package/kernel/index.ts +25 -0
- package/kernel/logger.ts +50 -0
- package/kernel/middlewares.ts +19 -7
- package/kernel/modules/create-module.ts +5 -0
- package/kernel/modules/system.ts +149 -0
- package/kernel/modules/types.ts +46 -0
- package/kernel/seeds.ts +48 -0
- package/kernel/static.ts +11 -2
- package/kernel/testing.ts +8 -3
- package/kernel/validator.ts +116 -0
- package/modules/events/index.ts +19 -3
- package/modules/mail/index.ts +14 -2
- package/modules/storage/local-adapter.ts +19 -5
- package/modules/ws/index.ts +123 -18
- package/package.json +8 -11
- package/skills/auth/SKILL.md +36 -220
- package/skills/cli/SKILL.md +32 -251
- package/skills/config/SKILL.md +30 -239
- package/skills/connectors/SKILL.md +32 -295
- package/skills/helpers/SKILL.md +26 -195
- package/skills/middlewares/SKILL.md +30 -280
- package/skills/orm/SKILL.md +42 -349
- package/skills/realtime/SKILL.md +22 -297
- package/skills/services/SKILL.md +40 -183
- package/skills/testing/SKILL.md +34 -266
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// cli/commands/db-utils.ts — Utilidades compartidas para comandos de BD
|
|
2
|
+
// Detecta tipo de BD desde .env y crea la conexión correspondiente
|
|
3
|
+
// Soporta: SQLite (bun:sqlite), MySQL (mysql2), PostgreSQL (pg)
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join, resolve, dirname } from 'node:path'
|
|
7
|
+
|
|
8
|
+
// ─── Env parsing ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export function parseEnv(basePath: string): Record<string, string> {
|
|
11
|
+
const envFile = join(basePath, '.env')
|
|
12
|
+
if (!existsSync(envFile)) return {}
|
|
13
|
+
const vars: Record<string, string> = {}
|
|
14
|
+
for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
|
|
15
|
+
const trimmed = line.trim()
|
|
16
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
17
|
+
const eqIdx = trimmed.indexOf('=')
|
|
18
|
+
if (eqIdx < 0) continue
|
|
19
|
+
vars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim()
|
|
20
|
+
}
|
|
21
|
+
return vars
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── CliDbRunner — interfaz unificada para los comandos CLI ──────────────────
|
|
25
|
+
// Compatible con bun:sqlite (sync envuelto), mysql2 single-connection, y pg Client
|
|
26
|
+
|
|
27
|
+
export interface CliDbRunner {
|
|
28
|
+
run(sql: string, params?: unknown[]): Promise<void>
|
|
29
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>
|
|
30
|
+
exec(sql: string): Promise<void>
|
|
31
|
+
inTransaction(fn: () => Promise<void>): Promise<void>
|
|
32
|
+
/** Dropea TODAS las tablas de usuario. Deshabilita FK checks internamente. */
|
|
33
|
+
dropAllTables(): Promise<void>
|
|
34
|
+
close(): Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export async function createCliDbRunner(
|
|
40
|
+
env: Record<string, string>,
|
|
41
|
+
basePath: string,
|
|
42
|
+
): Promise<CliDbRunner> {
|
|
43
|
+
if (env.DATABASE_URL?.match(/^postgres(ql)?:\/\//)) {
|
|
44
|
+
return createPostgresRunner(env.DATABASE_URL)
|
|
45
|
+
}
|
|
46
|
+
if (env.DB_HOST) {
|
|
47
|
+
return createMysqlRunner(env)
|
|
48
|
+
}
|
|
49
|
+
const raw = env.DB_PATH ?? './data/db.sqlite'
|
|
50
|
+
const dbPath = raw.startsWith('.') ? resolve(basePath, raw) : raw
|
|
51
|
+
return createSqliteRunner(dbPath)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── SQLite (bun:sqlite — sin dependencias externas) ─────────────────────────
|
|
55
|
+
|
|
56
|
+
async function createSqliteRunner(dbPath: string): Promise<CliDbRunner> {
|
|
57
|
+
const { mkdir } = await import('node:fs/promises')
|
|
58
|
+
await mkdir(dirname(dbPath), { recursive: true })
|
|
59
|
+
|
|
60
|
+
const { Database } = await import('bun:sqlite')
|
|
61
|
+
const db = new Database(dbPath)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
async run(sql, params = []) {
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
params.length > 0 ? db.prepare(sql).run(...(params as any[])) : db.prepare(sql).run()
|
|
67
|
+
},
|
|
68
|
+
async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
return (params.length > 0 ? db.prepare(sql).all(...(params as any[])) : db.prepare(sql).all()) as T[]
|
|
71
|
+
},
|
|
72
|
+
async exec(sql) { db.exec(sql) },
|
|
73
|
+
async inTransaction(fn) {
|
|
74
|
+
db.exec('BEGIN')
|
|
75
|
+
try { await fn(); db.exec('COMMIT') } catch (err) {
|
|
76
|
+
try { db.exec('ROLLBACK') } catch { /* ya hizo rollback */ }
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async dropAllTables() {
|
|
81
|
+
db.exec('PRAGMA foreign_keys = OFF')
|
|
82
|
+
const tables = db.prepare(
|
|
83
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
84
|
+
).all() as { name: string }[]
|
|
85
|
+
for (const { name } of tables) {
|
|
86
|
+
db.exec(`DROP TABLE IF EXISTS "${name}"`)
|
|
87
|
+
}
|
|
88
|
+
db.exec('PRAGMA foreign_keys = ON')
|
|
89
|
+
},
|
|
90
|
+
async close() { db.close() },
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── MySQL (mysql2 — conexión dedicada para transacciones correctas) ──────────
|
|
95
|
+
|
|
96
|
+
async function createMysqlRunner(env: Record<string, string>): Promise<CliDbRunner> {
|
|
97
|
+
const mysql = await import('mysql2/promise').catch(() => {
|
|
98
|
+
throw new Error('mysql2 no instalado. Ejecutá: bun add mysql2')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const conn = await mysql.createConnection({
|
|
102
|
+
host: env.DB_HOST,
|
|
103
|
+
port: env.DB_PORT ? parseInt(env.DB_PORT, 10) : 3306,
|
|
104
|
+
user: env.DB_USER ?? 'root',
|
|
105
|
+
password: env.DB_PASSWORD ?? '',
|
|
106
|
+
database: env.DB_NAME,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
async run(sql, params = []) {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
await conn.execute(sql, params as any[])
|
|
113
|
+
},
|
|
114
|
+
async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const [rows] = await conn.execute(sql, params as any[])
|
|
117
|
+
return rows as T[]
|
|
118
|
+
},
|
|
119
|
+
async exec(sql) {
|
|
120
|
+
for (const stmt of sql.split(';').map(s => s.trim()).filter(Boolean)) {
|
|
121
|
+
await conn.execute(stmt)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async inTransaction(fn) {
|
|
125
|
+
await conn.beginTransaction()
|
|
126
|
+
try { await fn(); await conn.commit() } catch (err) {
|
|
127
|
+
try { await conn.rollback() } catch { /* ya revirtió */ }
|
|
128
|
+
throw err
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
async dropAllTables() {
|
|
132
|
+
await conn.execute('SET FOREIGN_KEY_CHECKS = 0')
|
|
133
|
+
const [rows] = await conn.execute(
|
|
134
|
+
'SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE()'
|
|
135
|
+
)
|
|
136
|
+
for (const row of rows as { name: string }[]) {
|
|
137
|
+
await conn.execute(`DROP TABLE IF EXISTS \`${row.name}\``)
|
|
138
|
+
}
|
|
139
|
+
await conn.execute('SET FOREIGN_KEY_CHECKS = 1')
|
|
140
|
+
},
|
|
141
|
+
async close() { await conn.end() },
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── PostgreSQL (pg Client — single connection para serializar transacciones) ──
|
|
146
|
+
|
|
147
|
+
async function createPostgresRunner(connectionString: string): Promise<CliDbRunner> {
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
const pg = await import('pg').catch(() => {
|
|
150
|
+
throw new Error('pg no instalado. Ejecutá: bun add pg')
|
|
151
|
+
}) as any
|
|
152
|
+
|
|
153
|
+
// pg ESM exporta Client como named export en runtime pero el tipo de index.d.mts no lo expone
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
155
|
+
const client = new pg.Client({ connectionString })
|
|
156
|
+
await client.connect()
|
|
157
|
+
|
|
158
|
+
// Convierte ? a $1, $2, ... para compatibilidad con PostgreSQL
|
|
159
|
+
const toPg = (sql: string) => {
|
|
160
|
+
let idx = 0
|
|
161
|
+
return sql.replace(/\?/g, () => `$${++idx}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
async run(sql, params = []) { await client.query(toPg(sql), params) },
|
|
166
|
+
async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
167
|
+
const result = await client.query(toPg(sql), params)
|
|
168
|
+
return result.rows as T[]
|
|
169
|
+
},
|
|
170
|
+
async exec(sql) {
|
|
171
|
+
for (const stmt of sql.split(';').map(s => s.trim()).filter(Boolean)) {
|
|
172
|
+
if (stmt) await client.query(stmt)
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
async inTransaction(fn) {
|
|
176
|
+
await client.query('BEGIN')
|
|
177
|
+
try { await fn(); await client.query('COMMIT') } catch (err) {
|
|
178
|
+
try { await client.query('ROLLBACK') } catch { /* ya revirtió */ }
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
async dropAllTables() {
|
|
183
|
+
const result = await client.query(
|
|
184
|
+
"SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'"
|
|
185
|
+
)
|
|
186
|
+
for (const row of result.rows as { name: string }[]) {
|
|
187
|
+
await client.query(`DROP TABLE IF EXISTS "${row.name}" CASCADE`)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
async close() { await client.end() },
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { claudeMdStub } from '../stubs/claude-md-stub'
|
|
4
|
+
|
|
5
|
+
export type DbDriver = 'sqlite' | 'mysql' | 'postgres'
|
|
6
|
+
|
|
7
|
+
export async function newProject(name: string, db: DbDriver = 'sqlite'): Promise<void> {
|
|
8
|
+
const base = join(process.cwd(), name)
|
|
9
|
+
|
|
10
|
+
await mkdir(join(base, 'src', 'modules'), { recursive: true })
|
|
11
|
+
await mkdir(join(base, 'src', 'connectors'), { recursive: true })
|
|
12
|
+
await mkdir(join(base, 'data'), { recursive: true })
|
|
13
|
+
|
|
14
|
+
await writeFile(join(base, 'CLAUDE.md'), claudeMdStub({ projectName: name, db }))
|
|
15
|
+
await writeFile(join(base, 'src', 'composition-root.ts'), buildCompositionRoot(name, db))
|
|
16
|
+
await writeFile(join(base, 'package.json'), buildPackageJson(name, db))
|
|
17
|
+
await writeFile(join(base, '.env'), buildEnv(name, db))
|
|
18
|
+
await writeFile(join(base, '.env.example'), buildEnv(name, db))
|
|
19
|
+
await writeFile(join(base, '.gitignore'), `node_modules/\ndist/\ndata/\n.env\n`)
|
|
20
|
+
|
|
21
|
+
console.log(`\n✅ Proyecto "${name}" creado (DB: ${db})`)
|
|
22
|
+
console.log(`\n Siguientes pasos:`)
|
|
23
|
+
console.log(` cd ${name}`)
|
|
24
|
+
console.log(` bun install`)
|
|
25
|
+
console.log(` bun run dev`)
|
|
26
|
+
console.log(`\n La IA arranca leyendo CLAUDE.md → arckode.json`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildCompositionRoot(name: string, db: DbDriver): string {
|
|
30
|
+
const dbAdapterImport = db === 'sqlite'
|
|
31
|
+
? `import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'`
|
|
32
|
+
: db === 'mysql'
|
|
33
|
+
? `import { MySQLAdapter } from 'arckode-framework/adapters/mysql'`
|
|
34
|
+
: `import { PostgresAdapter } from 'arckode-framework/adapters/postgres'`
|
|
35
|
+
|
|
36
|
+
const dbConfigFields = db === 'sqlite'
|
|
37
|
+
? ` DB_PATH: { type: 'string', default: './data/db.sqlite' },`
|
|
38
|
+
: db === 'mysql'
|
|
39
|
+
? [
|
|
40
|
+
` DB_HOST: { type: 'string', required: true },`,
|
|
41
|
+
` DB_USER: { type: 'string', required: true },`,
|
|
42
|
+
` DB_PASSWORD:{ type: 'string', required: true },`,
|
|
43
|
+
` DB_NAME: { type: 'string', required: true },`,
|
|
44
|
+
` DB_PORT: { type: 'number', default: 3306 },`,
|
|
45
|
+
].join('\n')
|
|
46
|
+
: ` DATABASE_URL: { type: 'string', required: true },`
|
|
47
|
+
|
|
48
|
+
const dbAdapterInit = db === 'sqlite'
|
|
49
|
+
? `const dbAdapter = new SqliteAdapter({ path: config.get('DB_PATH') })`
|
|
50
|
+
: db === 'mysql'
|
|
51
|
+
? [
|
|
52
|
+
`const dbAdapter = new MySQLAdapter({`,
|
|
53
|
+
` host: config.get('DB_HOST'),`,
|
|
54
|
+
` port: config.get<number>('DB_PORT'),`,
|
|
55
|
+
` user: config.get('DB_USER'),`,
|
|
56
|
+
` password: config.get('DB_PASSWORD'),`,
|
|
57
|
+
` database: config.get('DB_NAME'),`,
|
|
58
|
+
`})`,
|
|
59
|
+
].join('\n')
|
|
60
|
+
: `const dbAdapter = new PostgresAdapter({ connectionString: config.get('DATABASE_URL') })`
|
|
61
|
+
|
|
62
|
+
return `// composition-root.ts — ÚNICO archivo que describe TODO el sistema
|
|
63
|
+
// La IA lee esto y sabe: módulos, conectores, dependencias.
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
import {
|
|
67
|
+
ConfigStore, Container, Logger, ORM, Router,
|
|
68
|
+
NodeServer, MemoryCache, System, Auth, loadEnv,
|
|
69
|
+
} from 'arckode-framework'
|
|
70
|
+
${dbAdapterImport}
|
|
71
|
+
import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
|
|
72
|
+
|
|
73
|
+
// ─── 1. ENV + CONFIG ─────────────────────────
|
|
74
|
+
const env = await loadEnv()
|
|
75
|
+
|
|
76
|
+
const config = new ConfigStore()
|
|
77
|
+
config.define({
|
|
78
|
+
PORT: { type: 'number', default: 3000 },
|
|
79
|
+
${dbConfigFields}
|
|
80
|
+
JWT_SECRET: { type: 'string', required: true },
|
|
81
|
+
LOG_LEVEL: { type: 'string', default: 'info' },
|
|
82
|
+
}).load(env)
|
|
83
|
+
|
|
84
|
+
// ─── 2. INFRAESTRUCTURA ──────────────────────
|
|
85
|
+
const logger = new Logger('app', config.get('LOG_LEVEL') as any)
|
|
86
|
+
const container = new Container()
|
|
87
|
+
${dbAdapterInit}
|
|
88
|
+
await dbAdapter.connect()
|
|
89
|
+
const orm = new ORM(dbAdapter)
|
|
90
|
+
const router = new Router()
|
|
91
|
+
const http = new NodeServer(config.get<number>('PORT'), logger)
|
|
92
|
+
const cache = new MemoryCache()
|
|
93
|
+
const auth = new Auth(jwtTokenAdapter, config.get('JWT_SECRET'), logger)
|
|
94
|
+
|
|
95
|
+
// ─── 3. CONTAINER ────────────────────────────
|
|
96
|
+
container.register('config', () => config)
|
|
97
|
+
container.register('logger', () => logger)
|
|
98
|
+
container.register('db', () => dbAdapter, () => dbAdapter.close())
|
|
99
|
+
container.register('orm', () => orm)
|
|
100
|
+
container.register('cache', () => cache)
|
|
101
|
+
container.init()
|
|
102
|
+
|
|
103
|
+
// ─── 4. SISTEMA ───────────────────────────────
|
|
104
|
+
const system = new System({
|
|
105
|
+
config, container, logger, orm, router, http, cache, auth,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// ─── 5. MÓDULOS ───────────────────────────────
|
|
109
|
+
// import { MiModuloModule } from './modules/mi-modulo'
|
|
110
|
+
// system.addModule(MiModuloModule())
|
|
111
|
+
|
|
112
|
+
// ─── 6. CONECTORES ────────────────────────────
|
|
113
|
+
// import { connectModAModB } from './connectors/mod-a-mod-b'
|
|
114
|
+
// system.addConnector('mod-a-mod-b', connectModAModB)
|
|
115
|
+
|
|
116
|
+
// ─── 7. RUTAS BASE ────────────────────────────
|
|
117
|
+
router.get('/health', async () => ({
|
|
118
|
+
status: 200, body: { status: 'ok', uptime: process.uptime() }
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
// ─── 8. INIT + MIGRATE + START ────────────────
|
|
122
|
+
system.init()
|
|
123
|
+
await orm.migrate()
|
|
124
|
+
|
|
125
|
+
await system.start()
|
|
126
|
+
|
|
127
|
+
process.on('SIGINT', async () => { await system.stop(); process.exit(0) })
|
|
128
|
+
process.on('SIGTERM', async () => { await system.stop(); process.exit(0) })
|
|
129
|
+
`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildPackageJson(name: string, db: DbDriver): string {
|
|
133
|
+
const dbDeps = db === 'mysql'
|
|
134
|
+
? { 'mysql2': '^3.0.0' }
|
|
135
|
+
: db === 'postgres'
|
|
136
|
+
? { 'pg': '^8.0.0' }
|
|
137
|
+
: {}
|
|
138
|
+
|
|
139
|
+
const dbDevDeps = db === 'postgres'
|
|
140
|
+
? { '@types/pg': '^8.0.0' }
|
|
141
|
+
: {}
|
|
142
|
+
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
name,
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
type: 'module',
|
|
147
|
+
scripts: {
|
|
148
|
+
dev: `bun --watch src/composition-root.ts`,
|
|
149
|
+
start: `bun run src/composition-root.ts`,
|
|
150
|
+
test: `bun test`,
|
|
151
|
+
'test:watch': `bun test --watch`,
|
|
152
|
+
analyze: `bun run node_modules/arckode-framework/cli/index.ts analyze`,
|
|
153
|
+
'analyze:ci': `bun run node_modules/arckode-framework/cli/index.ts analyze --ci`,
|
|
154
|
+
},
|
|
155
|
+
dependencies: {
|
|
156
|
+
'arckode-framework': 'latest',
|
|
157
|
+
'jsonwebtoken': '^9.0.0',
|
|
158
|
+
...dbDeps,
|
|
159
|
+
},
|
|
160
|
+
devDependencies: {
|
|
161
|
+
'@types/jsonwebtoken': '^9.0.0',
|
|
162
|
+
...dbDevDeps,
|
|
163
|
+
},
|
|
164
|
+
}, null, 2)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildEnv(name: string, db: DbDriver): string {
|
|
168
|
+
if (db === 'mysql') {
|
|
169
|
+
return `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`
|
|
170
|
+
}
|
|
171
|
+
if (db === 'postgres') {
|
|
172
|
+
return `PORT=3000\nDATABASE_URL=postgres://user:password@localhost:5432/${name}\nJWT_SECRET=cambiar-en-produccion\nLOG_LEVEL=info\n`
|
|
173
|
+
}
|
|
174
|
+
return `PORT=3000\nDB_PATH=./data/db.sqlite\nJWT_SECRET=cambiar-en-produccion\nLOG_LEVEL=info\n`
|
|
175
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
|
|
6
|
+
const METHOD_COLORS: Record<string, string> = {
|
|
7
|
+
get: '\x1b[32m',
|
|
8
|
+
post: '\x1b[33m',
|
|
9
|
+
put: '\x1b[34m',
|
|
10
|
+
patch: '\x1b[35m',
|
|
11
|
+
delete: '\x1b[31m',
|
|
12
|
+
}
|
|
13
|
+
const RESET = '\x1b[0m'
|
|
14
|
+
|
|
15
|
+
async function collectTsFiles(dir: string): Promise<string[]> {
|
|
16
|
+
const files: string[] = []
|
|
17
|
+
if (!existsSync(dir)) return files
|
|
18
|
+
for (const entry of await readdir(dir)) {
|
|
19
|
+
const full = join(dir, entry)
|
|
20
|
+
const s = await stat(full)
|
|
21
|
+
if (s.isDirectory()) files.push(...await collectTsFiles(full))
|
|
22
|
+
else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts') && !entry.endsWith('.d.ts')) files.push(full)
|
|
23
|
+
}
|
|
24
|
+
return files
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractRoutes(src: string, seen: Set<string>): string[] {
|
|
28
|
+
const found: string[] = []
|
|
29
|
+
for (const method of METHODS) {
|
|
30
|
+
const re = new RegExp("router\\." + method + "\\s*\\(\\s*['\"`]([^'\"`${}]+)['\"`]", 'g')
|
|
31
|
+
let match: RegExpExecArray | null
|
|
32
|
+
while ((match = re.exec(src)) !== null) {
|
|
33
|
+
const key = `${method}:${match[1]}`
|
|
34
|
+
if (seen.has(key)) continue
|
|
35
|
+
seen.add(key)
|
|
36
|
+
const color = METHOD_COLORS[method]
|
|
37
|
+
found.push(` ${color}${method.toUpperCase().padEnd(7)}${RESET} ${match[1]}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return found
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function routes(cwd = process.cwd()): Promise<void> {
|
|
44
|
+
const modulesPath = join(cwd, 'src', 'modules')
|
|
45
|
+
|
|
46
|
+
if (!existsSync(modulesPath)) {
|
|
47
|
+
console.log('❌ No se encuentra src/modules/')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('\n📋 Rutas registradas (análisis estático)\n')
|
|
52
|
+
|
|
53
|
+
const moduleDirs = await readdir(modulesPath)
|
|
54
|
+
let total = 0
|
|
55
|
+
const globalSeen = new Set<string>()
|
|
56
|
+
|
|
57
|
+
for (const mod of moduleDirs) {
|
|
58
|
+
const modDir = join(modulesPath, mod)
|
|
59
|
+
const s = await stat(modDir).catch(() => null)
|
|
60
|
+
if (!s?.isDirectory()) continue
|
|
61
|
+
|
|
62
|
+
const files = await collectTsFiles(modDir)
|
|
63
|
+
const seen = new Set<string>()
|
|
64
|
+
const routeLines: string[] = []
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const src = await readFile(file, 'utf-8')
|
|
68
|
+
routeLines.push(...extractRoutes(src, seen))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const key of seen) globalSeen.add(key)
|
|
72
|
+
|
|
73
|
+
if (routeLines.length > 0) {
|
|
74
|
+
console.log(`[${mod}]`)
|
|
75
|
+
routeLines.forEach(r => console.log(r))
|
|
76
|
+
console.log()
|
|
77
|
+
total += routeLines.length
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const crPath = join(cwd, 'src', 'composition-root.ts')
|
|
82
|
+
if (existsSync(crPath)) {
|
|
83
|
+
const src = await readFile(crPath, 'utf-8')
|
|
84
|
+
const globalRoutes = extractRoutes(src, globalSeen)
|
|
85
|
+
if (globalRoutes.length > 0) {
|
|
86
|
+
console.log('[global]')
|
|
87
|
+
globalRoutes.forEach(r => console.log(r))
|
|
88
|
+
console.log()
|
|
89
|
+
total += globalRoutes.length
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Total: ${total} ruta(s)`)
|
|
94
|
+
}
|