ansimax 1.1.1 → 1.2.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/CHANGELOG.md CHANGED
@@ -3,6 +3,158 @@
3
3
  All notable changes to **ansimax** are documented in this file.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.2.0] — Phase 2 complete: animated, eased & conic gradients
7
+
8
+ Minor release closing the **gradient engine roadmap (Phase 2)** with three
9
+ long-awaited capabilities. All additions are fully backwards-compatible —
10
+ existing `gradient()` calls work identically.
11
+
12
+ ### Added — Easing curves
13
+
14
+ `gradient()` now accepts an `easing` option to control how colors are
15
+ distributed along the text. Five built-in curves plus custom functions:
16
+
17
+ ```ts
18
+ gradient('hello world', ['#ff0000', '#0000ff'], { easing: 'ease-in' });
19
+ gradient('hello world', ['#ff0000', '#0000ff'], { easing: 'ease-out' });
20
+ gradient('hello world', ['#ff0000', '#0000ff'], { easing: 'ease-in-out' });
21
+ gradient('hello world', ['#ff0000', '#0000ff'], { easing: 'cubic-bezier' });
22
+
23
+ // Or pass a custom EasingFn (t → eased t, both in [0,1])
24
+ gradient('hello world', ['#ff0000', '#0000ff'], { easing: (t) => t * t * t });
25
+ ```
26
+
27
+ - `linear` — even distribution (default, identical to pre-1.2.0 behavior)
28
+ - `ease-in` — concentrates colors at the end (quadratic)
29
+ - `ease-out` — concentrates colors at the start (quadratic)
30
+ - `ease-in-out` — slow at both ends, fast in middle
31
+ - `cubic-bezier` — CSS-style `ease` curve (Newton-Raphson approximated)
32
+ - Out-of-range custom easings are clamped to `[0, 1]` automatically
33
+
34
+ ### Added — Phase offset (flowing colors)
35
+
36
+ `gradient()` now accepts a `phase` parameter `[0, 1)` that shifts the
37
+ gradient along the text. Combined with an animation loop, this produces
38
+ a flowing color effect:
39
+
40
+ ```ts
41
+ gradient('hello', ['#ff0000', '#0000ff'], { phase: 0.5 });
42
+ // negative values wrap forward; NaN/Infinity falls back to 0
43
+ ```
44
+
45
+ ### Added — `animateGradient()` API
46
+
47
+ High-level API for animated gradients with proper lifecycle management:
48
+
49
+ ```ts
50
+ import { animateGradient } from 'ansimax';
51
+
52
+ const ctrl = animateGradient('Loading...', ['#ff79c6', '#bd93f9', '#8be9fd'], {
53
+ duration: 2000,
54
+ fps: 30,
55
+ direction: 'forward', // or 'reverse'
56
+ infinite: true,
57
+ });
58
+
59
+ // Later
60
+ ctrl.stop();
61
+ // Or via AbortController
62
+ const abort = new AbortController();
63
+ animateGradient('hi', stops, { signal: abort.signal });
64
+ abort.abort();
65
+ ```
66
+
67
+ Returns an `AnimateGradientController` with `.stop()` and `.done` (Promise).
68
+ Supports `AbortSignal`, custom render via `onFrame`, fps cap at 60,
69
+ and direction reversal.
70
+
71
+ ### Added — Conic gradients
72
+
73
+ `gradientRect()` now supports `style: 'conic'` for radial sweeps around
74
+ the center point:
75
+
76
+ ```ts
77
+ import { gradientRect } from 'ansimax';
78
+
79
+ console.log(gradientRect({
80
+ width: 30, height: 15,
81
+ colors: ['#ff0000', '#00ff00', '#0000ff', '#ff0000'],
82
+ style: 'conic',
83
+ startAngle: 0, // rotation in degrees
84
+ dither: 'bayer',
85
+ }));
86
+ ```
87
+
88
+ - `startAngle` (degrees) rotates the sweep
89
+ - Non-finite `startAngle` falls back to `0`
90
+ - Compatible with all existing options (`dither`, `braille`, `width`, `height`)
91
+
92
+ ### Added — New exports
93
+
94
+ - `animateGradient` — function
95
+ - `AnimateGradientOptions` / `AnimateGradientController` — types
96
+ - `EasingName` — union type of built-in curve names
97
+ - `EasingFn` — `(t: number) => number` curve type
98
+
99
+ ### Notes
100
+
101
+ - All 1848 + 30 new tests pass.
102
+ - Backwards-compatible: existing `gradient()` calls work identically.
103
+ - No new runtime dependencies.
104
+ - Phase 2 of the [roadmap](README.md#%EF%B8%8F-roadmap) is now **fully complete**.
105
+
106
+ ---
107
+
108
+ ## [1.1.2] — maturity & robustness
109
+
110
+ Patch release focused on maturity: better error semantics, defensive
111
+ defaults, and cleaner type re-exports. No API breaking changes — every
112
+ 1.1.1 program runs identically.
113
+
114
+ ### Fixed
115
+
116
+ - **CI: `jest.config.js` syntax error.** The config file used `export default {}`
117
+ (ESM syntax), which crashed in Node CommonJS context — including in
118
+ GitHub Actions runners. Fixed by switching to `module.exports = {}` to
119
+ match `useESM: false` in the ts-jest configuration. Tests now run
120
+ correctly across Linux, macOS, and Windows runners.
121
+
122
+ ### Improved
123
+
124
+ - **`process.setMaxListeners` defensive bump.** Ansimax modules
125
+ (`animations`, `frames`, `loaders`, `utils/ansi`) each register
126
+ `SIGINT` / `SIGTERM` / `exit` handlers for crash-safe cursor
127
+ restoration. With Node's default cap of 10, hot-reload setups
128
+ (Vite HMR, nodemon, ts-node-dev) could occasionally emit
129
+ `MaxListenersExceededWarning`. We now bump the cap to 20 on first
130
+ install — silently and safely, only if the current limit is lower.
131
+ Production apps unaffected.
132
+ - **Uniform `TypeError` for theme validation.** `themes.register()`
133
+ now throws `TypeError` for any structural / type issue (missing
134
+ fields, non-string `name`, invalid hex), matching the rest of the
135
+ validation surface. Previously it threw a mix of `Error` and
136
+ `TypeError`, which made `try / catch` filtering inconsistent.
137
+ - **`themes.use()` throws `RangeError`** for unknown theme names
138
+ (was `Error`). `RangeError` better reflects "value out of allowed
139
+ set" semantics — same standard library convention as `Array(-1)`.
140
+ Error message now also says "Available themes:" instead of
141
+ "Available:" for clarity.
142
+ - **Cleaner type re-exports in the barrel.** Added a header comment
143
+ explaining the legacy aliases (`stripAnsiColors`, `stripAnsiCodes`)
144
+ and recommending `stripAnsi` for new code. Version string in the
145
+ barrel header updated from the stale `v1.0.0` to `v1.1.2`.
146
+
147
+ ### Notes
148
+
149
+ - All 1848 tests pass; 4 new tests cover the error-type guarantees.
150
+ - The error-type changes are technically observable via `instanceof`
151
+ checks, but `RangeError` and `TypeError` both extend `Error`, so any
152
+ `catch (e: Error)` block keeps working. We classify this as a
153
+ non-breaking quality-of-life improvement.
154
+ - No new dependencies — still zero runtime deps.
155
+
156
+ ---
157
+
6
158
  ## [1.1.1] — bug fixes + improved examples
7
159
 
8
160
  Patch release with two bug fixes from real-world testing of v1.1.0, plus
package/README.es.md CHANGED
@@ -7,7 +7,7 @@
7
7
  _Colores • Gradientes • Animaciones • ASCII Art • Pixel Art • Árboles • Componentes • Temas_
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
10
- [![npm](https://img.shields.io/badge/npm-v1.1.1-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
10
+ [![npm](https://img.shields.io/badge/npm-v1.2.0-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
11
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg?style=flat-square)](tsconfig.json)
12
12
  [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg?style=flat-square)](#testing)
13
13
  [![Tests](https://img.shields.io/badge/tests-1700%2B%20passing-brightgreen.svg?style=flat-square)](#testing)
@@ -50,14 +50,14 @@ npm install ansimax
50
50
  ```
51
51
 
52
52
  ```ts
53
- import { color, gradient, ascii, loader } from 'ansimax';
53
+ import { color, gradient, ascii, loader, sleep } from 'ansimax';
54
54
 
55
55
  console.log(ascii.banner('hola', {
56
56
  colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9', '#8be9fd']),
57
57
  }));
58
58
 
59
59
  const stop = loader.spin('Construyendo proyecto', { color: '#bd93f9' });
60
- await algunaTareaAsync();
60
+ await sleep(1500);
61
61
  stop('Build completado', true);
62
62
  ```
63
63
 
@@ -123,14 +123,14 @@ yarn add ansimax
123
123
  ## ⚡ Ejemplo en 30 segundos
124
124
 
125
125
  ```ts
126
- import { color, gradient, loader, ascii } from 'ansimax';
126
+ import { color, gradient, loader, ascii, sleep } from 'ansimax';
127
127
 
128
128
  console.log(ascii.banner('deploy', {
129
129
  colorFn: (t) => gradient(t, ['#ff6b6b', '#feca57', '#48dbfb']),
130
130
  }));
131
131
 
132
132
  const stop = loader.spin('Construyendo proyecto', { color: '#bd93f9' });
133
- await algunaTareaAsync();
133
+ await sleep(1500); // simula trabajo asíncrono
134
134
  stop('Build completado', true); // ✓ + color de éxito
135
135
 
136
136
  console.log(color.green('✓') + ' Listo en ' + color.bold('1.4s'));
@@ -187,12 +187,66 @@ console.log(themes.primary('primary de cyberpunk'));
187
187
  <img src="media/colors.png" alt="Colores y gradientes" />
188
188
 
189
189
  ```ts
190
- import { color, gradient } from 'ansimax';
190
+ import { color, gradient, rainbow } from 'ansimax';
191
191
 
192
- color.red('rojo'); color.green('verde'); color.blue('azul');
193
- color.bold(texto); color.italic(texto); color.underline(texto);
194
- gradient('fuego a océano', ['#ff6b6b', '#feca57', '#48dbfb']);
195
- color.rainbow('preset rainbow integrado');
192
+ // Colores básicos
193
+ console.log(color.red('rojo'), color.green('verde'), color.blue('azul'));
194
+
195
+ // Modificadores de estilo
196
+ console.log(color.bold('negrita'), color.italic('itálica'), color.underline('subrayado'));
197
+
198
+ // Gradiente multi-stop
199
+ console.log(gradient('fuego a océano', ['#ff6b6b', '#feca57', '#48dbfb']));
200
+
201
+ // Preset rainbow integrado
202
+ console.log(rainbow('preset rainbow integrado'));
203
+ ```
204
+
205
+ ### Gradientes animados (v1.2.0)
206
+
207
+ ```ts
208
+ import { animateGradient, sleep } from 'ansimax';
209
+
210
+ // Animación de flujo de color — corre hasta llamar stop()
211
+ const ctrl = animateGradient('Cargando...', ['#ff79c6', '#bd93f9', '#8be9fd'], {
212
+ duration: 2000, // ms por ciclo
213
+ fps: 30,
214
+ direction: 'forward', // o 'reverse'
215
+ });
216
+
217
+ await sleep(3000);
218
+ ctrl.stop();
219
+ ```
220
+
221
+ ### Curvas de interpolación (v1.2.0)
222
+
223
+ ```ts
224
+ import { gradient } from 'ansimax';
225
+
226
+ // Cinco easings built-in + soporte para funciones personalizadas
227
+ gradient('hola mundo', stops, { easing: 'linear' });
228
+ gradient('hola mundo', stops, { easing: 'ease-in' });
229
+ gradient('hola mundo', stops, { easing: 'ease-out' });
230
+ gradient('hola mundo', stops, { easing: 'ease-in-out' });
231
+ gradient('hola mundo', stops, { easing: 'cubic-bezier' });
232
+
233
+ // O tu propia función de easing (t → t suavizado, ambos en [0,1])
234
+ gradient('hola mundo', stops, { easing: (t) => t * t * t });
235
+ ```
236
+
237
+ ### Gradientes cónicos (v1.2.0)
238
+
239
+ ```ts
240
+ import { gradientRect } from 'ansimax';
241
+
242
+ // Barrido radial alrededor del centro — rueda arcoíris
243
+ console.log(gradientRect({
244
+ width: 30, height: 15,
245
+ colors: ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff', '#ff0000'],
246
+ style: 'conic',
247
+ startAngle: 0, // ángulo de rotación en grados
248
+ dither: 'bayer',
249
+ }));
196
250
  ```
197
251
 
198
252
  ### ASCII Art
@@ -202,13 +256,13 @@ color.rainbow('preset rainbow integrado');
202
256
  ```ts
203
257
  import { ascii, gradient } from 'ansimax';
204
258
 
205
- ascii.banner('HOLA', {
259
+ console.log(ascii.banner('HOLA', {
206
260
  font: 'big',
207
261
  align: 'center',
208
262
  colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9']),
209
- });
263
+ }));
210
264
 
211
- ascii.box('¡Caja arcoiris!', { padding: 1, borderStyle: 'rounded' });
265
+ console.log(ascii.box('¡Caja arcoiris!', { padding: 1, borderStyle: 'rounded' }));
212
266
  ```
213
267
 
214
268
  ### Árboles
@@ -235,7 +289,7 @@ console.log(proyecto.render({
235
289
  <img src="media/pixel_art.png" alt="Pixel art" />
236
290
 
237
291
  ```ts
238
- import { images, createCanvas, gradientRect } from 'ansimax';
292
+ import { images, createCanvas, gradientRect, SPRITES } from 'ansimax';
239
293
 
240
294
  // Sprite integrado
241
295
  console.log(images.sprite('heart'));
@@ -251,7 +305,8 @@ console.log(gradientRect({
251
305
  const c = createCanvas(40, 10);
252
306
  c.fill({ r: 18, g: 18, b: 38 });
253
307
  c.drawCircle(20, 5, 4, { r: 255, g: 200, b: 0 }, true);
254
- c.drawSprite(2, 2, images.sprites.star!.pixels);
308
+ const starSprite = SPRITES.star;
309
+ if (starSprite) c.drawSprite(2, 2, starSprite.pixels);
255
310
  c.print();
256
311
  ```
257
312
 
@@ -262,15 +317,15 @@ c.print();
262
317
  ```ts
263
318
  import { components, color } from 'ansimax';
264
319
 
265
- components.table([
320
+ console.log(components.table([
266
321
  ['Módulo', 'Estado', 'Cobertura'],
267
322
  ['colors', color.green('● listo'), '100%'],
268
323
  ['animations', color.green('● listo'), '100%'],
269
324
  ['loaders', color.green('● listo'), '100%'],
270
- ], { borderStyle: 'rounded' });
325
+ ], { borderStyle: 'rounded' }));
271
326
 
272
- components.badge('VERSION', 'v1.1.1');
273
- components.badge('BUILD', 'passing');
327
+ console.log(components.badge('VERSION', 'v1.2.0'));
328
+ console.log(components.badge('BUILD', 'passing'));
274
329
  ```
275
330
 
276
331
  ### Timeline
@@ -278,22 +333,24 @@ components.badge('BUILD', 'passing');
278
333
  <img src="media/timeline.png" alt="Timeline" />
279
334
 
280
335
  ```ts
281
- components.timeline([
336
+ import { components } from 'ansimax';
337
+
338
+ console.log(components.timeline([
282
339
  { label: 'Init del proyecto', done: true, time: '10:00' },
283
340
  { label: 'Pipeline de build', done: true, time: '10:15' },
284
341
  { label: 'Correr tests', done: false, time: '10:32' },
285
342
  { label: 'Deploy a npm', done: false },
286
- ]);
343
+ ]));
287
344
  ```
288
345
 
289
346
  ### Loaders y Progreso
290
347
 
291
348
  ```ts
292
- import { loader } from 'ansimax';
349
+ import { loader, sleep } from 'ansimax';
293
350
 
294
351
  // Spinner con éxito/fallo
295
352
  const stop = loader.spin('Cargando...', { color: '#bd93f9' });
296
- await tarea();
353
+ await sleep(1500);
297
354
  stop('¡Listo!', true); // ✓ ícono verde
298
355
 
299
356
  // Barra de progreso animada
@@ -303,18 +360,22 @@ await loader.progressAnimate(100, 'Descargando', {
303
360
 
304
361
  // Tareas jerárquicas con ejecución paralela
305
362
  await loader.tasks([
306
- { text: 'Build', fn: async () => build(), subtasks: [
307
- { text: 'TypeScript', fn: async () => tsc() },
308
- { text: 'Bundle', fn: async () => bundle() },
309
- ]},
310
- { text: 'Test', fn: async () => test() },
363
+ {
364
+ text: 'Build',
365
+ fn: async () => await sleep(500),
366
+ subtasks: [
367
+ { text: 'TypeScript', fn: async () => await sleep(800) },
368
+ { text: 'Bundle', fn: async () => await sleep(600) },
369
+ ],
370
+ },
371
+ { text: 'Test', fn: async () => await sleep(700) },
311
372
  ], { parallel: true });
312
373
  ```
313
374
 
314
375
  ### Animaciones
315
376
 
316
377
  ```ts
317
- import { animate, gradient } from 'ansimax';
378
+ import { animate, gradient, sleep } from 'ansimax';
318
379
 
319
380
  await animate.typewriter('Bienvenido al wizard de deployment...', {
320
381
  speed: 30,
@@ -325,9 +386,9 @@ await animate.fadeIn('Carga completa', { duration: 600 });
325
386
 
326
387
  // Carrera de pasos contra timeout — nunca se cuelga
327
388
  await animate.parallel([
328
- async () => await checkNetwork(),
329
- async () => await checkDatabase(),
330
- async () => await checkAuth(),
389
+ async () => await sleep(500), // simulación de chequeo de red
390
+ async () => await sleep(700), // simulación de chequeo de base de datos
391
+ async () => await sleep(400), // simulación de chequeo de auth
331
392
  ], { timeout: 5000 });
332
393
  ```
333
394
 
@@ -340,7 +401,7 @@ import { themes, createTheme } from 'ansimax';
340
401
 
341
402
  // Temas built-in
342
403
  themes.use('dracula');
343
- themes.primary('hola');
404
+ console.log(themes.primary('hola'));
344
405
 
345
406
  // Escuchar cambios
346
407
  const off = themes.onChange((nuevo, anterior) => {
@@ -350,7 +411,27 @@ const off = themes.onChange((nuevo, anterior) => {
350
411
  // Multi-tenant: cada instancia totalmente aislada
351
412
  const tenantA = createTheme('nord');
352
413
  const tenantB = createTheme('matrix');
353
- tenantA.register('custom', miDef); // no se filtra a tenantB
414
+
415
+ // Definir un tema personalizado y registrarlo SÓLO en tenantA
416
+ tenantA.register('custom', {
417
+ name: 'Custom',
418
+ primary: '#ff5e5e',
419
+ secondary: '#5e5eff',
420
+ accent: '#5eff5e',
421
+ success: '#10b981',
422
+ warning: '#fbbf24',
423
+ error: '#ef4444',
424
+ info: '#06b6d4',
425
+ muted: '#6b7280',
426
+ bg: '#1e293b',
427
+ surface: '#334155',
428
+ text: '#f1f5f9',
429
+ gradient: ['#ff5e5e', '#5eff5e', '#5e5eff'],
430
+ });
431
+
432
+ console.log('tenantA incluye custom?', tenantA.list().includes('custom'));
433
+ console.log('tenantB incluye custom?', tenantB.list().includes('custom'));
434
+ // ↑ false — aislamiento total
354
435
  ```
355
436
 
356
437
  ---
@@ -421,11 +502,11 @@ const off = onConfigKeyChange('theme', (nuevo, anterior) => {
421
502
 
422
503
  // Override temporal + restauración automática al completar o lanzar
423
504
  await withConfig({ animationSpeed: 'fast' }, async () => {
424
- await correrDemo();
505
+ // ...tu código en modo fast aquí...
425
506
  });
426
507
 
427
508
  // Modo strict captura typos en config
428
- configure({ unknwnKey: 'x' }, { strict: true }); // lanza RangeError
509
+ // configure({ unknwnKey: 'x' }, { strict: true }); // lanza RangeError
429
510
  ```
430
511
 
431
512
  ---
@@ -453,9 +534,9 @@ El roadmap apunta intencionalmente — y busca superar — gaps que ni siquiera
453
534
  - [x] Gradientes a ángulo arbitrario
454
535
  - [x] Dithering Bayer 4×4 para transiciones tonales suaves
455
536
  - [x] UX single-stop (comportamiento estilo CSS)
456
- - [ ] **Gradientes animados** (flujo de color en el tiempo, loops infinitos)
457
- - [ ] **Curvas de interpolación** (linear / ease-in / ease-out / cubic-bezier)
458
- - [ ] **Gradientes cónicos** (barrido radial)
537
+ - [x] **Gradientes animados** flujo de color en el tiempo con `animateGradient()` (v1.2.0)
538
+ - [x] **Curvas de interpolación** — `linear` / `ease-in` / `ease-out` / `ease-in-out` / `cubic-bezier` / personalizado (v1.2.0)
539
+ - [x] **Gradientes cónicos** barrido radial con `style: 'conic'` (v1.2.0)
459
540
 
460
541
  ### 🟡 Fase 3 — Motor ASCII
461
542
  - [x] Fuentes de bloque (`big`, `small`)
@@ -645,6 +726,35 @@ ansimax/
645
726
 
646
727
  ## 📝 Changelog
647
728
 
729
+ ### v1.2.0 — Fase 2 completa: gradientes animados, easing y cónicos
730
+
731
+ Release minor que cierra el roadmap del motor de gradientes con tres features potentes:
732
+
733
+ - 🌊 **`animateGradient()`** — flujo de color en el tiempo con ciclo de vida apropiado (Promise, signal, fps, direction)
734
+ - 📐 **Curvas de interpolación** — `linear` / `ease-in` / `ease-out` / `ease-in-out` / `cubic-bezier` / funciones personalizadas
735
+ - ⭕ **Gradientes cónicos** — `gradientRect({ style: 'conic', startAngle })` para barridos radiales
736
+
737
+ ```ts
738
+ import { animateGradient } from 'ansimax';
739
+
740
+ const ctrl = animateGradient('Cargando...', ['#ff79c6', '#bd93f9', '#8be9fd']);
741
+ await sleep(3000);
742
+ ctrl.stop();
743
+ ```
744
+
745
+ Totalmente retrocompatible — todo programa 1.1.x corre idénticamente.
746
+
747
+ ### v1.1.2 — Madurez y robustez
748
+
749
+ Release patch enfocado en refinamientos de calidad — sin cambios en la API.
750
+
751
+ - 🛡️ **Bump defensivo de `process.setMaxListeners`** — previene `MaxListenersExceededWarning` en setups con HMR / nodemon / ts-node-dev donde los módulos de ansimax re-registran handlers de restauración de cursor
752
+ - 🧪 **`TypeError` uniforme para validación de themes** — `themes.register()` ahora arroja consistentemente `TypeError` para errores estructurales / de tipo (antes era mezcla de `Error` y `TypeError`)
753
+ - 🎯 **`themes.use()` arroja `RangeError`** para nombres de tema desconocidos (antes `Error`) — mejor match semántico con "valor fuera del set permitido"
754
+ - 📝 **Re-exports más limpios en el barrel** — comentario de header ahora documenta aliases legacy y recomienda nombres canónicos
755
+
756
+ Drop-in replacement para `1.1.1`.
757
+
648
758
  ### v1.1.1 — Fixes de bugs + ejemplos limpios
649
759
 
650
760
  Release patch que arregla dos bugs encontrados en testing real de v1.1.0, más una carpeta de ejemplos refrescada.
@@ -750,4 +860,4 @@ Ansimax está licenciada bajo **Apache License, Version 2.0** — una licencia p
750
860
 
751
861
  Si Ansimax te ayuda a hacer mejores CLIs, ¡dale ⭐ en [GitHub](https://github.com/Brashkie/ansimax)!
752
862
 
753
- </div>
863
+ </div>