@icarusmx/creta 1.4.3 → 1.4.4

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.
@@ -20,11 +20,6 @@ export class LessonBuilder {
20
20
  try {
21
21
  const flow = this.collectActions()
22
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
- }
28
23
  } finally {
29
24
  this.rl.close()
30
25
  this.rl = null
@@ -377,6 +372,11 @@ export class LessonBuilder {
377
372
  console.log('')
378
373
  console.log(` ✓ ${action.message}`)
379
374
 
375
+ // Track lesson completion
376
+ if (this.lesson.id) {
377
+ UserState.addCompletedLesson(this.lesson.id)
378
+ }
379
+
380
380
  if (action.unlocked) {
381
381
  await new Promise(resolve => setTimeout(resolve, 300))
382
382
  console.log(` 🔓 Desbloqueaste: ${action.unlocked}`)
@@ -4,6 +4,7 @@ import { clearConsole } from '../utils/output.js'
4
4
  export class MenuBuilder {
5
5
  constructor(config) {
6
6
  this.config = config
7
+ this.firstRender = true
7
8
  }
8
9
 
9
10
  async show() {
@@ -23,8 +24,13 @@ export class MenuBuilder {
23
24
  }
24
25
  }
25
26
 
26
- renderHeader() {
27
- clearConsole()
27
+ renderHeader(skipClear = false) {
28
+ // Skip clear on first render if config says so, or if explicitly told to skip
29
+ const shouldClear = skipClear ? false : (this.firstRender ? this.config.clearConsole !== false : true)
30
+
31
+ if (shouldClear) {
32
+ clearConsole()
33
+ }
28
34
 
29
35
  if (this.config.banner) {
30
36
  console.log(this.config.banner)
@@ -41,6 +47,8 @@ export class MenuBuilder {
41
47
  if (this.config.title) {
42
48
  console.log(this.config.title)
43
49
  }
50
+
51
+ this.firstRender = false
44
52
  }
45
53
 
46
54
  renderOption(option, isSelected, index) {
@@ -65,12 +73,29 @@ export class MenuBuilder {
65
73
  }
66
74
 
67
75
  let selectedIndex = 0
76
+ let lineCount = 0
77
+
78
+ const render = (isFirstRender = false) => {
79
+ if (!isFirstRender && lineCount > 0) {
80
+ // Move cursor up and clear previous render
81
+ process.stdout.write(`\x1b[${lineCount}A`)
82
+ process.stdout.write('\x1b[0J')
83
+ }
68
84
 
69
- const render = () => {
70
- this.renderHeader()
85
+ // Never clear in renderHeader when doing cursor-based rendering
86
+ this.renderHeader(true)
71
87
  this.config.options.forEach((option, index) => {
72
88
  this.renderOption(option, index === selectedIndex, index)
73
89
  })
90
+
91
+ // Count lines for next render (approximate)
92
+ lineCount = 2 // title line + blank
93
+ if (this.config.description) {
94
+ lineCount += Array.isArray(this.config.description) ? this.config.description.length + 1 : 2
95
+ }
96
+ this.config.options.forEach(option => {
97
+ lineCount += option.description ? 3 : 2 // option + optional description + blank
98
+ })
74
99
  }
75
100
 
76
101
  return new Promise((resolve) => {
@@ -107,7 +132,7 @@ export class MenuBuilder {
107
132
  }
108
133
 
109
134
  process.stdin.on('data', onKeyPress)
110
- render()
135
+ render(true)
111
136
  })
112
137
  }
113
138
 
package/lib/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MenuBuilder } from '../builders/MenuBuilder.js'
2
- import { MAIN_MENU_CONFIG } from '../data/menus.js'
2
+ import { getMainMenuConfig } from '../data/menus.js'
3
3
  import { executeEnunciados } from '../executors/enunciados-executor.js'
4
4
  import { executeProyectos } from '../executors/proyectos-executor.js'
5
5
  import { executeSintaxis } from '../executors/sintaxis-executor.js'
@@ -9,7 +9,7 @@ import { CretaCodeSession } from '../session.js'
9
9
  import { greetUser } from '../utils/greeting.js'
10
10
 
11
11
  async function executeMainMenu() {
12
- const menu = new MenuBuilder(MAIN_MENU_CONFIG)
12
+ const menu = new MenuBuilder(getMainMenuConfig())
13
13
 
14
14
  while (true) {
15
15
  const choice = await menu.show()
@@ -33,12 +33,19 @@ async function runCodeSession() {
33
33
  await session.start()
34
34
  }
35
35
 
36
+ async function resetUserState() {
37
+ const { UserState } = await import('../utils/user-state.js')
38
+ UserState.reset()
39
+ console.log('✅ Estado de usuario reiniciado. La próxima vez que ejecutes creta, se te pedirá tu nombre de nuevo.')
40
+ }
41
+
36
42
  const COMMANDS = new Map([
37
43
  ['enunciados', () => executeEnunciados()],
38
44
  ['sintaxis', () => executeSintaxis()],
39
45
  ['proyectos', () => executeProyectos()],
40
46
  ['portafolio', () => executePortfolio(0)],
41
47
  ['code', () => runCodeSession()],
48
+ ['reset', () => resetUserState()],
42
49
  ['help', () => showHelp()],
43
50
  ['ayuda', () => showHelp()]
44
51
  ])
@@ -72,8 +79,10 @@ export async function handleCommand(command, args = []) {
72
79
  export async function runCLI(args = []) {
73
80
  const [command, ...rest] = args
74
81
 
75
- // Greet user (ask name if first time, or show stats if returning)
76
- await greetUser()
82
+ // Skip greeting for reset command
83
+ if (command !== 'reset') {
84
+ await greetUser()
85
+ }
77
86
 
78
87
  if (!command) {
79
88
  await executeMainMenu()
@@ -115,13 +115,27 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
115
115
  type: 'text'
116
116
  },
117
117
  {
118
- code: 'interface Book {\n' +
119
- ' isAvailable(): boolean\n' +
120
- ' markAsLoaned(): void\n' +
121
- ' markAsReturned(): void\n' +
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' +
122
137
  '}',
123
- title: '📄 Objeto: Book',
124
- after: 'Responsabilidad: Conocer su propio estado de disponibilidad',
138
+ title: '📄 Clase Book',
125
139
  type: 'code'
126
140
  },
127
141
  {
@@ -129,13 +143,28 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
129
143
  type: 'pause'
130
144
  },
131
145
  {
132
- code: 'interface User {\n' +
133
- ' canBorrow(): boolean\n' +
134
- ' addLoan(loan: Loan): void\n' +
135
- ' removeLoan(loan: Loan): void\n' +
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' +
136
166
  '}',
137
- title: '📄 Objeto: User',
138
- after: 'Responsabilidad: Conocer sus propios límites de préstamo',
167
+ title: '📄 Clase User',
139
168
  type: 'code'
140
169
  },
141
170
  {
@@ -143,13 +172,32 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
143
172
  type: 'pause'
144
173
  },
145
174
  {
146
- code: 'interface Loan {\n' +
147
- ' isOverdue(): boolean\n' +
148
- ' calculateFine(): number\n' +
149
- ' returnBook(): void\n' +
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' +
150
199
  '}',
151
- title: '📄 Objeto: Loan',
152
- after: 'Responsabilidad: Gestionar la relación temporal entre usuario y libro',
200
+ title: '📄 Clase Loan',
153
201
  type: 'code'
154
202
  },
155
203
  {
@@ -157,14 +205,36 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
157
205
  type: 'pause'
158
206
  },
159
207
  {
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'
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'
168
238
  },
169
239
  {
170
240
  message: '\nPresiona Enter para ver la interacción...',
@@ -175,29 +245,25 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
175
245
  },
176
246
  {
177
247
  lines: [
178
- '🔄 Interacciones entre objetos:',
179
- '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
248
+ 'Ejemplo de uso:',
180
249
  '',
181
- '1. Library solicita a User:',
182
- ' → user.canBorrow()',
183
- ' responde: true/false',
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')",
184
253
  '',
185
- '2. Library solicita a Book:',
186
- ' book.isAvailable()',
187
- ' responde: true/false',
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)',
188
258
  '',
189
- '3. Library crea Loan y coordina:',
190
- ' book.markAsLoaned()',
191
- ' → user.addLoan(loan)',
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',
192
263
  '',
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.'
264
+ '// Devolver libro',
265
+ 'const fine = library.returnBook(loan)',
266
+ 'console.log(book.isAvailable()) // true'
201
267
  ],
202
268
  type: 'text'
203
269
  },
@@ -224,10 +290,10 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
224
290
  '¿Cómo decidimos que necesitábamos exactamente estos 4 objetos?',
225
291
  '¿Por qué no 3? ¿Por qué no 10? ¿Cuál es el criterio?',
226
292
  '\n🔍 Criterio de descomposició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',
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',
231
297
  '\n📚 Conexión con lecciones siguientes:',
232
298
  '• Lección 2: Los objetos interactúan a través de solicitudes',
233
299
  '• Lección 3: Las solicitudes son la única forma de ejecutar operaciones',
@@ -241,11 +307,6 @@ export const LESSON_1_SYSTEM_DECOMPOSITION = {
241
307
  {
242
308
  message: '\n✨ ¡Lección 1 completada! Presiona Enter para continuar...',
243
309
  type: 'pause'
244
- },
245
- {
246
- type: 'maze-completion',
247
- message: 'Lección 1: Sistema completado!',
248
- unlocked: 'Lección 2: Solicitudes'
249
310
  }
250
311
  ]
251
312
  }
@@ -313,11 +313,6 @@ 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'
321
316
  }
322
317
  ]
323
318
  }
@@ -344,11 +344,6 @@ 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'
352
347
  }
353
348
  ]
354
349
  }
@@ -327,11 +327,6 @@ 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'
335
330
  }
336
331
  ]
337
332
  }
