@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.
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icarusmx/creta",
3
- "version": "1.5.14",
3
+ "version": "1.5.16",
4
4
  "description": "Salgamos de este laberinto.",
5
5
  "type": "module",
6
6
  "bin": {