@icarusmx/creta 1.3.5 → 1.4.1

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.
@@ -1,5 +1,6 @@
1
1
  import { createPromptInterface, askQuestion } from '../utils/input.js'
2
2
  import { clearConsole } from '../utils/output.js'
3
+ import { UserState } from '../utils/user-state.js'
3
4
 
4
5
  const DEFAULT_WAIT_MESSAGE = '\nPresiona Enter para continuar...'
5
6
 
@@ -19,6 +20,11 @@ export class LessonBuilder {
19
20
  try {
20
21
  const flow = this.collectActions()
21
22
  await this.executeActions(flow)
23
+
24
+ // Track lesson completion if lesson has an ID
25
+ if (this.lesson.id) {
26
+ UserState.completLesson(this.lesson.id)
27
+ }
22
28
  } finally {
23
29
  this.rl.close()
24
30
  this.rl = null
@@ -85,6 +91,9 @@ export class LessonBuilder {
85
91
  case 'command-intro':
86
92
  await this.handleCommandIntro(action)
87
93
  break
94
+ case 'maze-completion':
95
+ await this.handleMazeCompletion(action)
96
+ break
88
97
  case 'text':
89
98
  default:
90
99
  this.handleText(action)
@@ -194,21 +203,27 @@ export class LessonBuilder {
194
203
  }
195
204
 
196
205
  // Wait for Enter
197
- await askQuestion(this.rl, '\nPresiona Enter para comenzar...')
206
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
198
207
 
199
- // Show explanation and detail
208
+ // Clear screen and show explanation
200
209
  if (action.explanation) {
201
- console.log(`\n${action.explanation}`)
210
+ clearConsole()
211
+ console.log(action.explanation)
212
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
202
213
  }
203
214
 
215
+ // Clear screen and show detail
204
216
  if (action.detail) {
205
- console.log('')
217
+ clearConsole()
206
218
  console.log(action.detail)
219
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
207
220
  }
208
221
  }
209
222
 
210
223
  async handleCommandIntro(action) {
211
- console.log('')
224
+ // Clear screen before showing command
225
+ clearConsole()
226
+
212
227
  console.log(action.description)
213
228
  console.log('')
214
229
  console.log(action.explanation)
@@ -222,7 +237,152 @@ export class LessonBuilder {
222
237
  console.log(action.instruction)
223
238
 
224
239
  if (!action.exitOnComplete) {
225
- await askQuestion(this.rl, DEFAULT_WAIT_MESSAGE)
240
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
241
+ }
242
+ }
243
+
244
+ async handleMazeCompletion(action) {
245
+ const frames = [
246
+ // Frame 1: Cow enters maze
247
+ [
248
+ '███████████████',
249
+ '█🐄░░░███░░░░░█',
250
+ '█░███░███░███░█',
251
+ '█░███░░░░░███░█',
252
+ '█░░░░░███░░░░░█',
253
+ '███████████████'
254
+ ],
255
+ // Frame 2: Cow moves forward
256
+ [
257
+ '███████████████',
258
+ '█░🐄░░███░░░░░█',
259
+ '█░███░███░███░█',
260
+ '█░███░░░░░███░█',
261
+ '█░░░░░███░░░░░█',
262
+ '███████████████'
263
+ ],
264
+ // Frame 3: Cow continues
265
+ [
266
+ '███████████████',
267
+ '█░░🐄░███░░░░░█',
268
+ '█░███░███░███░█',
269
+ '█░███░░░░░███░█',
270
+ '█░░░░░███░░░░░█',
271
+ '███████████████'
272
+ ],
273
+ // Frame 4: Cow turns down
274
+ [
275
+ '███████████████',
276
+ '█░░░🐄███░░░░░█',
277
+ '█░███░███░███░█',
278
+ '█░███░░░░░███░█',
279
+ '█░░░░░███░░░░░█',
280
+ '███████████████'
281
+ ],
282
+ // Frame 5: Cow goes down
283
+ [
284
+ '███████████████',
285
+ '█░░░░░███░░░░░█',
286
+ '█░███🐄███░███░█',
287
+ '█░███░░░░░███░█',
288
+ '█░░░░░███░░░░░█',
289
+ '███████████████'
290
+ ],
291
+ // Frame 6: Cow continues down
292
+ [
293
+ '███████████████',
294
+ '█░░░░░███░░░░░█',
295
+ '█░███░███░███░█',
296
+ '█░███🐄░░░███░█',
297
+ '█░░░░░███░░░░░█',
298
+ '███████████████'
299
+ ],
300
+ // Frame 7: Cow moves right
301
+ [
302
+ '███████████████',
303
+ '█░░░░░███░░░░░█',
304
+ '█░███░███░███░█',
305
+ '█░███░🐄░░███░█',
306
+ '█░░░░░███░░░░░█',
307
+ '███████████████'
308
+ ],
309
+ // Frame 8: Cow reaches bottom
310
+ [
311
+ '███████████████',
312
+ '█░░░░░███░░░░░█',
313
+ '█░███░███░███░█',
314
+ '█░███░░🐄░███░█',
315
+ '█░░░░░███░░░░░█',
316
+ '███████████████'
317
+ ],
318
+ // Frame 9: Cow moves down to exit
319
+ [
320
+ '███████████████',
321
+ '█░░░░░███░░░░░█',
322
+ '█░███░███░███░█',
323
+ '█░███░░░░░███░█',
324
+ '█░░░░🐄███░░░░█',
325
+ '███████████████'
326
+ ],
327
+ // Frame 10: Cow approaches exit
328
+ [
329
+ '███████████████',
330
+ '█░░░░░███░░░░░█',
331
+ '█░███░███░███░█',
332
+ '█░███░░░░░███░█',
333
+ '█░░░░░███🐄░░░█',
334
+ '███████████████'
335
+ ],
336
+ // Frame 11: Cow exits!
337
+ [
338
+ '███████████████',
339
+ '█░░░░░███░░░░░█',
340
+ '█░███░███░███░█',
341
+ '█░███░░░░░███░█',
342
+ '█░░░░░███░░🐄░█',
343
+ '███████████████'
344
+ ],
345
+ // Frame 12: Cow is free!
346
+ [
347
+ '███████████████',
348
+ '█░░░░░███░░░░░█',
349
+ '█░███░███░███░█',
350
+ '█░███░░░░░███░█',
351
+ '█░░░░░███░░░🐄█',
352
+ '███████████████'
353
+ ],
354
+ // Frame 13: Cow outside the maze
355
+ [
356
+ '███████████████',
357
+ '█░░░░░███░░░░░█',
358
+ '█░███░███░███░█',
359
+ '█░███░░░░░███░█',
360
+ '█░░░░░███░░░░░█',
361
+ '███████████████ 🐄'
362
+ ]
363
+ ]
364
+
365
+ console.log('\n')
366
+
367
+ // Animate through frames
368
+ for (const frame of frames) {
369
+ clearConsole()
370
+ console.log('')
371
+ frame.forEach(line => console.log(' ' + line))
372
+ await new Promise(resolve => setTimeout(resolve, 300))
373
+ }
374
+
375
+ // Show completion message
376
+ await new Promise(resolve => setTimeout(resolve, 500))
377
+ console.log('')
378
+ console.log(` ✓ ${action.message}`)
379
+
380
+ if (action.unlocked) {
381
+ await new Promise(resolve => setTimeout(resolve, 300))
382
+ console.log(` 🔓 Desbloqueaste: ${action.unlocked}`)
226
383
  }
384
+
385
+ console.log('')
386
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
227
387
  }
228
388
  }
package/lib/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { executeSintaxis } from '../executors/sintaxis-executor.js'
6
6
  import { executePortfolio } from '../executors/portfolio-executor.js'
7
7
  import { showHelp } from '../commands/help.js'
8
8
  import { CretaCodeSession } from '../session.js'
9
+ import { greetUser } from '../utils/greeting.js'
9
10
 
10
11
  async function executeMainMenu() {
11
12
  const menu = new MenuBuilder(MAIN_MENU_CONFIG)
@@ -71,6 +72,9 @@ export async function handleCommand(command, args = []) {
71
72
  export async function runCLI(args = []) {
72
73
  const [command, ...rest] = args
73
74
 
75
+ // Greet user (ask name if first time, or show stats if returning)
76
+ await greetUser()
77
+
74
78
  if (!command) {
75
79
  await executeMainMenu()
76
80
  return 0
@@ -115,27 +115,13 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
115
115
  type: 'text'
116
116
  },
117
117
  {
118
- code: 'class Book {\n' +
119
- ' constructor(title, author, isbn) {\n' +
120
- ' this.title = title\n' +
121
- ' this.author = author\n' +
122
- ' this.isbn = isbn\n' +
123
- ' this.isLoaned = false\n' +
124
- ' }\n' +
125
- '\n' +
126
- ' isAvailable() {\n' +
127
- ' return !this.isLoaned\n' +
128
- ' }\n' +
129
- '\n' +
130
- ' markAsLoaned() {\n' +
131
- ' this.isLoaned = true\n' +
132
- ' }\n' +
133
- '\n' +
134
- ' markAsReturned() {\n' +
135
- ' this.isLoaned = false\n' +
136
- ' }\n' +
118
+ code: 'interface Book {\n' +
119
+ ' isAvailable(): boolean\n' +
120
+ ' markAsLoaned(): void\n' +
121
+ ' markAsReturned(): void\n' +
137
122
  '}',
138
- title: '📄 Clase Book',
123
+ title: '📄 Objeto: Book',
124
+ after: 'Responsabilidad: Conocer su propio estado de disponibilidad',
139
125
  type: 'code'
140
126
  },
141
127
  {
@@ -143,28 +129,13 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
143
129
  type: 'pause'
144
130
  },
145
131
  {
146
- code: 'class User {\n' +
147
- ' constructor(name, email, maxLoans = 3) {\n' +
148
- ' this.name = name\n' +
149
- ' this.email = email\n' +
150
- ' this.maxLoans = maxLoans\n' +
151
- ' this.currentLoans = []\n' +
152
- ' }\n' +
153
- '\n' +
154
- ' canBorrow() {\n' +
155
- ' return this.currentLoans.length < this.maxLoans\n' +
156
- ' }\n' +
157
- '\n' +
158
- ' addLoan(loan) {\n' +
159
- ' this.currentLoans.push(loan)\n' +
160
- ' }\n' +
161
- '\n' +
162
- ' removeLoan(loan) {\n' +
163
- ' const index = this.currentLoans.indexOf(loan)\n' +
164
- ' if (index > -1) this.currentLoans.splice(index, 1)\n' +
165
- ' }\n' +
132
+ code: 'interface User {\n' +
133
+ ' canBorrow(): boolean\n' +
134
+ ' addLoan(loan: Loan): void\n' +
135
+ ' removeLoan(loan: Loan): void\n' +
166
136
  '}',
167
- title: '📄 Clase User',
137
+ title: '📄 Objeto: User',
138
+ after: 'Responsabilidad: Conocer sus propios límites de préstamo',
168
139
  type: 'code'
169
140
  },
170
141
  {
@@ -172,32 +143,13 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
172
143
  type: 'pause'
173
144
  },
174
145
  {
175
- code: 'class Loan {\n' +
176
- ' constructor(user, book, durationDays = 14) {\n' +
177
- ' this.user = user\n' +
178
- ' this.book = book\n' +
179
- ' this.startDate = new Date()\n' +
180
- ' this.dueDate = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000)\n' +
181
- ' this.returned = false\n' +
182
- ' }\n' +
183
- '\n' +
184
- ' isOverdue() {\n' +
185
- ' return !this.returned && new Date() > this.dueDate\n' +
186
- ' }\n' +
187
- '\n' +
188
- ' calculateFine() {\n' +
189
- ' if (!this.isOverdue()) return 0\n' +
190
- ' const daysLate = Math.ceil((new Date() - this.dueDate) / (24 * 60 * 60 * 1000))\n' +
191
- ' return daysLate * 5 // $5 por día\n' +
192
- ' }\n' +
193
- '\n' +
194
- ' returnBook() {\n' +
195
- ' this.returned = true\n' +
196
- ' this.book.markAsReturned()\n' +
197
- ' this.user.removeLoan(this)\n' +
198
- ' }\n' +
146
+ code: 'interface Loan {\n' +
147
+ ' isOverdue(): boolean\n' +
148
+ ' calculateFine(): number\n' +
149
+ ' returnBook(): void\n' +
199
150
  '}',
200
- title: '📄 Clase Loan',
151
+ title: '📄 Objeto: Loan',
152
+ after: 'Responsabilidad: Gestionar la relación temporal entre usuario y libro',
201
153
  type: 'code'
202
154
  },
203
155
  {
@@ -205,36 +157,14 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
205
157
  type: 'pause'
206
158
  },
207
159
  {
208
- lines: [
209
- '\nclass Library {',
210
- ' constructor() {',
211
- ' this.books = []',
212
- ' this.users = []',
213
- ' this.loans = []',
214
- ' }',
215
- '',
216
- ' lendBook(user, book) {',
217
- ' if (!user.canBorrow()) return null',
218
- ' if (!book.isAvailable()) return null',
219
- '',
220
- ' const loan = new Loan(user, book)',
221
- ' book.markAsLoaned()',
222
- ' user.addLoan(loan)',
223
- ' this.loans.push(loan)',
224
- ' return loan',
225
- ' }',
226
- '',
227
- ' returnBook(loan) {',
228
- ' loan.returnBook()',
229
- ' return loan.calculateFine()',
230
- ' }',
231
- '',
232
- ' getOverdueLoans() {',
233
- ' return this.loans.filter(loan => loan.isOverdue())',
234
- ' }',
235
- '}'
236
- ],
237
- type: 'text'
160
+ code: 'interface Library {\n' +
161
+ ' lendBook(user: User, book: Book): Loan\n' +
162
+ ' returnBook(loan: Loan): number\n' +
163
+ ' getOverdueLoans(): Loan[]\n' +
164
+ '}',
165
+ title: '📄 Objeto: Library',
166
+ after: 'Responsabilidad: Coordinar las operaciones del sistema',
167
+ type: 'code'
238
168
  },
239
169
  {
240
170
  message: '\nPresiona Enter para ver la interacción...',
@@ -245,25 +175,29 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
245
175
  },
246
176
  {
247
177
  lines: [
248
- 'Ejemplo de uso:',
178
+ '🔄 Interacciones entre objetos:',
179
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
249
180
  '',
250
- 'const library = new Library()',
251
- "const book = new Book('1984', 'George Orwell', '978-0451524935')",
252
- "const user = new User('Ana García', 'ana@email.com')",
181
+ '1. Library solicita a User:',
182
+ ' → user.canBorrow()',
183
+ ' responde: true/false',
253
184
  '',
254
- '// Los objetos interactúan entre sí',
255
- 'const loan = library.lendBook(user, book)',
256
- 'console.log(book.isAvailable()) // false',
257
- 'console.log(user.canBorrow()) // true (2 préstamos disponibles)',
185
+ '2. Library solicita a Book:',
186
+ ' book.isAvailable()',
187
+ ' responde: true/false',
258
188
  '',
259
- '// Simular libro vencido',
260
- 'loan.dueDate = new Date(Date.now() - 24 * 60 * 60 * 1000)',
261
- 'console.log(loan.isOverdue()) // true',
262
- 'console.log(loan.calculateFine()) // 5',
189
+ '3. Library crea Loan y coordina:',
190
+ ' book.markAsLoaned()',
191
+ ' → user.addLoan(loan)',
263
192
  '',
264
- '// Devolver libro',
265
- 'const fine = library.returnBook(loan)',
266
- 'console.log(book.isAvailable()) // true'
193
+ '4. Al devolver, Library solicita a Loan:',
194
+ ' loan.isOverdue()',
195
+ ' responde: true/false',
196
+ ' → loan.calculateFine()',
197
+ ' ← responde: 25 (ejemplo)',
198
+ '',
199
+ '💡 Nota: Los objetos NO acceden directamente a datos de otros.',
200
+ ' Solo se comunican a través de solicitudes a sus interfaces.'
267
201
  ],
268
202
  type: 'text'
269
203
  },
@@ -290,10 +224,10 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
290
224
  '¿Cómo decidimos que necesitábamos exactamente estos 4 objetos?',
291
225
  '¿Por qué no 3? ¿Por qué no 10? ¿Cuál es el criterio?',
292
226
  '\n🔍 Criterio de descomposición:',
293
- '• Book - responsabilidad de estado propio',
294
- '• User - responsabilidad de sus límites',
295
- '• Loan - responsabilidad de la relación temporal',
296
- '• Library - responsabilidad de coordinación',
227
+ '• Book - define operaciones para gestionar su disponibilidad',
228
+ '• User - define operaciones para gestionar sus límites',
229
+ '• Loan - define operaciones para gestionar la relación temporal',
230
+ '• Library - define operaciones para coordinar el sistema',
297
231
  '\n📚 Conexión con lecciones siguientes:',
298
232
  '• Lección 2: Los objetos interactúan a través de solicitudes',
299
233
  '• Lección 3: Las solicitudes son la única forma de ejecutar operaciones',
@@ -307,6 +241,11 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
307
241
  {
308
242
  message: '\n✨ ¡Lección 1 completada! Presiona Enter para continuar...',
309
243
  type: 'pause'
244
+ },
245
+ {
246
+ type: 'maze-completion',
247
+ message: 'Lección 1: Sistema completado!',
248
+ unlocked: 'Lección 2: Solicitudes'
310
249
  }
311
250
  ]
312
251
  }
@@ -313,6 +313,11 @@ export const LESSON_2_OBJECT_REQUESTS = {
313
313
  {
314
314
  message: '\n✨ ¡Lección 2 completada! Presiona Enter para salir...',
315
315
  type: 'pause'
316
+ },
317
+ {
318
+ type: 'maze-completion',
319
+ message: 'Lección 2: Solicitudes completado!',
320
+ unlocked: 'Lección 3: Única forma'
316
321
  }
317
322
  ]
318
323
  }
@@ -344,6 +344,11 @@ export const LESSON_3_ONLY_WAY = {
344
344
  {
345
345
  message: '\n✨ ¡Lección 3 completada! Presiona Enter para salir...',
346
346
  type: 'pause'
347
+ },
348
+ {
349
+ type: 'maze-completion',
350
+ message: 'Lección 3: Única forma completado!',
351
+ unlocked: 'Lección 4: Firmas de operación'
347
352
  }
348
353
  ]
349
354
  }
@@ -327,6 +327,11 @@ export const LESSON_4_OPERATION_SIGNATURES = {
327
327
  {
328
328
  message: '\n✨ ¡Lección 4 completada! Presiona Enter para salir...',
329
329
  type: 'pause'
330
+ },
331
+ {
332
+ type: 'maze-completion',
333
+ message: 'Lección 4: Firmas de operación completado!',
334
+ unlocked: 'Lección 5: Conjunto de firmas'
330
335
  }
331
336
  ]
332
337
  }
