@icarusmx/creta 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/creta.js +8 -1576
  2. package/codex-refactor.txt +13 -0
  3. package/lib/builders/LessonBuilder.js +388 -0
  4. package/lib/builders/MenuBuilder.js +154 -0
  5. package/lib/builders/ProjectBuilder.js +56 -0
  6. package/lib/cli/index.js +85 -0
  7. package/lib/commands/help.js +5 -0
  8. package/lib/constants/paths.js +9 -0
  9. package/lib/data/enunciados.js +44 -0
  10. package/lib/data/lessons/index.js +25 -0
  11. package/lib/data/lessons/lesson1-system-decomposition.js +251 -0
  12. package/lib/data/lessons/lesson2-object-requests.js +323 -0
  13. package/lib/data/lessons/lesson3-only-way.js +354 -0
  14. package/lib/data/lessons/lesson4-operation-signatures.js +337 -0
  15. package/lib/data/lessons/lesson5-interface-set.js +346 -0
  16. package/lib/data/lessons/lesson6-interface-design.js +412 -0
  17. package/lib/data/lessons/lesson7-object-definition.js +380 -0
  18. package/lib/data/lessons/sintaxis/terminal-basico.js +50 -0
  19. package/lib/data/menus.js +43 -0
  20. package/lib/data/messages.js +28 -0
  21. package/lib/executors/enunciados-executor.js +73 -0
  22. package/lib/executors/portfolio-executor.js +167 -0
  23. package/lib/executors/proyectos-executor.js +23 -0
  24. package/lib/executors/sintaxis-executor.js +18 -0
  25. package/lib/templates/LevelModifier.js +287 -0
  26. package/lib/utils/file-utils.js +18 -0
  27. package/lib/utils/greeting.js +32 -0
  28. package/lib/utils/input.js +15 -0
  29. package/lib/utils/output.js +4 -0
  30. package/lib/utils/user-state.js +115 -0
  31. package/package.json +4 -1
  32. package/refactor.txt +581 -0
  33. package/test/enunciados.test.js +72 -0
  34. package/lessons/lesson1-system-decomposition.js +0 -313
  35. package/lessons/lesson2-object-requests.js +0 -309
  36. package/lessons/lesson3-only-way.js +0 -324
  37. package/lessons/lesson4-operation-signatures.js +0 -319
  38. package/lessons/lesson5-interface-set.js +0 -326
  39. package/lessons/lesson6-interface-design.js +0 -391
  40. package/lessons/lesson7-object-definition.js +0 -300