@@ -336,11 +336,6 @@ 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'
344
339
  }
345
340
  ]
346
341
  }
@@ -402,11 +402,6 @@ 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'
410
405
  }
411
406
  ]
412
407
  }
@@ -370,11 +370,6 @@ 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!'
378
373
  }
379
374
  ]
380
375
  }
@@ -0,0 +1,58 @@
1
+ // Git Básico - Comandos fundamentales de control de versiones
2
+ export const GIT_BASICO = {
3
+ id: 'git-basico',
4
+
5
+ intro: {
6
+ definition: 'Git es un sistema de control de versiones que te permite rastrear cambios en tu código.',
7
+ explanation: 'Con git puedes guardar "snapshots" de tu proyecto en diferentes momentos.',
8
+ detail: 'Esto te permite experimentar sin miedo, colaborar con otros, y volver atrás si algo sale mal.'
9
+ },
10
+
11
+ steps: [
12
+ {
13
+ type: 'command-intro',
14
+ command: 'git init',
15
+ description: 'Primer comando: git init',
16
+ explanation: 'git init inicializa un repositorio de git en tu carpeta actual.',
17
+ example: 'git init',
18
+ instruction: 'Úsalo una sola vez al crear un nuevo proyecto. Crea una carpeta oculta .git que rastrea cambios.'
19
+ },
20
+ {
21
+ type: 'command-intro',
22
+ command: 'git status',
23
+ description: 'Segundo comando: git status',
24
+ explanation: 'git status muestra el estado actual de tu repositorio.',
25
+ example: 'git status',
26
+ instruction: 'Te dice qué archivos han cambiado, cuáles están listos para commit, etc.'
27
+ },
28
+ {
29
+ type: 'command-intro',
30
+ command: 'git add',
31
+ description: 'Tercer comando: git add',
32
+ explanation: 'git add prepara archivos para ser guardados en el siguiente commit.',
33
+ example: 'git add archivo.js\ngit add .',
34
+ instruction: 'Usa "git add ." para agregar todos los cambios, o especifica archivos individuales.'
35
+ },
36
+ {
37
+ type: 'command-intro',
38
+ command: 'git commit',
39
+ description: 'Cuarto comando: git commit',
40
+ explanation: 'git commit guarda un snapshot de los cambios que agregaste con git add.',
41
+ example: 'git commit -m "Mensaje descriptivo"',
42
+ instruction: 'Siempre incluye un mensaje que describa qué cambiaste y por qué.'
43
+ },
44
+ {
45
+ type: 'command-intro',
46
+ command: 'git log',
47
+ description: 'Quinto comando: git log',
48
+ explanation: 'git log muestra el historial de commits de tu proyecto.',
49
+ example: 'git log\ngit log --oneline',
50
+ instruction: 'Usa --oneline para ver una versión compacta del historial.'
51
+ },
52
+ {
53
+ type: 'maze-completion',
54
+ message: 'Git básico completado!',
55
+ unlocked: 'Git colaboración (próximamente)'
56
+ }
57
+ ]
58
+ }
package/lib/data/menus.js CHANGED
@@ -1,14 +1,56 @@
1
1
  import { ENUNCIADOS } from './enunciados.js'
