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,78 @@
1
+ // modules/queue/index.ts — Módulo de colas / jobs
2
+ // Procesa tareas en background. Adapter intercambiable (in-process, Redis, DB).
3
+
4
+ export interface Job {
5
+ id: string
6
+ name: string
7
+ data: unknown
8
+ attempts: number
9
+ maxAttempts: number
10
+ createdAt: string
11
+ error?: string
12
+ }
13
+
14
+ export type JobHandler = (job: Job) => Promise<void>
15
+
16
+ export interface QueueAdapter {
17
+ push(name: string, data: unknown, opts?: { delay?: number; maxAttempts?: number }): Promise<string>
18
+ process(name: string, handler: JobHandler): void
19
+ onFailed?(name: string, job: Job, error: Error): void
20
+ }
21
+
22
+ export class QueueService {
23
+ private handlers = new Map<string, JobHandler>()
24
+
25
+ constructor(private adapter: QueueAdapter) {}
26
+
27
+ async dispatch(name: string, data: unknown, opts?: { delay?: number; maxAttempts?: number }): Promise<string> {
28
+ return this.adapter.push(name, data, opts)
29
+ }
30
+
31
+ register(name: string, handler: JobHandler): void {
32
+ this.handlers.set(name, handler)
33
+ this.adapter.process(name, handler)
34
+ }
35
+ }
36
+
37
+ // ─── Adapter en memoria (para desarrollo) ───
38
+ export class MemoryQueueAdapter implements QueueAdapter {
39
+ private handlers = new Map<string, JobHandler>()
40
+ private jobs: Job[] = []
41
+
42
+ async push(name: string, data: unknown, opts?: { delay?: number; maxAttempts?: number }): Promise<string> {
43
+ const job: Job = {
44
+ id: crypto.randomUUID(),
45
+ name,
46
+ data,
47
+ attempts: 0,
48
+ maxAttempts: opts?.maxAttempts ?? 3,
49
+ createdAt: new Date().toISOString(),
50
+ }
51
+
52
+ const execute = async () => {
53
+ const handler = this.handlers.get(name)
54
+ if (!handler) return
55
+ try {
56
+ job.attempts++
57
+ await handler(job)
58
+ } catch (error) {
59
+ job.error = String(error)
60
+ if (job.attempts < job.maxAttempts) {
61
+ setTimeout(execute, 1000 * job.attempts)
62
+ }
63
+ }
64
+ }
65
+
66
+ if (opts?.delay) {
67
+ setTimeout(execute, opts.delay)
68
+ } else {
69
+ process.nextTick(execute)
70
+ }
71
+
72
+ return job.id
73
+ }
74
+
75
+ process(name: string, handler: JobHandler): void {
76
+ this.handlers.set(name, handler)
77
+ }
78
+ }
@@ -0,0 +1,40 @@
1
+ // modules/storage/index.ts — Módulo de almacenamiento
2
+ // Sube y sirve archivos via adapter (local disk, S3, etc.)
3
+
4
+ export interface FileUpload {
5
+ fieldName: string
6
+ originalName: string
7
+ buffer: Buffer
8
+ mimeType: string
9
+ size: number
10
+ }
11
+
12
+ export interface StoredFile {
13
+ url: string
14
+ path: string
15
+ originalName: string
16
+ mimeType: string
17
+ size: number
18
+ }
19
+
20
+ export interface StorageAdapter {
21
+ upload(file: FileUpload, directory?: string): Promise<StoredFile>
22
+ delete(path: string): Promise<void>
23
+ getUrl(path: string): string
24
+ }
25
+
26
+ export class StorageService {
27
+ constructor(private adapter: StorageAdapter) {}
28
+
29
+ async upload(file: FileUpload, directory?: string): Promise<StoredFile> {
30
+ return this.adapter.upload(file, directory)
31
+ }
32
+
33
+ async delete(path: string): Promise<void> {
34
+ return this.adapter.delete(path)
35
+ }
36
+
37
+ getUrl(path: string): string {
38
+ return this.adapter.getUrl(path)
39
+ }
40
+ }
@@ -0,0 +1,41 @@
1
+ // modules/storage/local-adapter.ts — Adapter de disco local
2
+ // Almacena archivos en el sistema de archivos local
3
+
4
+ import { mkdir, writeFile, unlink } from 'node:fs/promises'
5
+ import { join, extname } from 'node:path'
6
+ import type { StorageAdapter, FileUpload, StoredFile } from './index'
7
+
8
+ export class LocalStorageAdapter implements StorageAdapter {
9
+ constructor(
10
+ private basePath: string = './uploads',
11
+ private baseUrl: string = '/uploads',
12
+ ) {}
13
+
14
+ async upload(file: FileUpload, directory?: string): Promise<StoredFile> {
15
+ const dir = directory ?? 'general'
16
+ const ext = extname(file.originalName)
17
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`
18
+ const relativePath = `${dir}/${filename}`
19
+ const fullPath = join(this.basePath, relativePath)
20
+
21
+ await mkdir(join(this.basePath, dir), { recursive: true })
22
+ await writeFile(fullPath, file.buffer)
23
+
24
+ return {
25
+ url: `${this.baseUrl}/${relativePath}`,
26
+ path: relativePath,
27
+ originalName: file.originalName,
28
+ mimeType: file.mimeType,
29
+ size: file.size,
30
+ }
31
+ }
32
+
33
+ async delete(path: string): Promise<void> {
34
+ const fullPath = join(this.basePath, path)
35
+ await unlink(fullPath).catch(() => {})
36
+ }
37
+
38
+ getUrl(path: string): string {
39
+ return `${this.baseUrl}/${path}`
40
+ }
41
+ }
@@ -0,0 +1,114 @@
1
+ // modules/ws/__tests__/ws.test.ts — Tests del WebSocket server
2
+ // Prueba: handshake, parseFrame, sendFrame, broadcast, sendTo
3
+
4
+ import { describe, it, expect } from 'bun:test'
5
+ import { WSServer } from '../index'
6
+ import { createHash } from 'node:crypto'
7
+
8
+ // ─── Test de generateAccept ────────────────────────────
9
+ describe('WSServer - handshake', () => {
10
+ it('generateAccept produce output correcto', () => {
11
+ // El WebSocket server tiene método generateAccept
12
+ // Probamos que devuelve un string base64 válido
13
+ const ws = new WSServer() as any
14
+ const key = 'dGhlIHNhbXBsZSBub25jZQ=='
15
+ const accept = ws.generateAccept(key)
16
+ expect(accept).toBeTruthy()
17
+ expect(typeof accept).toBe('string')
18
+ expect(accept.length).toBeGreaterThan(0)
19
+
20
+ // Verificar que el hash es correcto según RFC 6455
21
+ const expected = createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-5AB5DC11B725').digest('base64')
22
+ expect(accept).toBe(expected)
23
+ })
24
+ })
25
+
26
+ // ─── Test de parseFrame ────────────────────────────────
27
+ describe('WSServer - parseFrame', () => {
28
+ it('parsea un frame de texto sin mask', () => {
29
+ const ws = new WSServer() as any
30
+ // Frame: FIN=1, opcode=1 (text), payload=Hola
31
+ const frame = Buffer.alloc(6)
32
+ frame[0] = 0x81 // FIN + texto
33
+ frame[1] = 4 // payload length
34
+ frame.write('Hola', 2) // payload
35
+ const result = ws.parseFrame(frame)
36
+ expect(result).toBe('Hola')
37
+ })
38
+
39
+ it('parsea un frame con mask', () => {
40
+ const ws = new WSServer() as any
41
+ const payload = Buffer.from('Hello')
42
+ const mask = Buffer.from([0x01, 0x02, 0x03, 0x04])
43
+ const masked = Buffer.alloc(payload.length)
44
+ for (let i = 0; i < payload.length; i++) {
45
+ masked[i] = (payload[i] ?? 0) ^ (mask[i % 4] ?? 0)
46
+ }
47
+
48
+ const frame = Buffer.alloc(2 + 4 + payload.length)
49
+ frame[0] = 0x81
50
+ frame[1] = 0x80 | payload.length // MASK bit + length
51
+ mask.copy(frame, 2)
52
+ masked.copy(frame, 6)
53
+
54
+ const result = ws.parseFrame(frame)
55
+ expect(result).toBe('Hello')
56
+ })
57
+
58
+ it('devuelve null para frames no-texto', () => {
59
+ const ws = new WSServer() as any
60
+ // Frame: ping (opcode 0x09)
61
+ const frame = Buffer.alloc(2)
62
+ frame[0] = 0x89
63
+ frame[1] = 0
64
+ const result = ws.parseFrame(frame)
65
+ expect(result).toBeNull()
66
+ })
67
+ })
68
+
69
+ // ─── Test de sendFrame ────────────────────────────────
70
+ describe('WSServer - sendFrame', () => {
71
+ it('construye frame correctamente', () => {
72
+ const ws = new WSServer() as any
73
+ const chunks: Buffer[] = []
74
+ const mockSocket = { write: (b: Buffer) => chunks.push(b) }
75
+
76
+ ws.sendFrame(mockSocket, 'Hola')
77
+ expect(chunks).toHaveLength(1)
78
+ const frame = chunks[0]!
79
+ expect(frame[0]).toBe(0x81) // FIN + texto
80
+ expect(frame[1]).toBe(4) // longitud
81
+ expect(frame.slice(2).toString()).toBe('Hola')
82
+ })
83
+ })
84
+
85
+ // ─── Test de broadcast ────────────────────────────────
86
+ describe('WSServer - broadcast', () => {
87
+ it('envía a todos los clientes conectados', () => {
88
+ const ws = new WSServer() as any
89
+ const received: string[] = []
90
+
91
+ // Simular clientes conectados
92
+ ws.clients.set('c1', { id: 'c1', send: (d: string) => received.push(d), close: () => {} })
93
+ ws.clients.set('c2', { id: 'c2', send: (d: string) => received.push(d), close: () => {} })
94
+
95
+ ws.broadcast('test.event', { msg: 'hello' })
96
+ expect(received).toHaveLength(2)
97
+ expect(received[0]).toContain('test.event')
98
+ expect(received[1]).toContain('hello')
99
+ })
100
+ })
101
+
102
+ // ─── Test de sendTo ────────────────────────────────────
103
+ describe('WSServer - sendTo', () => {
104
+ it('envía a un cliente específico', () => {
105
+ const ws = new WSServer() as any
106
+ const received: string[] = []
107
+
108
+ ws.clients.set('target', { id: 'target', send: (d: string) => received.push(d), close: () => {} })
109
+ ws.clients.set('other', { id: 'other', send: (d: string) => received.push(d), close: () => {} })
110
+
111
+ ws.sendTo('target', 'private.event', { secret: true })
112
+ expect(received).toHaveLength(1)
113
+ })
114
+ })
@@ -0,0 +1,136 @@
1
+ // modules/ws/index.ts — WebSocket server (tiempo real)
2
+ // Comunicación bidireccional entre servidor y clientes
3
+ // Uso: WS en backend, EventSource o WS nativo en frontend
4
+
5
+ import type { Server as HttpServer } from 'node:http'
6
+ import { createHash } from 'node:crypto'
7
+
8
+ export interface WSClient {
9
+ id: string
10
+ send(data: unknown): void
11
+ close(): void
12
+ onMessage(data: unknown): void
13
+ onClose(): void
14
+ }
15
+
16
+ export interface WSAdapter {
17
+ broadcast(event: string, data: unknown): void
18
+ sendTo(clientId: string, event: string, data: unknown): void
19
+ getClients(): WSClient[]
20
+ }
21
+
22
+ // ─── WebSocket nativo (sin dependencias externas) ───
23
+ // Usa el servidor HTTP de Node. Para producción usar `ws` o `uWebSockets`.
24
+
25
+ export class WSServer implements WSAdapter {
26
+ private clients = new Map<string, WSClient>()
27
+
28
+ attach(server: HttpServer): void {
29
+ server.on('upgrade', (req, socket, head) => {
30
+ // Implementación básica de WebSocket handshake
31
+ const key = req.headers['sec-websocket-key']
32
+ if (!key) { socket.destroy(); return }
33
+
34
+ const accept = this.generateAccept(key)
35
+ socket.write([
36
+ 'HTTP/1.1 101 Switching Protocols',
37
+ 'Upgrade: websocket',
38
+ 'Connection: Upgrade',
39
+ `Sec-WebSocket-Accept: ${accept}`,
40
+ '',
41
+ '',
42
+ ].join('\r\n'))
43
+
44
+ const clientId = crypto.randomUUID()
45
+ const client: WSClient = {
46
+ id: clientId,
47
+ send: (data) => this.sendFrame(socket, JSON.stringify(data)),
48
+ close: () => socket.end(),
49
+ onMessage: () => {},
50
+ onClose: () => {},
51
+ }
52
+
53
+ this.clients.set(clientId, client)
54
+ console.log(`[WS] Client connected: ${clientId}`)
55
+
56
+ socket.on('data', (buffer) => {
57
+ const message = this.parseFrame(buffer)
58
+ if (message) client.onMessage(message)
59
+ })
60
+
61
+ socket.on('close', () => {
62
+ this.clients.delete(clientId)
63
+ client.onClose()
64
+ console.log(`[WS] Client disconnected: ${clientId}`)
65
+ })
66
+ })
67
+ }
68
+
69
+ broadcast(event: string, data: unknown): void {
70
+ const message = JSON.stringify({ event, data })
71
+ for (const client of this.clients.values()) {
72
+ try { client.send(message) } catch { /* ignore */ }
73
+ }
74
+ }
75
+
76
+ sendTo(clientId: string, event: string, data: unknown): void {
77
+ const client = this.clients.get(clientId)
78
+ if (client) {
79
+ client.send(JSON.stringify({ event, data }))
80
+ }
81
+ }
82
+
83
+ getClients(): WSClient[] {
84
+ return [...this.clients.values()]
85
+ }
86
+
87
+ private generateAccept(key: string): string {
88
+ const GUID = '258EAFA5-E914-47DA-95CA-5AB5DC11B725'
89
+ return createHash('sha1').update(key + GUID).digest('base64')
90
+ }
91
+
92
+ private sendFrame(socket: any, data: string): void {
93
+ const buffer = Buffer.from(data, 'utf-8')
94
+ const frame = Buffer.alloc(2 + buffer.length)
95
+ frame[0] = 0x81 // FIN + texto
96
+ frame[1] = buffer.length
97
+ buffer.copy(frame, 2)
98
+ socket.write(frame)
99
+ }
100
+
101
+ private parseFrame(buffer: Buffer): string | null {
102
+ if (buffer.length < 2) return null
103
+ const opcode = (buffer[0] ?? 0) & 0x0F
104
+ if (opcode !== 0x01) return null // solo texto
105
+ const masked = ((buffer[1] ?? 0) & 0x80) !== 0
106
+ let payloadLength = (buffer[1] ?? 0) & 0x7F
107
+ let offset = 2
108
+
109
+ if (payloadLength === 126) {
110
+ payloadLength = buffer.readUInt16BE(2)
111
+ offset = 4
112
+ } else if (payloadLength === 127) {
113
+ payloadLength = Number(buffer.readBigUInt64BE(2))
114
+ offset = 10
115
+ }
116
+
117
+ if (masked) {
118
+ const mask = buffer.slice(offset, offset + 4)
119
+ offset += 4
120
+ const payload = Buffer.alloc(payloadLength)
121
+ for (let i = 0; i < payloadLength; i++) {
122
+ payload[i] = (buffer[offset + i] ?? 0) ^ (mask[i % 4] ?? 0)
123
+ }
124
+ return payload.toString('utf-8')
125
+ }
126
+
127
+ return buffer.slice(offset, offset + payloadLength).toString('utf-8')
128
+ }
129
+ }
130
+
131
+ // ─── Uso en composition root ───────────────────────────
132
+ // import { WSServer } from 'arckode-framework/modules/ws'
133
+ // const ws = new WSServer()
134
+ // ws.attach(http.server)
135
+ //
136
+ // ws.broadcast('pedido.nuevo', { id: '123' })
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "arckode-framework",
3
+ "version": "1.0.0",
4
+ "description": "AI-first TypeScript/Bun framework. Modular, SOLID, zero magic. The AI reads the composition root and knows everything.",
5
+ "type": "module",
6
+ "main": "./kernel/framework.ts",
7
+ "exports": {
8
+ ".": "./kernel/framework.ts",
9
+ "./middlewares": "./kernel/middlewares.ts",
10
+ "./static": "./kernel/static.ts",
11
+ "./events": "./modules/events/index.ts",
12
+ "./ws": "./modules/ws/index.ts",
13
+ "./adapters/sqlite": "./adapters/sqlite.ts",
14
+ "./adapters/jwt": "./adapters/jwt.ts",
15
+ "./adapters/postgres": "./adapters/postgres.ts",
16
+ "./adapters/redis-cache": "./adapters/redis-cache.ts",
17
+ "./adapters/mysql": "./adapters/mysql.ts",
18
+ "./modules/mail": "./modules/mail/index.ts",
19
+ "./modules/storage": "./modules/storage/index.ts",
20
+ "./modules/queue": "./modules/queue/index.ts",
21
+ "./testing": "./kernel/testing.ts"
22
+ },
23
+ "bin": {
24
+ "arckode": "bin/arckode.js"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "kernel/",
29
+ "adapters/",
30
+ "modules/",
31
+ "cli/",
32
+ "skills/",
33
+ "README.md"
34
+ ],
35
+ "engines": {
36
+ "bun": ">=1.0.0"
37
+ },
38
+ "scripts": {
39
+ "arckode": "bun run cli/index.ts",
40
+ "test": "bun test",
41
+ "test:watch": "bun test --watch",
42
+ "test:coverage": "bun test --coverage",
43
+ "typecheck": "bunx tsc --noEmit",
44
+ "analyze:framework": "bun run cli/index.ts analyze"
45
+ },
46
+ "dependencies": {
47
+ "jsonwebtoken": "^9.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "better-sqlite3": ">=11.0.0",
51
+ "mysql2": ">=3.0.0",
52
+ "pg": ">=8.0.0",
53
+ "redis": ">=4.0.0",
54
+ "nodemailer": ">=6.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "better-sqlite3": {
58
+ "optional": true
59
+ },
60
+ "mysql2": {
61
+ "optional": true
62
+ },
63
+ "pg": {
64
+ "optional": true
65
+ },
66
+ "redis": {
67
+ "optional": true
68
+ },
69
+ "nodemailer": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@types/better-sqlite3": "^7.6.12",
75
+ "@types/jsonwebtoken": "^9.0.7",
76
+ "@types/nodemailer": "^6.4.0",
77
+ "@types/pg": "^8.11.0",
78
+ "bun-types": "^1.3.14",
79
+ "typescript": "^5.6.0"
80
+ },
81
+ "keywords": [
82
+ "framework",
83
+ "modular",
84
+ "SOLID",
85
+ "ai",
86
+ "bun",
87
+ "typescript",
88
+ "clean-architecture",
89
+ "composition-root",
90
+ "api",
91
+ "arckode"
92
+ ],
93
+ "license": "MIT",
94
+ "repository": {
95
+ "type": "git",
96
+ "url": "git+https://gitlab.com/underworf/nexus-framework.git"
97
+ },
98
+ "homepage": "https://gitlab.com/underworf/nexus-framework#readme"
99
+ }