@icarusmx/creta 1.5.14 → 1.5.16
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/README.md +476 -74
- package/bin/creta.js +15 -1
- package/lib/data/command-help/lz.js +40 -30
- package/lib/exercises/13-shell-aliases.md +183 -38
- package/lib/exercises/14-gh-fundamentals.md +976 -0
- package/lib/exercises/API_CHANGES.md +193 -0
- package/lib/exercises/utils/README.md +114 -0
- package/lib/exercises/utils/lz-functions.sh +240 -0
- package/lib/readers/exercise-reader.js +285 -0
- package/package.json +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync, mkdirSync, copyFileSync, statSync } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
|
|
10
|
+
export class ExerciseReader {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.exercisesDir = join(__dirname, '../exercises')
|
|
13
|
+
this.bibliotecaDir = join(homedir(), '.creta', 'biblioteca')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure biblioteca directory exists and sync exercises
|
|
18
|
+
*/
|
|
19
|
+
syncBiblioteca(silent = false) {
|
|
20
|
+
try {
|
|
21
|
+
// Create directory if needed
|
|
22
|
+
if (!existsSync(this.bibliotecaDir)) {
|
|
23
|
+
mkdirSync(this.bibliotecaDir, { recursive: true })
|
|
24
|
+
if (!silent) {
|
|
25
|
+
console.log(chalk.cyan('\n📚 Creando biblioteca en ~/.creta/biblioteca...'))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get all exercise files
|
|
30
|
+
const exercises = this.getAvailableExercises()
|
|
31
|
+
let copied = 0
|
|
32
|
+
let updated = 0
|
|
33
|
+
let skipped = 0
|
|
34
|
+
|
|
35
|
+
exercises.forEach(exercise => {
|
|
36
|
+
const sourcePath = join(this.exercisesDir, exercise.filename)
|
|
37
|
+
const destPath = join(this.bibliotecaDir, exercise.filename)
|
|
38
|
+
|
|
39
|
+
// Check if destination exists and is up to date
|
|
40
|
+
if (existsSync(destPath)) {
|
|
41
|
+
const sourceStats = statSync(sourcePath)
|
|
42
|
+
const destStats = statSync(destPath)
|
|
43
|
+
|
|
44
|
+
if (sourceStats.mtime > destStats.mtime) {
|
|
45
|
+
copyFileSync(sourcePath, destPath)
|
|
46
|
+
updated++
|
|
47
|
+
} else {
|
|
48
|
+
skipped++
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
copyFileSync(sourcePath, destPath)
|
|
52
|
+
copied++
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (!silent) {
|
|
57
|
+
console.log(chalk.green('\n✅ Biblioteca sincronizada'))
|
|
58
|
+
if (copied > 0) console.log(chalk.gray(` ${copied} ejercicio${copied !== 1 ? 's' : ''} copiado${copied !== 1 ? 's' : ''}`))
|
|
59
|
+
if (updated > 0) console.log(chalk.gray(` ${updated} ejercicio${updated !== 1 ? 's' : ''} actualizado${updated !== 1 ? 's' : ''}`))
|
|
60
|
+
if (skipped > 0) console.log(chalk.gray(` ${skipped} ejercicio${skipped !== 1 ? 's' : ''} sin cambios`))
|
|
61
|
+
console.log(chalk.cyan(`\n💡 Abre ejercicios con: nvim ~/.creta/biblioteca/<archivo>.md`))
|
|
62
|
+
console.log(chalk.gray(` Ejemplo: nvim ~/.creta/biblioteca/14-gh-fundamentals.md\n`))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { copied, updated, skipped, total: exercises.length }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!silent) {
|
|
68
|
+
console.error(chalk.red('\n❌ Error sincronizando biblioteca:'), error.message)
|
|
69
|
+
}
|
|
70
|
+
return { copied: 0, updated: 0, skipped: 0, total: 0, error }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if biblioteca needs initialization (show tip on first read)
|
|
76
|
+
*/
|
|
77
|
+
checkBiblioteca() {
|
|
78
|
+
if (!existsSync(this.bibliotecaDir)) {
|
|
79
|
+
return {
|
|
80
|
+
exists: false,
|
|
81
|
+
message: chalk.dim(`\n💡 Tip: Ejecuta ${chalk.cyan('creta biblioteca')} para copiar ejercicios a ~/.creta/biblioteca/\n Así podrás abrirlos en nvim con fold markers.`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { exists: true, message: null }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all available exercises
|
|
89
|
+
*/
|
|
90
|
+
getAvailableExercises() {
|
|
91
|
+
try {
|
|
92
|
+
const files = readdirSync(this.exercisesDir)
|
|
93
|
+
return files
|
|
94
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md' && f !== 'API_CHANGES.md')
|
|
95
|
+
.map(f => ({
|
|
96
|
+
filename: f,
|
|
97
|
+
id: f.replace('.md', ''),
|
|
98
|
+
number: this.extractNumber(f),
|
|
99
|
+
title: this.extractTitle(f)
|
|
100
|
+
}))
|
|
101
|
+
.sort((a, b) => (a.number || 999) - (b.number || 999))
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract number from filename (e.g., "01-" -> 1)
|
|
109
|
+
*/
|
|
110
|
+
extractNumber(filename) {
|
|
111
|
+
const match = filename.match(/^(\d+)-/)
|
|
112
|
+
return match ? parseInt(match[1], 10) : null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract title from filename (e.g., "01-developing-muscle-for-nvim" -> "Developing Muscle for Nvim")
|
|
117
|
+
*/
|
|
118
|
+
extractTitle(filename) {
|
|
119
|
+
const withoutExt = filename.replace('.md', '')
|
|
120
|
+
const withoutNumber = withoutExt.replace(/^\d+-/, '')
|
|
121
|
+
return withoutNumber
|
|
122
|
+
.split('-')
|
|
123
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
124
|
+
.join(' ')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Find exercise by ID (number or slug)
|
|
129
|
+
*/
|
|
130
|
+
findExercise(query) {
|
|
131
|
+
const exercises = this.getAvailableExercises()
|
|
132
|
+
|
|
133
|
+
// Try exact match by ID first
|
|
134
|
+
let exercise = exercises.find(e => e.id === query)
|
|
135
|
+
if (exercise) return exercise
|
|
136
|
+
|
|
137
|
+
// Try by number
|
|
138
|
+
const queryNum = parseInt(query, 10)
|
|
139
|
+
if (!isNaN(queryNum)) {
|
|
140
|
+
exercise = exercises.find(e => e.number === queryNum)
|
|
141
|
+
if (exercise) return exercise
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Try partial match (fuzzy)
|
|
145
|
+
const queryLower = query.toLowerCase()
|
|
146
|
+
exercise = exercises.find(e =>
|
|
147
|
+
e.id.toLowerCase().includes(queryLower) ||
|
|
148
|
+
e.title.toLowerCase().includes(queryLower)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return exercise || null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Read and display exercise
|
|
156
|
+
*/
|
|
157
|
+
read(query) {
|
|
158
|
+
const exercise = this.findExercise(query)
|
|
159
|
+
|
|
160
|
+
if (!exercise) {
|
|
161
|
+
this.showExerciseNotFound(query)
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const filepath = join(this.exercisesDir, exercise.filename)
|
|
167
|
+
const content = readFileSync(filepath, 'utf-8')
|
|
168
|
+
|
|
169
|
+
this.displayExercise(exercise, content)
|
|
170
|
+
|
|
171
|
+
// Show biblioteca tip on first read
|
|
172
|
+
const bibliotecaStatus = this.checkBiblioteca()
|
|
173
|
+
if (!bibliotecaStatus.exists && bibliotecaStatus.message) {
|
|
174
|
+
console.log(bibliotecaStatus.message)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(chalk.red('❌ Error leyendo ejercicio:'), error.message)
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Display exercise with formatting
|
|
186
|
+
*/
|
|
187
|
+
displayExercise(exercise, content) {
|
|
188
|
+
console.clear()
|
|
189
|
+
|
|
190
|
+
// Header
|
|
191
|
+
console.log(chalk.cyan('\n📚 Ejercicio Creta'))
|
|
192
|
+
console.log(chalk.gray('═'.repeat(70)))
|
|
193
|
+
console.log(chalk.bold(`\n${exercise.number ? `${exercise.number}. ` : ''}${exercise.title}`))
|
|
194
|
+
console.log(chalk.gray('─'.repeat(70)))
|
|
195
|
+
|
|
196
|
+
// Content (render markdown as-is, terminal will handle it)
|
|
197
|
+
console.log('\n' + content + '\n')
|
|
198
|
+
|
|
199
|
+
// Footer
|
|
200
|
+
console.log(chalk.gray('─'.repeat(70)))
|
|
201
|
+
console.log(chalk.dim('💡 Tip: Abre este archivo en nvim para usar fold markers: {{{ }}}'))
|
|
202
|
+
console.log(chalk.dim(` Archivo: lib/exercises/${exercise.filename}`))
|
|
203
|
+
console.log(chalk.gray('═'.repeat(70)) + '\n')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Show error when exercise not found
|
|
208
|
+
*/
|
|
209
|
+
showExerciseNotFound(query) {
|
|
210
|
+
console.log(chalk.red(`\n❌ Ejercicio "${query}" no encontrado\n`))
|
|
211
|
+
console.log(chalk.yellow('📚 Ejercicios disponibles:\n'))
|
|
212
|
+
|
|
213
|
+
const exercises = this.getAvailableExercises()
|
|
214
|
+
exercises.forEach(e => {
|
|
215
|
+
const num = e.number ? chalk.cyan(`${e.number}.`.padEnd(4)) : ' '
|
|
216
|
+
const title = chalk.white(e.title)
|
|
217
|
+
const id = chalk.gray(`(${e.id})`)
|
|
218
|
+
console.log(` ${num}${title} ${id}`)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
console.log(chalk.gray('\n💡 Uso: creta read <número|nombre>'))
|
|
222
|
+
console.log(chalk.gray(' Ejemplos:'))
|
|
223
|
+
console.log(chalk.gray(' creta read 1'))
|
|
224
|
+
console.log(chalk.gray(' creta read gh-fundamentals'))
|
|
225
|
+
console.log(chalk.gray(' creta read nvim\n'))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List all available exercises
|
|
230
|
+
*/
|
|
231
|
+
list() {
|
|
232
|
+
console.log(chalk.cyan('\n📚 Ejercicios Disponibles en Creta'))
|
|
233
|
+
console.log(chalk.gray('═'.repeat(70)))
|
|
234
|
+
|
|
235
|
+
const exercises = this.getAvailableExercises()
|
|
236
|
+
|
|
237
|
+
if (exercises.length === 0) {
|
|
238
|
+
console.log(chalk.yellow('\n⚠️ No hay ejercicios disponibles\n'))
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log()
|
|
243
|
+
exercises.forEach(e => {
|
|
244
|
+
const num = e.number ? chalk.cyan(`${e.number}.`.padEnd(4)) : ' '
|
|
245
|
+
const title = chalk.bold(e.title)
|
|
246
|
+
const id = chalk.gray(`(${e.id})`)
|
|
247
|
+
console.log(` ${num}${title}`)
|
|
248
|
+
console.log(` ${id}`)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
console.log(chalk.gray('\n💡 Para leer un ejercicio:'))
|
|
252
|
+
console.log(chalk.yellow(' creta read <número|nombre>'))
|
|
253
|
+
console.log(chalk.gray(' Ejemplo: creta read 14\n'))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* CLI entry point
|
|
259
|
+
*/
|
|
260
|
+
export async function readExercise(query) {
|
|
261
|
+
const reader = new ExerciseReader()
|
|
262
|
+
|
|
263
|
+
if (!query) {
|
|
264
|
+
reader.list()
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
reader.read(query)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* List exercises CLI entry point
|
|
273
|
+
*/
|
|
274
|
+
export async function listExercises() {
|
|
275
|
+
const reader = new ExerciseReader()
|
|
276
|
+
reader.list()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Sync biblioteca CLI entry point
|
|
281
|
+
*/
|
|
282
|
+
export async function syncBiblioteca() {
|
|
283
|
+
const reader = new ExerciseReader()
|
|
284
|
+
reader.syncBiblioteca()
|
|
285
|
+
}
|