2
+ import { UNLOCK_CONFIG, isUnlocked, hasCompletedLesson } from './progression.js'
3
+ import { UserState } from '../utils/user-state.js'
2
4
 
3
- export const MAIN_MENU_CONFIG = {
4
- title: 'Te ofrecemos las siguientes opciones:',
5
+ export const SINTAXIS_MENU_CONFIG = {
6
+ title: 'Elige qué lección de sintaxis te gustaría explorar:',
7
+ description: 'Aprende los comandos fundamentales de terminal y git.',
5
8
  options: [
6
- { id: 'sintaxis', title: 'Aprender sintaxis' },
7
- { id: 'enunciados', title: 'Aprender conceptos de diseño' },
8
- { id: 'proyectos', title: 'Construir proyectos' }
9
+ { id: 'terminal-basico', title: '1. Terminal básico', description: 'ls, cd, mkdir' },
10
+ { id: 'git-basico', title: '2. Git básico', description: 'init, status, add, commit' }
9
11
  ]
10
12
  }
11
13
 
14
+ const MENU_ITEMS = {
15
+ sintaxis: { baseTitle: 'Aprender sintaxis' },
16
+ enunciados: { baseTitle: 'Aprender conceptos de diseño' },
17
+ proyectos: { baseTitle: 'Construir proyectos' }
18
+ }
19
+
20
+ export function getMainMenuConfig() {
21
+ const state = UserState.get()
22
+ const options = []
23
+
24
+ for (const [id, itemConfig] of Object.entries(MENU_ITEMS)) {
25
+ const unlockConfig = UNLOCK_CONFIG.mainMenu[id]
26
+
27
+ if (!isUnlocked(id, unlockConfig)) continue
28
+
29
+ const showNew = typeof unlockConfig.showNewIndicator === 'function'
30
+ ? unlockConfig.showNewIndicator(state)
31
+ : unlockConfig.showNewIndicator
32
+
33
+ const description = typeof unlockConfig.description === 'function'
34
+ ? unlockConfig.description(state)
35
+ : unlockConfig.description
36
+
37
+ options.push({
38
+ id,
39
+ title: showNew ? `✨ ${itemConfig.baseTitle}` : itemConfig.baseTitle,
40
+ description
41
+ })
42
+ }
43
+
44
+ return {
45
+ clearConsole: false,
46
+ title: 'Te ofrecemos las siguientes opciones:',
47
+ options
48
+ }
49
+ }
50
+
51
+ // For backward compatibility
52
+ export const MAIN_MENU_CONFIG = getMainMenuConfig()
53
+
12
54
  export const ENUNCIADOS_MENU_CONFIG = {
13
55
  title: 'Elige qué enunciado te gustaría explorar:',
14
56
  description: [
@@ -22,4 +22,7 @@ export const HELP_TEXT = `
22
22
 
23
23
  💡 Tip: Si estás dentro de un proyecto existente, los comandos
24
24
  portafolio-1/2/3 actualizarán tus archivos directamente
25
+
26
+ 🎯 La filosofía Creta: partir de enunciados que generan 'ruido' para
27
+ construir comprensión real, no solo sintaxis.
25
28
  `
@@ -0,0 +1,44 @@
1
+ // Centralized progression and unlock logic
2
+ import { UserState } from '../utils/user-state.js'
3
+
4
+ export function hasCompletedLesson(lessonId) {
5
+ const state = UserState.get()
6
+ return state.lessonsCompleted.some(
7
+ lesson => typeof lesson === 'object' ? lesson.id === lessonId : lesson === lessonId
8
+ )
9
+ }
10
+
11
+ // Data-driven unlock configuration
12
+ export const UNLOCK_CONFIG = {
13
+ mainMenu: {
14
+ sintaxis: {
15
+ alwaysVisible: true,
16
+ showNewIndicator: (state) =>
17
+ hasCompletedLesson('terminal-basico') && !hasCompletedLesson('git-basico'),
18
+ description: (state) =>
19
+ hasCompletedLesson('terminal-basico') && !hasCompletedLesson('git-basico')
20
+ ? 'Git básico desbloqueado'
21
+ : undefined
22
+ },
23
+ enunciados: {
24
+ unlockAfter: 'terminal-basico',
25
+ showNewIndicator: true,
26
+ description: '7 enunciados fundamentales de OOP'
27
+ },
28
+ proyectos: {
29
+ unlockAfter: 'git-basico',
30
+ showNewIndicator: true,
31
+ description: 'Portafolios y pull requests'
32
+ }
33
+ },
34
+ sintaxis: {
35
+ directToLesson: (state) =>
36
+ !hasCompletedLesson('terminal-basico') ? 'terminal-basico' : null
37
+ }
38
+ }
39
+
40
+ export function isUnlocked(itemKey, config) {
41
+ if (config.alwaysVisible) return true
42
+ if (!config.unlockAfter) return true
43
+ return hasCompletedLesson(config.unlockAfter)
44
+ }
@@ -3,7 +3,6 @@ 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'
7
6
 
8
7
  const UPCOMING_MESSAGE = [
9
8
  '\n🚀 Próximamente:',
@@ -31,15 +30,6 @@ async function runLesson(lessonId, enunciadoTexto, { LessonCtor = LessonBuilder,
31
30
  return
32
31
  }
33
32
 
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
43
33
  console.log(enunciadoTexto)
44
34
  await new Promise((resolve) => delay(resolve, 1500))
45
35
 
@@ -1,18 +1,72 @@
1
+ import { MenuBuilder } from '../builders/MenuBuilder.js'
2
+ import { SINTAXIS_MENU_CONFIG } from '../data/menus.js'
1
3
  import { LessonBuilder } from '../builders/LessonBuilder.js'
2
4
  import { TERMINAL_BASICO } from '../data/lessons/sintaxis/terminal-basico.js'
5
+ import { GIT_BASICO } from '../data/lessons/sintaxis/git-basico.js'
3
6
  import { clearConsole } from '../utils/output.js'
7
+ import { createPromptInterface } from '../utils/input.js'
8
+ import { UserState } from '../utils/user-state.js'
9
+ import { UNLOCK_CONFIG } from '../data/progression.js'
4
10
 
5
- export async function executeSintaxis() {
6
- // Show selection confirmation
7
- console.log('\nElegiste: Aprender sintaxis')
11
+ const LESSONS_BY_ID = new Map([
12
+ ['terminal-basico', TERMINAL_BASICO],
13
+ ['git-basico', GIT_BASICO]
14
+ ])
15
+
16
+ function waitForEnter(message = '\nPresiona Enter para continuar...') {
17
+ const rl = createPromptInterface()
18
+ return new Promise((resolve) => {
19
+ rl.question(message, () => {
20
+ rl.close()
21
+ resolve()
22
+ })
23
+ })
24
+ }
25
+
26
+ async function runLesson(lessonId, lessonTitle) {
27
+ const lessonData = LESSONS_BY_ID.get(lessonId)
28
+
29
+ if (!lessonData) {
30
+ console.log('\n🚀 Próximamente: ' + lessonTitle)
31
+ return
32
+ }
33
+
34
+ console.log(`\nElegiste: ${lessonTitle}`)
8
35
  console.log('Cargando...')
9
36
 
10
37
  await new Promise(resolve => setTimeout(resolve, 1000))
11
38
 
12
- // Clear screen but keep scroll history
13
39
  clearConsole()
14
40
 
15
- // Start lesson
16
- const lesson = new LessonBuilder(TERMINAL_BASICO)
41
+ const lesson = new LessonBuilder(lessonData)
17
42
  await lesson.start()
18
43
  }
44
+
45
+ export async function executeSintaxis() {
46
+ const state = UserState.get()
47
+
48
+ // Check if we should go directly to a lesson
49
+ const directLesson = UNLOCK_CONFIG.sintaxis.directToLesson(state)
50
+
51
+ if (directLesson) {
52
+ await runLesson(directLesson, 'Terminal básico')
53
+
54
+ // After completing, show what was unlocked
55
+ console.log('\n✨ Ahora puedes acceder al menú de sintaxis para aprender Git básico')
56
+ await waitForEnter()
57
+ return
58
+ }
59
+
60
+ // Show menu
61
+ const menu = new MenuBuilder(SINTAXIS_MENU_CONFIG)
62
+
63
+ while (true) {
64
+ const choice = await menu.show()
65
+ if (!choice) {
66
+ return
67
+ }
68
+
69
+ await runLesson(choice.id, choice.title)
70
+ await waitForEnter('\nPresiona Enter para regresar al menú de sintaxis...')
71
+ }
72
+ }
@@ -1,22 +1,32 @@
1
1
  import chalk from 'chalk'
2
2
  import { UserState } from './user-state.js'
3
3
  import { askQuestion, createPromptInterface } from './input.js'
4
+ import { clearConsole } from './output.js'
4
5
 
5
6
  const BANNER_ART = `
6
7
  █████████████████████████
7
8
  █ █ █ █ █ █ █
8
9
  █ C █ R █ E █ T █ A █ █ █
9
10
  █ █ █ █ █ █ █
10
- █████████████████████████
11
+ █████████████████████████`
12
+
13
+ const FIRST_TIME_BANNER = `${BANNER_ART}
14
+
15
+ Bienvenido a Creta, el taller de software de icarus.mx`
11
16
 
12
- Bienvenido a la escuela de software de icarus.mx`
17
+ const WELCOME_BANNER = `${BANNER_ART}
18
+
19
+ Bienvenido a Creta, el taller de software de icarus.mx`
13
20
 
14
21
  export async function greetUser() {
22
+ // Clear screen completely on startup
23
+ clearConsole()
24
+
15
25
  const state = UserState.updateLastSeen()
16
26
 
17
27
  // First time user - ask for name
18
28
  if (!state.name) {
19
- console.log(BANNER_ART)
29
+ console.log(FIRST_TIME_BANNER)
20
30
 
21
31
  const rl = createPromptInterface()
22
32
 
@@ -25,20 +35,32 @@ export async function greetUser() {
25
35
 
26
36
  if (name && name.trim()) {
27
37
  UserState.setName(name.trim())
38
+
39
+ // Clear and show welcome banner with animation feel
40
+ await new Promise(resolve => setTimeout(resolve, 300))
41
+ clearConsole()
42
+ console.log(WELCOME_BANNER)
28
43
  console.log(chalk.cyan(`\nSalgamos de este laberinto, ${name.trim()} 🏛️`))
29
44
  }
30
45
 
31
46
  rl.close()
32
47
  console.log('')
48
+
49
+ // Small delay before showing menu
50
+ await new Promise(resolve => setTimeout(resolve, 800))
33
51
  return
34
52
  }
35
53
 
36
54
  // Returning user - show personalized greeting
55
+ console.log(WELCOME_BANNER)
56
+
37
57
  const lessonsCount = state.lessonsCompleted.length
38
58
  const greeting = lessonsCount > 0
39
59
  ? `¡Hola de vuelta, ${state.name}! 📊 ${lessonsCount} ${lessonsCount === 1 ? 'lección completada' : 'lecciones completadas'}`
40
60
  : `Salgamos de este laberinto, ${state.name} 🏛️`
41
61
 
42
- console.log(BANNER_ART)
43
- console.log(chalk.cyan(`${greeting}\n`))
62
+ console.log(chalk.cyan(`\n${greeting}\n`))
63
+
64
+ // Small delay before showing menu
65
+ await new Promise(resolve => setTimeout(resolve, 500))
44
66
  }
@@ -5,31 +5,16 @@ import os from 'os'
5
5
  const STATE_DIR = path.join(os.homedir(), '.creta')
6
6
  const STATE_FILE = path.join(STATE_DIR, 'user.json')
7
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
- }
8
+ function ensureStateDir() {
9
+ if (!fs.existsSync(STATE_DIR)) {
10
+ fs.mkdirSync(STATE_DIR, { recursive: true })
18
11
  }
12
+ }
19
13
 
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
- }
14
+ function loadState() {
15
+ ensureStateDir()
31
16
 
32
- static createDefault() {
17
+ if (!fs.existsSync(STATE_FILE)) {
33
18
  return {
34
19
  name: null,
35
20
  createdAt: new Date().toISOString(),
@@ -39,77 +24,94 @@ export class UserState {
39
24
  stats: {
40
25
  totalSessions: 0,
41
26
  lastSeen: null
42
- }
27
+ },
28
+ lastSeen: null
43
29
  }
44
30
  }
45
31
 
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
32
+ try {
33
+ const data = fs.readFileSync(STATE_FILE, 'utf8')
34
+ return JSON.parse(data)
35
+ } catch (error) {
36
+ console.error('Error loading user state:', error.message)
37
+ return {
38
+ name: null,
39
+ createdAt: new Date().toISOString(),
40
+ lessonsCompleted: [],
41
+ projectsCreated: [],
42
+ discord: null,
43
+ stats: {
44
+ totalSessions: 0,
45
+ lastSeen: null
46
+ },
47
+ lastSeen: null
48
+ }
52
49
  }
50
+ }
53
51
 
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
- })
52
+ function saveState(state) {
53
+ ensureStateDir()
65
54
 
66
- this.save(state)
67
- return state
55
+ try {
56
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8')
57
+ } catch (error) {
58
+ console.error('Error saving user state:', error.message)
68
59
  }
60
+ }
69
61
 
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
62
+ export class UserState {
63
+ static get() {
64
+ return loadState()
82
65
  }
83
66
 
84
67
  static setName(name) {
85
- const state = this.load()
68
+ const state = loadState()
86
69
  state.name = name
87
- this.save(state)
70
+ saveState(state)
88
71
  return state
89
72
  }
90
73
 
91
- static getPath() {
92
- return STATE_FILE
74
+ static updateLastSeen() {
75
+ const state = loadState()
76
+ const now = new Date().toISOString()
77
+ state.lastSeen = now
78
+ state.stats = state.stats || { totalSessions: 0, lastSeen: null }
79
+ state.stats.totalSessions = (state.stats.totalSessions || 0) + 1
80
+ state.stats.lastSeen = now
81
+ saveState(state)
82
+ return state
93
83
  }
94
84
 
95
- static hasName() {
96
- const state = this.load()
97
- return state.name !== null && state.name.trim() !== ''
85
+ static addCompletedLesson(lessonId) {
86
+ const state = loadState()
87
+ const alreadyCompleted = state.lessonsCompleted.some(
88
+ lesson => typeof lesson === 'object' ? lesson.id === lessonId : lesson === lessonId
89
+ )
90
+
91
+ if (!alreadyCompleted) {
92
+ state.lessonsCompleted.push({
93
+ id: lessonId,
94
+ completedAt: new Date().toISOString()
95
+ })
96
+ saveState(state)
97
+ }
98
+ return state
98
99
  }
99
100
 
100
- static getName() {
101
- const state = this.load()
102
- return state.name
101
+ static addProject(projectName) {
102
+ const state = loadState()
103
+ state.projectsCreated = state.projectsCreated || []
104
+ state.projectsCreated.push({
105
+ name: projectName,
106
+ createdAt: new Date().toISOString()
107
+ })
108
+ saveState(state)
109
+ return state
103
110
  }
104
111
 
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
112
+ static reset() {
113
+ if (fs.existsSync(STATE_FILE)) {
114
+ fs.unlinkSync(STATE_FILE)
113
115
  }
114
116
  }
115
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icarusmx/creta",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Salgamos de este laberinto.",
5
5
  "type": "module",
6
6
  "bin": {