@@ -336,6 +336,11 @@ export const LESSON_5_INTERFACE_SET = {
336
336
  {
337
337
  message: '\n✨ ¡Lección 5 completada! Presiona Enter para salir...',
338
338
  type: 'pause'
339
+ },
340
+ {
341
+ type: 'maze-completion',
342
+ message: 'Lección 5: Conjunto de firmas completado!',
343
+ unlocked: 'Lección 6: Énfasis en interfaces'
339
344
  }
340
345
  ]
341
346
  }
@@ -402,6 +402,11 @@ export const LESSON_6_INTERFACE_DESIGN = {
402
402
  {
403
403
  message: '\n✨ ¡Lección 6 completada! Presiona Enter para salir...',
404
404
  type: 'pause'
405
+ },
406
+ {
407
+ type: 'maze-completion',
408
+ message: 'Lección 6: Énfasis en interfaces completado!',
409
+ unlocked: 'Lección 7: Definición de objeto'
405
410
  }
406
411
  ]
407
412
  }
@@ -370,6 +370,11 @@ export const LESSON_7_OBJECT_DEFINITION = {
370
370
  {
371
371
  message: '\n✨ ¡Lección 7 completada! Presiona Enter para continuar...',
372
372
  type: 'pause'
373
+ },
374
+ {
375
+ type: 'maze-completion',
376
+ message: 'Lección 7: Definición de objeto completado!',
377
+ unlocked: '¡Completaste todos los enunciados fundamentales!'
373
378
  }
374
379
  ]