@@ -0,0 +1,13 @@
1
+ # creta cli refactor - progress log
2
+
3
+ ## 2025-02-14
4
+ - restored `bin/creta.js` to the original working version so the CLI behaved correctly again.
5
+ - extracted reusable data for the welcome banner, help text, and enunciados into `lib/data/messages.js` and `lib/data/enunciados.js`.
6
+ - added initial utilities (`lib/utils/input.js`, `lib/utils/output.js`, `lib/utils/file-utils.js`) plus `lib/templates/LevelModifier.js` to reuse template logic.
7
+
8
+ ## 2025-02-15
9
+ - introduced Discord-style builders (`lib/builders/MenuBuilder.js`, `lib/builders/ProjectBuilder.js`).
10
+ - defined menu configurations in `lib/data/menus.js` and template paths in `lib/constants/paths.js`.
11
+ - created executors for enunciados, sintaxis, proyectos, and portafolio to encapsulate orchestration logic.
12
+ - added `lib/cli/index.js` as the central command router and reduced `bin/creta.js` to a minimal entry point (<20 lines).
13
+ - verified primary commands (`help`, `portafolio-*`, invalid command handling) after restructuring.
@@ -0,0 +1,388 @@
1
+ import { createPromptInterface, askQuestion } from '../utils/input.js'
2
+ import { clearConsole } from '../utils/output.js'
3
+ import { UserState } from '../utils/user-state.js'
4
+
5
+ const DEFAULT_WAIT_MESSAGE = '\nPresiona Enter para continuar...'
6
+
7
+ export class LessonBuilder {
8
+ constructor(lessonData) {
9
+ this.lesson = lessonData
10
+ this.rl = null
11
+ }
12
+
13
+ async start() {
14
+ if (!this.lesson) {
15
+ throw new Error('Se requiere un objeto de lección para ejecutar el LessonBuilder')
16
+ }
17
+
18
+ this.rl = createPromptInterface()
19
+
20
+ try {
21
+ const flow = this.collectActions()
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
+ } finally {
29
+ this.rl.close()
30
+ this.rl = null
31
+ }
32
+ }
33
+
34
+ collectActions() {
35
+ const actions = []
36
+
37
+ const parts = [
38
+ this.lesson.title && { type: 'text', text: this.lesson.title },
39
+ this.lesson.subtitle && { type: 'text', text: this.lesson.subtitle },
40
+ this.lesson.intro && { type: 'intro', ...this.lesson.intro },
41
+ this.lesson.flow,
42
+ this.lesson.actions,
43
+ this.lesson.sections,
44
+ this.lesson.steps,
45
+ this.lesson.body,
46
+ this.lesson.conclusion
47
+ ]
48
+
49
+ for (const part of parts) {
50
+ if (!part) continue
51
+
52
+ if (Array.isArray(part)) {
53
+ actions.push(...part)
54
+ } else if (Array.isArray(part.actions)) {
55
+ actions.push(...part.actions)
56
+ } else {
57
+ actions.push(part)
58
+ }
59
+ }
60
+
61
+ return actions
62
+ }
63
+
64
+ async executeActions(actions = []) {
65
+ for (const action of actions) {
66
+ if (!action) continue
67
+ await this.executeAction(action)
68
+ }
69
+ }
70
+
71
+ async executeAction(action) {
72
+ const type = action.type || 'text'
73
+
74
+ switch (type) {
75
+ case 'clear':
76
+ this.handleClear(action)
77
+ break
78
+ case 'intro':
79
+ await this.handleIntro(action)
80
+ break
81
+ case 'pause':
82
+ case 'wait':
83
+ await this.handlePause(action)
84
+ break
85
+ case 'code':
86
+ this.handleCode(action)
87
+ break
88
+ case 'sleep':
89
+ await this.handleSleep(action)
90
+ break
91
+ case 'command-intro':
92
+ await this.handleCommandIntro(action)
93
+ break
94
+ case 'maze-completion':
95
+ await this.handleMazeCompletion(action)
96
+ break
97
+ case 'text':
98
+ default:
99
+ this.handleText(action)
100
+ break
101
+ }
102
+ }
103
+
104
+ handleClear(action) {
105
+ if (action.message) {
106
+ console.log(action.message)
107
+ }
108
+
109
+ if (typeof action.useConsoleClear === 'boolean' && action.useConsoleClear) {
110
+ console.clear()
111
+ return
112
+ }
113
+
114
+ clearConsole()
115
+ }
116
+
117
+ handleText(action) {
118
+ const lines = this.normalizeLines(action)
119
+ lines.forEach(line => console.log(line))
120
+ if (action.extraNewLine) {
121
+ console.log('')
122
+ }
123
+ }
124
+
125
+ handleCode(action) {
126
+ if (action.title) {
127
+ console.log(`\n${action.title}`)
128
+ console.log('━'.repeat(action.width || 50))
129
+ }
130
+
131
+ const lines = typeof action.code === 'string' ? action.code.split('\n') : []
132
+ lines.forEach(line => console.log(this.formatCode(line)))
133
+
134
+ if (action.title) {
135
+ console.log('━'.repeat(action.width || 50))
136
+ }
137
+
138
+ if (action.after) {
139
+ const afterLines = Array.isArray(action.after) ? action.after : [action.after]
140
+ afterLines.forEach(line => console.log(line))
141
+ }
142
+ }
143
+
144
+ async handlePause(action) {
145
+ const message = action.message || DEFAULT_WAIT_MESSAGE
146
+ await askQuestion(this.rl, message)
147
+ if (action.appendNewLines !== false) {
148
+ console.log('\n')
149
+ }
150
+ }
151
+
152
+ async handleSleep(action) {
153
+ const duration = typeof action.duration === 'number' ? action.duration : 800
154
+ await new Promise(resolve => setTimeout(resolve, duration))
155
+ }
156
+
157
+ normalizeLines(action) {
158
+ if (Array.isArray(action.lines)) {
159
+ return action.lines.map(item => (item === null || item === undefined ? '' : String(item)))
160
+ }
161
+
162
+ if (typeof action.lines === 'string') {
163
+ return action.lines.split('\n')
164
+ }
165
+
166
+ if (Array.isArray(action.text)) {
167
+ return action.text.map(item => (item === null || item === undefined ? '' : String(item)))
168
+ }
169
+
170
+ if (typeof action.text === 'string') {
171
+ return action.text.split('\n')
172
+ }
173
+
174
+ if (typeof action.text !== 'undefined') {
175
+ return [String(action.text)]
176
+ }
177
+
178
+ return []
179
+ }
180
+
181
+ formatCode(line = '') {
182
+ const colors = {
183
+ keyword: '\u001b[35m',
184
+ property: '\u001b[32m',
185
+ string: '\u001b[33m',
186
+ comment: '\u001b[90m',
187
+ reset: '\u001b[0m'
188
+ }
189
+
190
+ return line
191
+ .replace(/\b(class|constructor|return|if|const|let|new|async|await|throw|try|catch)\b/g, `${colors.keyword}$1${colors.reset}`)
192
+ .replace(/\bthis\./g, `${colors.property}this.${colors.reset}`)
193
+ .replace(/'([^']*)'/g, (_, inner) => `${colors.string}'${inner}'${colors.reset}`)
194
+ .replace(/"([^\"]*)"/g, (_, inner) => `${colors.string}"${inner}"${colors.reset}`)
195
+ .replace(/`([^`]*)`/g, (_, inner) => `${colors.string}\`${inner}\`${colors.reset}`)
196
+ .replace(/\/\/.*$/g, match => `${colors.comment}${match}${colors.reset}`)
197
+ }
198
+
199
+ async handleIntro(action) {
200
+ // Show definition first
201
+ if (action.definition) {
202
+ console.log(action.definition)
203
+ }
204
+
205
+ // Wait for Enter
206
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
207
+
208
+ // Clear screen and show explanation
209
+ if (action.explanation) {
210
+ clearConsole()
211
+ console.log(action.explanation)
212
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
213
+ }
214
+
215
+ // Clear screen and show detail
216
+ if (action.detail) {
217
+ clearConsole()
218
+ console.log(action.detail)
219
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
220
+ }
221
+ }
222
+
223
+ async handleCommandIntro(action) {
224
+ // Clear screen before showing command
225
+ clearConsole()
226
+
227
+ console.log(action.description)
228
+ console.log('')
229
+ console.log(action.explanation)
230
+
231
+ if (action.example) {
232
+ console.log('')
233
+ console.log(`Ejemplo: ${action.example}`)
234
+ }
235
+
236
+ console.log('')
237
+ console.log(action.instruction)
238
+
239
+ if (!action.exitOnComplete) {
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}`)
383
+ }
384
+
385
+ console.log('')
386
+ await askQuestion(this.rl, '\nPresiona Enter para continuar...')
387
+ }
388
+ }
@@ -0,0 +1,154 @@
1
+ import { createPromptInterface, askQuestion } from '../utils/input.js'
2
+ import { clearConsole } from '../utils/output.js'
3
+
4
+ export class MenuBuilder {
5
+ constructor(config) {
6
+ this.config = config
7
+ }
8
+
9
+ async show() {
10
+ if (this.config?.options?.length === 0) {
11
+ return null
12
+ }
13
+
14
+ if (!process.stdin.isTTY) {
15
+ return this.showFallback()
16
+ }
17
+
18
+ try {
19
+ return await this.showInteractive()
20
+ } catch (error) {
21
+ console.log('\nModo interactivo no disponible, usando selección numérica...\n')
22
+ return await this.showFallback()
23
+ }
24
+ }
25
+
26
+ renderHeader() {
27
+ clearConsole()
28
+
29
+ if (this.config.banner) {
30
+ console.log(this.config.banner)
31
+ }
32
+
33
+ if (this.config.description) {
34
+ const lines = Array.isArray(this.config.description)
35
+ ? this.config.description
36
+ : [this.config.description]
37
+ lines.forEach(line => console.log(line))
38
+ console.log('')
39
+ }
40
+
41
+ if (this.config.title) {
42
+ console.log(this.config.title)
43
+ }
44
+ }
45
+
46
+ renderOption(option, isSelected, index) {
47
+ const prefix = isSelected ? '▶ ' : ' '
48
+ const label = option.title || option.label || `Opción ${index + 1}`
49
+
50
+ if (isSelected) {
51
+ console.log(`\x1b[36m${prefix}${label}\x1b[0m`)
52
+ } else {
53
+ console.log(`${prefix}${label}`)
54
+ }
55
+
56
+ if (option.description) {
57
+ console.log(` ${option.description}`)
58
+ }
59
+ console.log('')
60
+ }
61
+
62
+ async showInteractive() {
63
+ if (typeof process.stdin.setRawMode !== 'function') {
64
+ throw new Error('setRawMode not available')
65
+ }
66
+
67
+ let selectedIndex = 0
68
+
69
+ const render = () => {
70
+ this.renderHeader()
71
+ this.config.options.forEach((option, index) => {
72
+ this.renderOption(option, index === selectedIndex, index)
73
+ })
74
+ }
75
+
76
+ return new Promise((resolve) => {
77
+ process.stdin.setRawMode(true)
78
+ process.stdin.resume()
79
+ process.stdin.setEncoding('utf8')
80
+
81
+ const onKeyPress = (key) => {
82
+ if (key === 'q' || key === '\u0003') {
83
+ cleanup()
84
+ resolve(null)
85
+ return
86
+ }
87
+
88
+ if (key === '\r' || key === '\n') {
89
+ const choice = this.config.options[selectedIndex]
90
+ cleanup()
91
+ resolve(choice)
92
+ return
93
+ }
94
+
95
+ if (key === '\u001b[A') {
96
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : this.config.options.length - 1
97
+ render()
98
+ } else if (key === '\u001b[B') {
99
+ selectedIndex = selectedIndex < this.config.options.length - 1 ? selectedIndex + 1 : 0
100
+ render()
101
+ }
102
+ }
103
+
104
+ const cleanup = () => {
105
+ process.stdin.setRawMode(false)
106
+ process.stdin.removeListener('data', onKeyPress)
107
+ }
108
+
109
+ process.stdin.on('data', onKeyPress)
110
+ render()
111
+ })
112
+ }
113
+
114
+ async showFallback() {
115
+ const rl = createPromptInterface()
116
+
117
+ if (this.config.banner) {
118
+ console.log(this.config.banner)
119
+ }
120
+
121
+ if (this.config.description) {
122
+ const lines = Array.isArray(this.config.description)
123
+ ? this.config.description
124
+ : [this.config.description]
125
+ lines.forEach(line => console.log(line))
126
+ console.log('')
127
+ }
128
+
129
+ if (this.config.title) {
130
+ console.log(this.config.title)
131
+ }
132
+
133
+ this.config.options.forEach((option, index) => {
134
+ const label = option.title || option.label || `Opción ${index + 1}`
135
+ console.log(`${index + 1}. ${label}`)
136
+ if (option.description) {
137
+ console.log(` ${option.description}`)
138
+ }
139
+ })
140
+
141
+ console.log('')
142
+ const answer = await askQuestion(rl, `Elige una opción (1-${this.config.options.length}) o 'q' para salir: `)
143
+
144
+ if (answer.toLowerCase() === 'q') {
145
+ rl.close()
146
+ return null
147
+ }
148
+
149
+ const selectedIndex = parseInt(answer, 10) - 1
150
+ const choice = this.config.options[selectedIndex]
151
+ rl.close()
152
+ return choice || null
153
+ }
154
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { isBinaryFile } from '../utils/file-utils.js'
4
+ import { LevelModifier } from '../templates/LevelModifier.js'
5
+
6
+ export class ProjectBuilder {
7
+ constructor(templatePath) {
8
+ this.templatePath = templatePath
9
+ }
10
+
11
+ createProject(targetDir, projectName, studentName, level) {
12
+ if (!fs.existsSync(this.templatePath)) {
13
+ throw new Error(`Template no encontrado en: ${this.templatePath}`)
14
+ }
15
+
16
+ if (fs.existsSync(targetDir)) {
17
+ throw new Error(`El directorio ${projectName} ya existe`)
18
+ }
19
+
20
+ fs.mkdirSync(targetDir, { recursive: true })
21
+ const modifier = new LevelModifier(level)
22
+ this.copyDirectory(this.templatePath, targetDir, projectName, studentName, modifier)
23
+ }
24
+
25
+ applyPlaceholders(content, projectName, studentName) {
26
+ return content
27
+ .replace(/\{\{PROJECT_NAME\}\}/g, projectName)
28
+ .replace(/\{\{STUDENT_NAME\}\}/g, studentName)
29
+ }
30
+
31
+ copyDirectory(source, target, projectName, studentName, modifier) {
32
+ const entries = fs.readdirSync(source)
33
+
34
+ for (const entry of entries) {
35
+ const sourcePath = path.join(source, entry)
36
+ const targetPath = path.join(target, entry)
37
+ const stats = fs.statSync(sourcePath)
38
+
39
+ if (stats.isDirectory()) {
40
+ fs.mkdirSync(targetPath, { recursive: true })
41
+ this.copyDirectory(sourcePath, targetPath, projectName, studentName, modifier)
42
+ continue
43
+ }
44
+
45
+ if (isBinaryFile(sourcePath)) {
46
+ fs.copyFileSync(sourcePath, targetPath)
47
+ continue
48
+ }
49
+
50
+ let content = fs.readFileSync(sourcePath, 'utf8')
51
+ content = modifier.apply(content, entry)
52
+ content = this.applyPlaceholders(content, projectName, studentName)
53
+ fs.writeFileSync(targetPath, content)
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,85 @@
1
+ import { MenuBuilder } from '../builders/MenuBuilder.js'
2
+ import { MAIN_MENU_CONFIG } from '../data/menus.js'
3
+ import { executeEnunciados } from '../executors/enunciados-executor.js'
4
+ import { executeProyectos } from '../executors/proyectos-executor.js'
5
+ import { executeSintaxis } from '../executors/sintaxis-executor.js'
6
+ import { executePortfolio } from '../executors/portfolio-executor.js'
7
+ import { showHelp } from '../commands/help.js'
8
+ import { CretaCodeSession } from '../session.js'
9
+ import { greetUser } from '../utils/greeting.js'
10
+
11
+ async function executeMainMenu() {
12
+ // Greet user (ask name if first time, or show stats if returning)
13
+ await greetUser()
14
+
15
+ const menu = new MenuBuilder(MAIN_MENU_CONFIG)
16
+
17
+ while (true) {
18
+ const choice = await menu.show()
19
+ if (!choice) {
20
+ console.log('\nHecho con <3 por icarus.mx')
21
+ return
22
+ }
23
+
24
+ if (choice.id === 'sintaxis') {
25
+ await executeSintaxis()
26
+ } else if (choice.id === 'enunciados') {
27
+ await executeEnunciados()
28
+ } else if (choice.id === 'proyectos') {
29
+ await executeProyectos()
30
+ }
31
+ }
32
+ }
33
+
34
+ async function runCodeSession() {
35
+ const session = new CretaCodeSession()
36
+ await session.start()
37
+ }
38
+
39
+ const COMMANDS = new Map([
40
+ ['enunciados', () => executeEnunciados()],
41
+ ['sintaxis', () => executeSintaxis()],
42
+ ['proyectos', () => executeProyectos()],
43
+ ['portafolio', () => executePortfolio(0)],
44
+ ['code', () => runCodeSession()],
45
+ ['help', () => showHelp()],
46
+ ['ayuda', () => showHelp()]
47
+ ])
48
+
49
+ export async function handleCommand(command, args = []) {
50
+ if (!command) {
51
+ return true
52
+ }
53
+
54
+ const normalized = command.toLowerCase()
55
+
56
+ if (normalized.startsWith('portafolio')) {
57
+ const levelPart = normalized.split('-')[1]
58
+ const level = levelPart ? parseInt(levelPart, 10) : 0
59
+ await executePortfolio(Number.isNaN(level) ? 0 : level)
60
+ return true
61
+ }
62
+
63
+ const handler = COMMANDS.get(normalized)
64
+
65
+ if (!handler) {
66
+ console.log(`Comando no reconocido: ${command}`)
67
+ console.log("Escribe 'creta help' para ver comandos disponibles")
68
+ return false
69
+ }
70
+
71
+ await handler(args)
72
+ return true
73
+ }
74
+
75
+ export async function runCLI(args = []) {
76
+ const [command, ...rest] = args
77
+
78
+ if (!command) {
79
+ await executeMainMenu()
80
+ return 0
81
+ }
82
+
83
+ const handled = await handleCommand(command, rest)
84
+ return handled ? 0 : 1
85
+ }
@@ -0,0 +1,5 @@
1
+ import { HELP_TEXT } from '../data/messages.js'
2
+
3
+ export function showHelp() {
4
+ console.log(HELP_TEXT)
5
+ }