@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.
- package/bin/creta.js +8 -1576
- package/codex-refactor.txt +13 -0
- package/lib/builders/LessonBuilder.js +388 -0
- package/lib/builders/MenuBuilder.js +154 -0
- package/lib/builders/ProjectBuilder.js +56 -0
- package/lib/cli/index.js +85 -0
- package/lib/commands/help.js +5 -0
- package/lib/constants/paths.js +9 -0
- package/lib/data/enunciados.js +44 -0
- package/lib/data/lessons/index.js +25 -0
- package/lib/data/lessons/lesson1-system-decomposition.js +251 -0
- package/lib/data/lessons/lesson2-object-requests.js +323 -0
- package/lib/data/lessons/lesson3-only-way.js +354 -0
- package/lib/data/lessons/lesson4-operation-signatures.js +337 -0
- package/lib/data/lessons/lesson5-interface-set.js +346 -0
- package/lib/data/lessons/lesson6-interface-design.js +412 -0
- package/lib/data/lessons/lesson7-object-definition.js +380 -0
- package/lib/data/lessons/sintaxis/terminal-basico.js +50 -0
- package/lib/data/menus.js +43 -0
- package/lib/data/messages.js +28 -0
- package/lib/executors/enunciados-executor.js +73 -0
- package/lib/executors/portfolio-executor.js +167 -0
- package/lib/executors/proyectos-executor.js +23 -0
- package/lib/executors/sintaxis-executor.js +18 -0
- package/lib/templates/LevelModifier.js +287 -0
- package/lib/utils/file-utils.js +18 -0
- package/lib/utils/greeting.js +32 -0
- package/lib/utils/input.js +15 -0
- package/lib/utils/output.js +4 -0
- package/lib/utils/user-state.js +115 -0
- package/package.json +4 -1
- package/refactor.txt +581 -0
- package/test/enunciados.test.js +72 -0
- package/lessons/lesson1-system-decomposition.js +0 -313
- package/lessons/lesson2-object-requests.js +0 -309
- package/lessons/lesson3-only-way.js +0 -324
- package/lessons/lesson4-operation-signatures.js +0 -319
- package/lessons/lesson5-interface-set.js +0 -326
- package/lessons/lesson6-interface-design.js +0 -391
- 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
|
+
}
|
package/lib/cli/index.js
ADDED
|
@@ -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
|
+
}
|