375
380
  }
@@ -1,7 +1,6 @@
1
1
  // Terminal Básico - Comandos fundamentales de navegación
2
2
  export const TERMINAL_BASICO = {
3
3
  id: 'terminal-basico',
4
- title: 'Terminal Básico',
5
4
 
6
5
  intro: {
7
6
  definition: 'Sintaxis: estudia el modo en que se combinan las palabras y los distintos grupos que forman para expresar significado.',
@@ -41,6 +40,11 @@ export const TERMINAL_BASICO = {
41
40
  explanation: 'mkdir crea un nuevo directorio (carpeta).',
42
41
  example: 'mkdir mi-proyecto',
43
42
  instruction: 'Crea una carpeta nueva. Prueba: mkdir prueba'
43
+ },
44
+ {
45
+ type: 'maze-completion',
46
+ message: 'Terminal básico completado!',
47
+ unlocked: 'Git básico'
44
48
  }
45
49
  ]
46
50
  }
@@ -3,6 +3,7 @@ import { ENUNCIADOS_MENU_CONFIG } from '../data/menus.js'
3
3
  import { LESSONS_BY_ID } from '../data/lessons/index.js'
4
4
  import { LessonBuilder } from '../builders/LessonBuilder.js'
5
5
  import { createPromptInterface } from '../utils/input.js'
