@icarusmx/creta 1.4.9 → 1.4.11
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/data/lessons/sintaxis/piping-redireccion.js +120 -5
- package/lib/data/menus.js +42 -13
- package/lib/data/progression.js +8 -8
- package/lib/executors/sintaxis-executor.js +19 -7
- package/package.json +1 -1
- package/test/sintaxis-menu.test.js +45 -0
- package/.claude/settings.local.json +0 -16
- package/deploy-patch.sh +0 -30
|
@@ -9,11 +9,89 @@ export const PIPING_REDIRECCION = {
|
|
|
9
9
|
},
|
|
10
10
|
|
|
11
11
|
steps: [
|
|
12
|
+
{
|
|
13
|
+
type: 'text',
|
|
14
|
+
content: 'Primero, entendamos los 3 canales de comunicación que tiene cada programa.'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'pause'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
content: 'Cada comando que ejecutas tiene 3 "streams" (flujos) estándar:'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
content: '• stdin (standard input) - Canal 0: Por donde entra información'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
content: '• stdout (standard output) - Canal 1: Por donde sale información normal'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
content: '• stderr (standard error) - Canal 2: Por donde salen mensajes de error'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'pause'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
content: 'Por defecto:'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
content: '• stdin lee del teclado'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
content: '• stdout y stderr escriben a la pantalla'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'pause'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
content: 'Pero con piping y redirección puedes cambiar estos destinos.'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'pause'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'code',
|
|
62
|
+
title: 'Los 3 streams en acción',
|
|
63
|
+
code: `# ls produce salida normal (stdout)
|
|
64
|
+
ls archivo.txt
|
|
65
|
+
# → archivo.txt (aparece en pantalla)
|
|
66
|
+
|
|
67
|
+
# ls con archivo inexistente produce error (stderr)
|
|
68
|
+
ls archivo-que-no-existe
|
|
69
|
+
# → ls: archivo-que-no-existe: No such file or directory (aparece en pantalla)
|
|
70
|
+
|
|
71
|
+
# read espera entrada (stdin)
|
|
72
|
+
read NOMBRE
|
|
73
|
+
# → [cursor esperando que escribas]`,
|
|
74
|
+
after: [
|
|
75
|
+
'',
|
|
76
|
+
'Aunque ambos aparecen en pantalla, stdout y stderr son canales separados.',
|
|
77
|
+
'Esto te permite tratarlos diferente con redirección.'
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'pause'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'text',
|
|
85
|
+
content: 'Ahora veamos cómo manipular estos streams.'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: 'pause'
|
|
89
|
+
},
|
|
12
90
|
{
|
|
13
91
|
type: 'command-intro',
|
|
14
92
|
command: '|',
|
|
15
93
|
description: 'El pipe (tubería)',
|
|
16
|
-
explanation: 'El símbolo |
|
|
94
|
+
explanation: 'El símbolo | conecta el stdout de un comando al stdin de otro.',
|
|
17
95
|
example: 'ls | grep ".js"\ncat archivo.txt | wc -l',
|
|
18
96
|
instruction: 'Úsalo para encadenar comandos. La salida del izquierdo entra al derecho.'
|
|
19
97
|
},
|
|
@@ -68,19 +146,56 @@ export const PIPING_REDIRECCION = {
|
|
|
68
146
|
{
|
|
69
147
|
type: 'command-intro',
|
|
70
148
|
command: '2>',
|
|
71
|
-
description: 'Redirección de errores',
|
|
72
|
-
explanation: '2
|
|
149
|
+
description: 'Redirección de errores (stderr)',
|
|
150
|
+
explanation: 'El 2 se refiere al file descriptor de stderr. 2> redirige solo los errores a un archivo.',
|
|
73
151
|
example: 'npm install 2> errores.log\nls archivo-que-no-existe 2> /dev/null',
|
|
74
152
|
instruction: '/dev/null es como un "agujero negro" - descarta todo lo que le envíes.'
|
|
75
153
|
},
|
|
154
|
+
{
|
|
155
|
+
type: 'text',
|
|
156
|
+
content: 'También puedes redirigir stdout explícitamente con 1> (aunque > solo es equivalente).'
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'pause'
|
|
160
|
+
},
|
|
76
161
|
{
|
|
77
162
|
type: 'command-intro',
|
|
78
163
|
command: '&>',
|
|
79
|
-
description: 'Redirección de
|
|
80
|
-
explanation: '&> redirige
|
|
164
|
+
description: 'Redirección de stdout Y stderr',
|
|
165
|
+
explanation: '&> redirige ambos canales (1 y 2) al mismo destino. Es como hacer 1> archivo 2>&1',
|
|
81
166
|
example: 'npm install &> install-log.txt\ngit pull &> pull.log',
|
|
82
167
|
instruction: 'Útil cuando quieres capturar todo lo que un comando produce.'
|
|
83
168
|
},
|
|
169
|
+
{
|
|
170
|
+
type: 'pause'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'code',
|
|
174
|
+
title: 'Standard streams en acción',
|
|
175
|
+
code: `# Separar stdout y stderr en diferentes archivos
|
|
176
|
+
ls archivo.txt archivo-inexistente 1> salidas.txt 2> errores.txt
|
|
177
|
+
# → archivo.txt va a salidas.txt
|
|
178
|
+
# → el error va a errores.txt
|
|
179
|
+
|
|
180
|
+
# Guardar salida, descartar errores
|
|
181
|
+
npm install 1> install.log 2> /dev/null
|
|
182
|
+
|
|
183
|
+
# Redirigir stderr a stdout, luego todo a un archivo
|
|
184
|
+
ls archivo-inexistente 2>&1 | grep "No such"
|
|
185
|
+
# → El error se convierte en stdout y puede ser procesado por grep
|
|
186
|
+
|
|
187
|
+
# Capturar todo en un solo archivo (equivalentes)
|
|
188
|
+
comando &> todo.log
|
|
189
|
+
comando > todo.log 2>&1`,
|
|
190
|
+
after: [
|
|
191
|
+
'',
|
|
192
|
+
'Entender los file descriptors (0, 1, 2) te da control total del flujo de datos.',
|
|
193
|
+
'Puedes separar, combinar, o descartar cualquier stream según necesites.'
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: 'pause'
|
|
198
|
+
},
|
|
84
199
|
{
|
|
85
200
|
type: 'code',
|
|
86
201
|
title: 'Ejemplo real: Procesando una versión',
|
package/lib/data/menus.js
CHANGED
|
@@ -2,15 +2,43 @@ import { ENUNCIADOS } from './enunciados.js'
|
|
|
2
2
|
import { UNLOCK_CONFIG, isUnlocked, hasCompletedLesson } from './progression.js'
|
|
3
3
|
import { UserState } from '../utils/user-state.js'
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
export const SINTAXIS_LESSONS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'terminal-basico',
|
|
8
|
+
title: '1. Terminal básico',
|
|
9
|
+
description: '',
|
|
10
|
+
unlockAfter: null
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'git-basico',
|
|
14
|
+
title: '2. Git básico',
|
|
15
|
+
description: '',
|
|
16
|
+
unlockAfter: 'terminal-basico'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'piping-redireccion',
|
|
20
|
+
title: '3. Piping y Redirección',
|
|
21
|
+
description: '',
|
|
22
|
+
unlockAfter: 'git-basico'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'bash-scripts',
|
|
26
|
+
title: '4. Bash Scripts',
|
|
27
|
+
description: '',
|
|
28
|
+
unlockAfter: 'piping-redireccion'
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export function getSintaxisMenuConfig(state = UserState.get()) {
|
|
33
|
+
const options = SINTAXIS_LESSONS
|
|
34
|
+
.filter((lesson) => !lesson.unlockAfter || hasCompletedLesson(lesson.unlockAfter, state))
|
|
35
|
+
.map(({ id, title, description }) => ({ id, title, description }))
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
title: 'Elige qué lección de sintaxis te gustaría explorar:',
|
|
39
|
+
description: 'Aprende los comandos fundamentales de terminal y git.',
|
|
40
|
+
options
|
|
41
|
+
}
|
|
14
42
|
}
|
|
15
43
|
|
|
16
44
|
const MENU_ITEMS = {
|
|
@@ -19,14 +47,13 @@ const MENU_ITEMS = {
|
|
|
19
47
|
proyectos: { baseTitle: 'Construir proyectos' }
|
|
20
48
|
}
|
|
21
49
|
|
|
22
|
-
export function getMainMenuConfig() {
|
|
23
|
-
const state = UserState.get()
|
|
50
|
+
export function getMainMenuConfig(state = UserState.get()) {
|
|
24
51
|
const options = []
|
|
25
52
|
|
|
26
53
|
for (const [id, itemConfig] of Object.entries(MENU_ITEMS)) {
|
|
27
54
|
const unlockConfig = UNLOCK_CONFIG.mainMenu[id]
|
|
28
55
|
|
|
29
|
-
if (!isUnlocked(id, unlockConfig)) continue
|
|
56
|
+
if (!isUnlocked(id, unlockConfig, state)) continue
|
|
30
57
|
|
|
31
58
|
const showNew = typeof unlockConfig.showNewIndicator === 'function'
|
|
32
59
|
? unlockConfig.showNewIndicator(state)
|
|
@@ -45,7 +72,9 @@ export function getMainMenuConfig() {
|
|
|
45
72
|
|
|
46
73
|
return {
|
|
47
74
|
clearConsole: false,
|
|
48
|
-
title:
|
|
75
|
+
title: options.length === 1
|
|
76
|
+
? 'Empecemos por aquí:'
|
|
77
|
+
: 'Te ofrecemos las siguientes opciones:',
|
|
49
78
|
options
|
|
50
79
|
}
|
|
51
80
|
}
|
package/lib/data/progression.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Centralized progression and unlock logic
|
|
2
2
|
import { UserState } from '../utils/user-state.js'
|
|
3
3
|
|
|
4
|
-
export function hasCompletedLesson(lessonId) {
|
|
5
|
-
const
|
|
6
|
-
return
|
|
4
|
+
export function hasCompletedLesson(lessonId, state = UserState.get()) {
|
|
5
|
+
const lessonsCompleted = state?.lessonsCompleted || []
|
|
6
|
+
return lessonsCompleted.some(
|
|
7
7
|
lesson => typeof lesson === 'object' ? lesson.id === lessonId : lesson === lessonId
|
|
8
8
|
)
|
|
9
9
|
}
|
|
@@ -14,9 +14,9 @@ export const UNLOCK_CONFIG = {
|
|
|
14
14
|
sintaxis: {
|
|
15
15
|
alwaysVisible: true,
|
|
16
16
|
showNewIndicator: (state) =>
|
|
17
|
-
hasCompletedLesson('terminal-basico') && !hasCompletedLesson('git-basico'),
|
|
17
|
+
hasCompletedLesson('terminal-basico', state) && !hasCompletedLesson('git-basico', state),
|
|
18
18
|
description: (state) =>
|
|
19
|
-
hasCompletedLesson('terminal-basico') && !hasCompletedLesson('git-basico')
|
|
19
|
+
hasCompletedLesson('terminal-basico', state) && !hasCompletedLesson('git-basico', state)
|
|
20
20
|
? 'Git básico desbloqueado'
|
|
21
21
|
: undefined
|
|
22
22
|
},
|
|
@@ -33,12 +33,12 @@ export const UNLOCK_CONFIG = {
|
|
|
33
33
|
},
|
|
34
34
|
sintaxis: {
|
|
35
35
|
directToLesson: (state) =>
|
|
36
|
-
!hasCompletedLesson('terminal-basico') ? 'terminal-basico' : null
|
|
36
|
+
!hasCompletedLesson('terminal-basico', state) ? 'terminal-basico' : null
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function isUnlocked(itemKey, config) {
|
|
40
|
+
export function isUnlocked(itemKey, config, state = UserState.get()) {
|
|
41
41
|
if (config.alwaysVisible) return true
|
|
42
42
|
if (!config.unlockAfter) return true
|
|
43
|
-
return hasCompletedLesson(config.unlockAfter)
|
|
43
|
+
return hasCompletedLesson(config.unlockAfter, state)
|
|
44
44
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MenuBuilder } from '../builders/MenuBuilder.js'
|
|
2
|
-
import {
|
|
2
|
+
import { getSintaxisMenuConfig, SINTAXIS_LESSONS } from '../data/menus.js'
|
|
3
3
|
import { LessonBuilder } from '../builders/LessonBuilder.js'
|
|
4
4
|
import { TERMINAL_BASICO } from '../data/lessons/sintaxis/terminal-basico.js'
|
|
5
5
|
import { GIT_BASICO } from '../data/lessons/sintaxis/git-basico.js'
|
|
@@ -53,18 +53,30 @@ export async function executeSintaxis() {
|
|
|
53
53
|
const directLesson = UNLOCK_CONFIG.sintaxis.directToLesson(state)
|
|
54
54
|
|
|
55
55
|
if (directLesson) {
|
|
56
|
-
|
|
56
|
+
const lessonMeta = SINTAXIS_LESSONS.find((lesson) => lesson.id === directLesson)
|
|
57
|
+
const lessonTitle = lessonMeta?.title || 'Lección de sintaxis'
|
|
58
|
+
|
|
59
|
+
await runLesson(directLesson, lessonTitle)
|
|
60
|
+
|
|
61
|
+
const nextLesson = SINTAXIS_LESSONS.find((lesson) => lesson.unlockAfter === directLesson)
|
|
62
|
+
if (nextLesson) {
|
|
63
|
+
console.log(`\n✨ ${nextLesson.title} desbloqueado.`)
|
|
64
|
+
console.log(' Vuelve al menú de sintaxis para explorarlo.')
|
|
65
|
+
}
|
|
57
66
|
|
|
58
|
-
// After completing, show what was unlocked
|
|
59
|
-
console.log('\n✨ Ahora puedes acceder al menú de sintaxis para aprender Git básico')
|
|
60
67
|
await waitForEnter()
|
|
61
68
|
return
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
// Show menu
|
|
65
|
-
const menu = new MenuBuilder(SINTAXIS_MENU_CONFIG)
|
|
66
|
-
|
|
67
71
|
while (true) {
|
|
72
|
+
const menuConfig = getSintaxisMenuConfig()
|
|
73
|
+
|
|
74
|
+
if (menuConfig.options.length === 0) {
|
|
75
|
+
console.log('\nPor ahora no hay lecciones disponibles en este camino.')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const menu = new MenuBuilder(menuConfig)
|
|
68
80
|
const choice = await menu.show()
|
|
69
81
|
if (!choice) {
|
|
70
82
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { getSintaxisMenuConfig, getMainMenuConfig } from '../lib/data/menus.js'
|
|
5
|
+
|
|
6
|
+
const baseState = {
|
|
7
|
+
lessonsCompleted: []
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('sintaxis menu shows only terminal básico for new users', () => {
|
|
11
|
+
const menu = getSintaxisMenuConfig(baseState)
|
|
12
|
+
assert.equal(menu.options.length, 1)
|
|
13
|
+
assert.equal(menu.options[0].id, 'terminal-basico')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('sintaxis menu unlocks git básico after completing terminal básico', () => {
|
|
17
|
+
const state = {
|
|
18
|
+
lessonsCompleted: [{ id: 'terminal-basico' }]
|
|
19
|
+
}
|
|
20
|
+
const menu = getSintaxisMenuConfig(state)
|
|
21
|
+
assert.equal(menu.options.length, 2)
|
|
22
|
+
assert.deepEqual(menu.options.map(option => option.id), ['terminal-basico', 'git-basico'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('sintaxis menu unlocks bash scripts after completing git básico', () => {
|
|
26
|
+
const state = {
|
|
27
|
+
lessonsCompleted: [
|
|
28
|
+
{ id: 'terminal-basico' },
|
|
29
|
+
{ id: 'git-basico' }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
const menu = getSintaxisMenuConfig(state)
|
|
33
|
+
assert.equal(menu.options.length, 3)
|
|
34
|
+
assert.deepEqual(menu.options.map(option => option.id), ['terminal-basico', 'git-basico', 'bash-scripts'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('main menu title changes when only one option is available', () => {
|
|
38
|
+
const config = getMainMenuConfig({ lessonsCompleted: [] })
|
|
39
|
+
assert.equal(config.title, 'Empecemos por aquí:')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('main menu title defaults once multiple options unlock', () => {
|
|
43
|
+
const config = getMainMenuConfig({ lessonsCompleted: [{ id: 'terminal-basico' }] })
|
|
44
|
+
assert.equal(config.title, 'Te ofrecemos las siguientes opciones:')
|
|
45
|
+
})
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Read(//private/tmp/**)",
|
|
5
|
-
"Bash(node:*)",
|
|
6
|
-
"WebFetch(domain:github.com)",
|
|
7
|
-
"Bash(npm install)",
|
|
8
|
-
"Bash(npm run dev:*)",
|
|
9
|
-
"Bash(rm:*)",
|
|
10
|
-
"Bash(npm publish:*)",
|
|
11
|
-
"Bash(npm view:*)"
|
|
12
|
-
],
|
|
13
|
-
"deny": [],
|
|
14
|
-
"ask": []
|
|
15
|
-
}
|
|
16
|
-
}
|
package/deploy-patch.sh
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Get current version from package.json
|
|
4
|
-
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
5
|
-
|
|
6
|
-
# Split version into parts
|
|
7
|
-
IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
|
|
8
|
-
MAJOR="${VERSION_PARTS[0]}"
|
|
9
|
-
MINOR="${VERSION_PARTS[1]}"
|
|
10
|
-
PATCH="${VERSION_PARTS[2]}"
|
|
11
|
-
|
|
12
|
-
# Increment patch
|
|
13
|
-
NEW_PATCH=$((PATCH + 1))
|
|
14
|
-
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
|
|
15
|
-
|
|
16
|
-
echo "📦 Bumping version: $CURRENT_VERSION → $NEW_VERSION"
|
|
17
|
-
|
|
18
|
-
# Update package.json
|
|
19
|
-
sed -i '' "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" package.json
|
|
20
|
-
|
|
21
|
-
echo "🚀 Publishing to npm..."
|
|
22
|
-
npm publish
|
|
23
|
-
|
|
24
|
-
if [ $? -eq 0 ]; then
|
|
25
|
-
echo "✅ Successfully published @icarusmx/creta@$NEW_VERSION"
|
|
26
|
-
else
|
|
27
|
-
echo "❌ Publish failed, reverting version..."
|
|
28
|
-
sed -i '' "s/\"version\": \"$NEW_VERSION\"/\"version\": \"$CURRENT_VERSION\"/" package.json
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|