@icarusmx/creta 1.4.2 → 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.
- package/lib/builders/LessonBuilder.js +5 -5
- package/lib/builders/MenuBuilder.js +30 -5
- package/lib/cli/index.js +13 -4
- package/lib/data/lessons/lesson1-system-decomposition.js +115 -54
- package/lib/data/lessons/lesson2-object-requests.js +0 -5
- package/lib/data/lessons/lesson3-only-way.js +0 -5
- package/lib/data/lessons/lesson4-operation-signatures.js +0 -5
- package/lib/data/lessons/lesson5-interface-set.js +0 -5
- package/lib/data/lessons/lesson6-interface-design.js +0 -5
- package/lib/data/lessons/lesson7-object-definition.js +0 -5
- package/lib/data/lessons/sintaxis/git-basico.js +58 -0
- package/lib/data/menus.js +47 -7
- package/lib/data/messages.js +3 -0
- package/lib/data/progression.js +44 -0
- package/lib/executors/enunciados-executor.js +0 -10
- package/lib/executors/sintaxis-executor.js +60 -6
- package/lib/utils/greeting.js +27 -5
- package/lib/utils/user-state.js +74 -72
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
//
|
|
76
|
-
|
|
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: '
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
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: '📄
|
|
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: '
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
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: '📄
|
|
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: '
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
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: '📄
|
|
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
|
-
|
|
161
|
-
'
|
|
162
|
-
'
|
|
163
|
-
'
|
|
164
|
-
'
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
'
|
|
179
|
-
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
248
|
+
'Ejemplo de uso:',
|
|
180
249
|
'',
|
|
181
|
-
'
|
|
182
|
-
|
|
183
|
-
'
|
|
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
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
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
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'
|
|
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
|
-
'
|
|
194
|
-
'
|
|
195
|
-
'
|
|
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 -
|
|
228
|
-
'• User -
|
|
229
|
-
'• Loan -
|
|
230
|
-
'• Library -
|
|
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,16 +1,56 @@
|
|
|
1
|
-
import { WELCOME_BANNER } from './messages.js'
|
|
2
1
|
import { ENUNCIADOS } from './enunciados.js'
|
|
2
|
+
import { UNLOCK_CONFIG, isUnlocked, hasCompletedLesson } from './progression.js'
|
|
3
|
+
import { UserState } from '../utils/user-state.js'
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
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.',
|
|
7
8
|
options: [
|
|
8
|
-
{ id: '
|
|
9
|
-
{ id: '
|
|
10
|
-
{ 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' }
|
|
11
11
|
]
|
|
12
12
|
}
|
|
13
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
|
+
|
|
14
54
|
export const ENUNCIADOS_MENU_CONFIG = {
|
|
15
55
|
title: 'Elige qué enunciado te gustaría explorar:',
|
|
16
56
|
description: [
|
package/lib/data/messages.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/utils/greeting.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
43
|
-
|
|
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
|
}
|
package/lib/utils/user-state.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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 =
|
|
68
|
+
const state = loadState()
|
|
86
69
|
state.name = name
|
|
87
|
-
|
|
70
|
+
saveState(state)
|
|
88
71
|
return state
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
static
|
|
92
|
-
|
|
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
|
|
96
|
-
const state =
|
|
97
|
-
|
|
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
|
|
101
|
-
const state =
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
}
|