6
+ import { clearConsole } from '../utils/output.js'
6
7
 
7
8
  const UPCOMING_MESSAGE = [
8
9
  '\n🚀 Próximamente:',
@@ -30,6 +31,15 @@ async function runLesson(lessonId, enunciadoTexto, { LessonCtor = LessonBuilder,
30
31
  return
31
32
  }
32
33
 
34
+ // Show selection confirmation (NEW UX PATTERN)
35
+ console.log(`\nElegiste: Enunciado ${lessonId}`)
36
+ console.log('Cargando...')
37
+ await new Promise((resolve) => delay(resolve, 1000))
38
+
39
+ // Clear screen but keep scroll history (NEW UX PATTERN)
40
+ clearConsole()
41
+
42
+ // Show enunciado text
33
43
  console.log(enunciadoTexto)
34
44
  await new Promise((resolve) => delay(resolve, 1500))
35
45
 
@@ -1,7 +1,18 @@
1
1
  import { LessonBuilder } from '../builders/LessonBuilder.js'
2
2
  import { TERMINAL_BASICO } from '../data/lessons/sintaxis/terminal-basico.js'
3
+ import { clearConsole } from '../utils/output.js'
3
4
 
4
5
  export async function executeSintaxis() {
6
+ // Show selection confirmation
7
+ console.log('\nElegiste: Aprender sintaxis')
8
+ console.log('Cargando...')
9
+
10
+ await new Promise(resolve => setTimeout(resolve, 1000))
11
+
12
+ // Clear screen but keep scroll history
13
+ clearConsole()
14
+
15
+ // Start lesson
5
16
  const lesson = new LessonBuilder(TERMINAL_BASICO)
6
17
  await lesson.start()
7
18
  }
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk'
2
+ import { UserState } from './user-state.js'
3
+ import { askQuestion, createPromptInterface } from './input.js'
4
+
5
+ export async function greetUser() {
6
+ const state = UserState.updateLastSeen()
7
+
8
+ // First time user - ask for name
9
+ if (!state.name) {
10
+ const rl = createPromptInterface()
11
+
12
+ console.log('')
13
+ const name = await askQuestion(rl, chalk.cyan('👋 ¿Cómo te llamas? '))
14
+
15
+ if (name && name.trim()) {
16
+ UserState.setName(name.trim())
17
+ console.log(chalk.green(`\n¡Bienvenido, ${name.trim()}!`))
18
+ }
19
+
20
+ rl.close()
21
+ console.log('')
22
+ return
23
+ }
24
+
25
+ // Returning user - show personalized greeting
26
+ const lessonsCount = state.lessonsCompleted.length
27
+ const greeting = lessonsCount > 0
28
+ ? `¡Hola de vuelta, ${state.name}! 📊 ${lessonsCount} ${lessonsCount === 1 ? 'lección completada' : 'lecciones completadas'}`
29
+ : `¡Hola de vuelta, ${state.name}!`
30
+
31
+ console.log(chalk.cyan(`\n${greeting}\n`))
32
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ const STATE_DIR = path.join(os.homedir(), '.creta')
6
+ const STATE_FILE = path.join(STATE_DIR, 'user.json')
7
+
8
+ export class UserState {
9
+ static load() {
10
+ try {
11
+ if (!fs.existsSync(STATE_FILE)) {
12
+ return this.createDefault()
13
+ }
14
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
15
+ } catch {
16
+ return this.createDefault()
17
+ }
18
+ }
19
+
20
+ static save(data) {
21
+ try {
22
+ if (!fs.existsSync(STATE_DIR)) {
23
+ fs.mkdirSync(STATE_DIR, { recursive: true })
24
+ }
25
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2))
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ static createDefault() {
33
+ return {
34
+ name: null,
35
+ createdAt: new Date().toISOString(),
36
+ lessonsCompleted: [],
37
+ projectsCreated: [],
38
+ discord: null,
39
+ stats: {
40
+ totalSessions: 0,
41
+ lastSeen: null
42
+ }
43
+ }
44
+ }
45
+
46
+ static updateLastSeen() {
47
+ const state = this.load()
48
+ state.stats.lastSeen = new Date().toISOString()
49
+ state.stats.totalSessions += 1
50
+ this.save(state)
51
+ return state
52
+ }
53
+
54
+ static completLesson(lessonId) {
55
+ const state = this.load()
56
+
57
+ // Check if already completed
58
+ const alreadyCompleted = state.lessonsCompleted.find(l => l.id === lessonId)
59
+ if (alreadyCompleted) return state
60
+
61
+ state.lessonsCompleted.push({
62
+ id: lessonId,
63
+ completedAt: new Date().toISOString()
64
+ })
65
+
66
+ this.save(state)
67
+ return state
68
+ }
69
+
70
+ static addProject(projectName, projectType, level = 0) {
71
+ const state = this.load()
72
+
73
+ state.projectsCreated.push({
74
+ name: projectName,
75
+ type: projectType,
76
+ level,
77
+ createdAt: new Date().toISOString()
78
+ })
79
+
80
+ this.save(state)
81
+ return state
82
+ }
83
+
84
+ static setName(name) {
85
+ const state = this.load()
86
+ state.name = name
87
+ this.save(state)
88
+ return state
89
+ }
90
+
91
+ static getPath() {
92
+ return STATE_FILE
93
+ }
94
+
95
+ static hasName() {
96
+ const state = this.load()
97
+ return state.name !== null && state.name.trim() !== ''
98
+ }
99
+
100
+ static getName() {
101
+ const state = this.load()
102
+ return state.name
103
+ }
104
+
105
+ static getStats() {
106
+ const state = this.load()
107
+ return {
108
+ name: state.name,
109
+ lessonsCompleted: state.lessonsCompleted.length,
110
+ projectsCreated: state.projectsCreated.length,
111
+ totalSessions: state.stats.totalSessions,
112
+ lastSeen: state.stats.lastSeen
113
+ }
114
+ }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icarusmx/creta",
3
- "version": "1.3.5",
3
+ "version": "1.4.1",
4
4
  "description": "Salgamos de este laberinto.",
5
5
  "type": "module",
6
6
  "bin": {