@meebs/meeb 1.0.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.
Files changed (89) hide show
  1. package/README.md +158 -0
  2. package/animals/alien.js +34 -0
  3. package/animals/axolotl.js +38 -0
  4. package/animals/bat.js +34 -0
  5. package/animals/bear.js +41 -0
  6. package/animals/bee.js +32 -0
  7. package/animals/bunny.js +45 -0
  8. package/animals/butterfly.js +36 -0
  9. package/animals/capybara.js +36 -0
  10. package/animals/cat.js +44 -0
  11. package/animals/chameleon.js +44 -0
  12. package/animals/chick.js +27 -0
  13. package/animals/cookie.js +30 -0
  14. package/animals/crab.js +32 -0
  15. package/animals/crocodile.js +32 -0
  16. package/animals/deer.js +35 -0
  17. package/animals/dino.js +37 -0
  18. package/animals/dog.js +44 -0
  19. package/animals/dragon.js +44 -0
  20. package/animals/duck.js +45 -0
  21. package/animals/eagle.js +37 -0
  22. package/animals/elephant.js +33 -0
  23. package/animals/firefly.js +37 -0
  24. package/animals/fish.js +32 -0
  25. package/animals/flamingo.js +36 -0
  26. package/animals/fox.js +43 -0
  27. package/animals/frog.js +46 -0
  28. package/animals/ghost.js +32 -0
  29. package/animals/giraffe.js +37 -0
  30. package/animals/gorilla.js +37 -0
  31. package/animals/hamster.js +35 -0
  32. package/animals/hedgehog.js +32 -0
  33. package/animals/hippo.js +34 -0
  34. package/animals/index.js +87 -0
  35. package/animals/jellyfish.js +34 -0
  36. package/animals/koala.js +34 -0
  37. package/animals/ladybug.js +34 -0
  38. package/animals/lion.js +36 -0
  39. package/animals/mantisshrimp.js +49 -0
  40. package/animals/monkey.js +35 -0
  41. package/animals/moth.js +41 -0
  42. package/animals/mouse.js +33 -0
  43. package/animals/narwhal.js +38 -0
  44. package/animals/octopus.js +31 -0
  45. package/animals/otter.js +43 -0
  46. package/animals/owl.js +38 -0
  47. package/animals/panda.js +42 -0
  48. package/animals/parrot.js +39 -0
  49. package/animals/peacock.js +42 -0
  50. package/animals/penguin.js +47 -0
  51. package/animals/penguin2.js +38 -0
  52. package/animals/pig.js +33 -0
  53. package/animals/raccoon.js +35 -0
  54. package/animals/redpanda.js +39 -0
  55. package/animals/robot.js +37 -0
  56. package/animals/scorpion.js +33 -0
  57. package/animals/seahorse.js +39 -0
  58. package/animals/shark.js +33 -0
  59. package/animals/shit.js +33 -0
  60. package/animals/sloth.js +34 -0
  61. package/animals/snail.js +34 -0
  62. package/animals/snake.js +36 -0
  63. package/animals/spider.js +33 -0
  64. package/animals/squid.js +33 -0
  65. package/animals/starfish.js +32 -0
  66. package/animals/stingray.js +41 -0
  67. package/animals/swan.js +34 -0
  68. package/animals/toucan.js +41 -0
  69. package/animals/turtle.js +35 -0
  70. package/animals/unicorn.js +40 -0
  71. package/animals/walrus.js +35 -0
  72. package/animals/whale.js +42 -0
  73. package/animals/wolf.js +36 -0
  74. package/animals/yak.js +36 -0
  75. package/animals/zebra.js +34 -0
  76. package/color.js +9 -0
  77. package/completions/_meeb +41 -0
  78. package/completions/meeb.bash +16 -0
  79. package/emoji.js +30 -0
  80. package/fortune.js +41 -0
  81. package/gif.js +435 -0
  82. package/index.js +915 -0
  83. package/info.js +80 -0
  84. package/package.json +39 -0
  85. package/png.js +88 -0
  86. package/svg.js +43 -0
  87. package/theme.js +80 -0
  88. package/tui.js +76 -0
  89. package/util.js +313 -0
package/index.js ADDED
@@ -0,0 +1,915 @@
1
+ #!/usr/bin/env node
2
+
3
+ const animals = require('./animals')
4
+ const visible = Object.fromEntries(Object.entries(animals).filter(([, v]) => !v.hidden))
5
+ const { rgb } = require('./color')
6
+ const { addSay, addHat, addLabel, stripAnsi, flipArt, stretchArt, squishArt, stretchVArt, squishVArt, sideBySide } = require('./util')
7
+ const { resolve: emojiResolve } = require('./emoji')
8
+ const { getFortune } = require('./fortune')
9
+
10
+ const args = process.argv.slice(2)
11
+ const flags = {}
12
+ const positional = []
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ if (args[i] === '--say') {
16
+ const words = []
17
+ while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
18
+ words.push(args[++i])
19
+ }
20
+ if (words.length) flags.say = words.join(' ')
21
+ } else if (args[i] === '--gif') {
22
+ flags.gif = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.gif'
23
+ } else if (args[i] === '--png') {
24
+ flags.png = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.png'
25
+ } else if (args[i] === '--svg') {
26
+ flags.svg = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.svg'
27
+ } else if (args[i] === '--label') {
28
+ const words = []
29
+ while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
30
+ words.push(args[++i])
31
+ }
32
+ if (words.length) flags.label = words.join(' ')
33
+ } else if (args[i] === '--hat') {
34
+ flags.hat = true
35
+ } else if (args[i] === '--tiny') {
36
+ flags.tiny = true
37
+ } else if (args[i] === '--no-color') {
38
+ flags.noColor = true
39
+ } else if (args[i] === '--theme') {
40
+ flags.theme = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
41
+ } else if (args[i] === '--pair') {
42
+ flags.pair = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
43
+ } else if (args[i] === '--diff') {
44
+ flags.diff = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
45
+ } else if (args[i] === '--animate') {
46
+ flags.animate = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
47
+ } else if (args[i].startsWith('--')) {
48
+ flags[args[i].slice(2)] = true
49
+ } else {
50
+ positional.push(args[i])
51
+ }
52
+ }
53
+
54
+ // Treat bare subcommand words in positional as flags
55
+ // e.g. "meeb mouse say hi" => animal=mouse, flags.say="hi"
56
+ const flagWords = {
57
+ say: 'say', says: 'say', saying: 'say', info: 'info', pair: 'pair', diff: 'diff',
58
+ tiny: 'tiny', hat: 'hat', flip: 'flip', fortune: 'fortune',
59
+ theme: 'theme', animate: 'animate', motd: 'motd', calendar: 'calendar',
60
+ commit: 'commit', ci: 'ci', gif: 'gif', png: 'png', svg: 'svg',
61
+ stretch: 'stretch', squish: 'squish', stretchv: 'stretchv', squishv: 'squishv',
62
+ white: 'white', label: 'label', delay: 'delay', slide: 'slide', fall: 'fall', shatter: 'shatter',
63
+ }
64
+ const cleaned = []
65
+ for (let i = 0; i < positional.length; i++) {
66
+ const word = positional[i].toLowerCase()
67
+ if (flagWords[word] && !flags[flagWords[word]]) {
68
+ const key = flagWords[word]
69
+ if (key === 'say') {
70
+ const words = []
71
+ while (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
72
+ words.push(positional[++i])
73
+ }
74
+ if (words.length) flags.say = words.join(' ')
75
+ } else if (key === 'label') {
76
+ const words = []
77
+ while (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
78
+ words.push(positional[++i])
79
+ }
80
+ if (words.length) flags.label = words.join(' ')
81
+ } else if (['tiny', 'hat', 'flip', 'fortune', 'motd', 'calendar', 'commit', 'ci', 'stretch', 'squish', 'stretchv', 'squishv', 'white', 'delay', 'slide', 'fall', 'shatter'].includes(key)) {
82
+ flags[key] = true
83
+ } else if (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
84
+ flags[key] = positional[++i]
85
+ } else {
86
+ flags[key] = true
87
+ }
88
+ } else {
89
+ cleaned.push(positional[i])
90
+ }
91
+ }
92
+ positional.length = 0
93
+ positional.push(...cleaned)
94
+
95
+ const cmdWords = ['all', 'random', 'browse', 'list', 'count', 'help']
96
+ const cmd = (cmdWords.includes(positional[0]) ? positional[0] : null)
97
+ || Object.keys(flags).find(f => cmdWords.includes(f))
98
+ || positional[0]
99
+
100
+ function output(art) {
101
+ if (flags.noColor) return stripAnsi(art)
102
+ return art
103
+ }
104
+
105
+ function applyPipeline(art) {
106
+ if (flags.hat) art = addHat(art)
107
+ if (flags.theme) {
108
+ const { applyTheme } = require('./theme')
109
+ art = applyTheme(art, flags.theme)
110
+ }
111
+ if (flags.stretch) art = stretchArt(art)
112
+ if (flags.squish) art = squishArt(art)
113
+ if (flags.stretchv) art = stretchVArt(art)
114
+ if (flags.squishv) art = squishVArt(art)
115
+ if (flags.flip) art = flipArt(art)
116
+ if (flags.say) art = addSay(art, flags.say)
117
+ if (flags.label) art = addLabel(art, flags.label)
118
+ return art
119
+ }
120
+
121
+ const HIDE = '\x1b[?25l'
122
+ const SHOW = '\x1b[?25h'
123
+ const RESET = '\x1b[0m'
124
+ const CLEAR = '\x1b[2J\x1b[H'
125
+
126
+ function parseArtCells(art) {
127
+ const lines = art.split('\n')
128
+ const cells = []
129
+ let maxCol = 0
130
+ for (let row = 0; row < lines.length; row++) {
131
+ const line = lines[row]
132
+ let currentStyle = ''
133
+ let col = 0
134
+ let i = 0
135
+ while (i < line.length) {
136
+ if (line[i] === '\x1b' && i + 1 < line.length && line[i + 1] === '[') {
137
+ let j = i + 2
138
+ while (j < line.length && line[j] !== 'm') j++
139
+ const code = line.slice(i, j + 1)
140
+ if (code === '\x1b[0m') currentStyle = ''
141
+ else currentStyle += code
142
+ i = j + 1
143
+ } else {
144
+ if (line[i] !== ' ') {
145
+ cells.push({ row, col, style: currentStyle, char: line[i] })
146
+ if (col > maxCol) maxCol = col
147
+ }
148
+ col++
149
+ i++
150
+ }
151
+ }
152
+ }
153
+ return { cells, height: lines.length, width: maxCol + 1 }
154
+ }
155
+
156
+ function animSetup(label) {
157
+ process.stdout.write(HIDE + CLEAR)
158
+ let artStartRow = 1
159
+ if (label) {
160
+ process.stdout.write(output(label) + '\n')
161
+ artStartRow = 2
162
+ }
163
+ return artStartRow
164
+ }
165
+
166
+ function animCleanup(height, artStartRow) {
167
+ process.stdout.write(`\x1b[${height + artStartRow};1H` + SHOW)
168
+ }
169
+
170
+ function drawCells(cells, artStartRow, offR, offC, termW, termH) {
171
+ let buf = ''
172
+ for (const c of cells) {
173
+ const r = c.row + artStartRow + (offR || 0)
174
+ const col = c.col + 1 + (offC || 0)
175
+ if (r < 1 || col < 1 || (termH && r > termH) || (termW && col > termW)) continue
176
+ buf += `\x1b[${r};${col}H${RESET}${c.style}${c.char}`
177
+ }
178
+ buf += RESET
179
+ process.stdout.write(buf)
180
+ }
181
+
182
+ function delayReveal(art, label) {
183
+ if (!process.stdout.isTTY) {
184
+ if (label) console.log(output(label))
185
+ console.log(output(art))
186
+ return
187
+ }
188
+
189
+ const { cells, height } = parseArtCells(art)
190
+
191
+ // Fisher-Yates shuffle
192
+ for (let i = cells.length - 1; i > 0; i--) {
193
+ const j = Math.floor(Math.random() * (i + 1))
194
+ ;[cells[i], cells[j]] = [cells[j], cells[i]]
195
+ }
196
+
197
+ const artStartRow = animSetup(label)
198
+
199
+ const totalSteps = 20
200
+ const batchSize = Math.max(1, Math.ceil(cells.length / totalSteps))
201
+ let idx = 0
202
+
203
+ function step() {
204
+ const end = Math.min(idx + batchSize, cells.length)
205
+ let buf = ''
206
+ for (let i = idx; i < end; i++) {
207
+ const c = cells[i]
208
+ buf += `\x1b[${c.row + artStartRow};${c.col + 1}H${RESET}${c.style}${c.char}`
209
+ }
210
+ buf += RESET
211
+ process.stdout.write(buf)
212
+ idx = end
213
+ if (idx < cells.length) {
214
+ setTimeout(step, 60)
215
+ } else {
216
+ animCleanup(height, artStartRow)
217
+ }
218
+ }
219
+
220
+ process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
221
+ step()
222
+ }
223
+
224
+ function slideReveal(art, label) {
225
+ if (!process.stdout.isTTY) {
226
+ if (label) console.log(output(label))
227
+ console.log(output(art))
228
+ return
229
+ }
230
+
231
+ const { cells, height, width } = parseArtCells(art)
232
+ const termW = process.stdout.columns || 80
233
+ const artStartRow = animSetup(label)
234
+
235
+ const startOffset = termW
236
+ const totalFrames = 18
237
+ let frame = 0
238
+
239
+ function step() {
240
+ const t = frame / (totalFrames - 1) // 0 to 1
241
+ // ease-out: decelerate as it arrives
242
+ const ease = 1 - Math.pow(1 - t, 3)
243
+ const offset = Math.round(startOffset * (1 - ease))
244
+
245
+ process.stdout.write(CLEAR)
246
+ if (label) {
247
+ process.stdout.write(`\x1b[1;1H${output(label)}`)
248
+ }
249
+ drawCells(cells, artStartRow, 0, offset, termW, null)
250
+
251
+ frame++
252
+ if (frame < totalFrames) {
253
+ setTimeout(step, 45)
254
+ } else {
255
+ animCleanup(height, artStartRow)
256
+ }
257
+ }
258
+
259
+ process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
260
+ step()
261
+ }
262
+
263
+ function fallReveal(art, label) {
264
+ if (!process.stdout.isTTY) {
265
+ if (label) console.log(output(label))
266
+ console.log(output(art))
267
+ return
268
+ }
269
+
270
+ const { cells, height } = parseArtCells(art)
271
+ const termH = process.stdout.rows || 24
272
+ const artStartRow = animSetup(label)
273
+
274
+ const startOffset = -(height + artStartRow)
275
+ const totalFrames = 20
276
+ let frame = 0
277
+
278
+ function step() {
279
+ const t = frame / (totalFrames - 1)
280
+ // bounce ease: overshoot then settle
281
+ let ease
282
+ if (t < 0.7) {
283
+ ease = (t / 0.7)
284
+ } else if (t < 0.85) {
285
+ ease = 1 + 0.15 * Math.sin((t - 0.7) / 0.15 * Math.PI)
286
+ } else {
287
+ ease = 1
288
+ }
289
+ const offset = Math.round(startOffset * (1 - ease))
290
+
291
+ process.stdout.write(CLEAR)
292
+ if (label) {
293
+ process.stdout.write(`\x1b[1;1H${output(label)}`)
294
+ }
295
+ drawCells(cells, artStartRow, offset, 0, null, termH)
296
+
297
+ frame++
298
+ if (frame < totalFrames) {
299
+ setTimeout(step, 45)
300
+ } else {
301
+ animCleanup(height, artStartRow)
302
+ }
303
+ }
304
+
305
+ process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
306
+ step()
307
+ }
308
+
309
+ function shatterReveal(art, label) {
310
+ if (!process.stdout.isTTY) {
311
+ if (label) console.log(output(label))
312
+ console.log(output(art))
313
+ return
314
+ }
315
+
316
+ const { cells, height, width } = parseArtCells(art)
317
+ const termW = process.stdout.columns || 80
318
+ const termH = process.stdout.rows || 24
319
+ const artStartRow = animSetup(label)
320
+
321
+ // Draw the full art first
322
+ drawCells(cells, artStartRow, 0, 0, null, null)
323
+
324
+ // After a pause, shatter
325
+ setTimeout(() => {
326
+ // Give each cell physics properties
327
+ const particles = cells.map(c => ({
328
+ x: c.col,
329
+ y: c.row,
330
+ vx: (Math.random() - 0.5) * 4,
331
+ vy: -(Math.random() * 3 + 1),
332
+ style: c.style,
333
+ char: c.char,
334
+ }))
335
+
336
+ const gravity = 0.35
337
+ let frame = 0
338
+ const maxFrames = 40
339
+
340
+ function step() {
341
+ process.stdout.write(CLEAR)
342
+ if (label) {
343
+ process.stdout.write(`\x1b[1;1H${output(label)}`)
344
+ }
345
+
346
+ let anyVisible = false
347
+ let buf = ''
348
+ for (const p of particles) {
349
+ p.vy += gravity
350
+ p.x += p.vx
351
+ p.y += p.vy
352
+
353
+ const r = Math.round(p.y) + artStartRow
354
+ const col = Math.round(p.x) + 1
355
+ if (r >= 1 && r <= termH && col >= 1 && col <= termW) {
356
+ buf += `\x1b[${r};${col}H${RESET}${p.style}${p.char}`
357
+ anyVisible = true
358
+ }
359
+ }
360
+ buf += RESET
361
+ process.stdout.write(buf)
362
+
363
+ frame++
364
+ if (frame < maxFrames && anyVisible) {
365
+ setTimeout(step, 40)
366
+ } else {
367
+ process.stdout.write(CLEAR)
368
+ animCleanup(0, 1)
369
+ }
370
+ }
371
+
372
+ step()
373
+ }, 800)
374
+
375
+ process.on('SIGINT', () => { process.stdout.write(CLEAR); animCleanup(0, 1); process.exit(0) })
376
+ }
377
+
378
+ function exportOrPrint(art, label) {
379
+ const bgColor = flags.white ? [255, 255, 255] : undefined
380
+ const exported = []
381
+ if (flags.gif) {
382
+ const { generateGif } = require('./gif')
383
+ const path = typeof flags.gif === 'string' ? flags.gif : 'meeb.gif'
384
+ generateGif(art, path, bgColor)
385
+ exported.push(path)
386
+ }
387
+ if (flags.png) {
388
+ const { generatePng } = require('./png')
389
+ const path = typeof flags.png === 'string' ? flags.png : 'meeb.png'
390
+ generatePng(art, path, bgColor)
391
+ exported.push(path)
392
+ }
393
+ if (flags.svg) {
394
+ const { generateSvg } = require('./svg')
395
+ const path = typeof flags.svg === 'string' ? flags.svg : 'meeb.svg'
396
+ generateSvg(art, path, bgColor)
397
+ exported.push(path)
398
+ }
399
+ if (exported.length > 0) {
400
+ console.log(` saved to ${exported.join(', ')}`)
401
+ } else if (flags.delay) {
402
+ delayReveal(output(art), label ? output(label) : null)
403
+ } else if (flags.slide) {
404
+ slideReveal(output(art), label ? output(label) : null)
405
+ } else if (flags.fall) {
406
+ fallReveal(output(art), label ? output(label) : null)
407
+ } else if (flags.shatter) {
408
+ shatterReveal(output(art), label ? output(label) : null)
409
+ } else {
410
+ if (label) console.log(output(label))
411
+ console.log(output(art))
412
+ }
413
+ }
414
+
415
+ function levenshtein(a, b) {
416
+ const m = a.length, n = b.length
417
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1))
418
+ for (let i = 0; i <= m; i++) dp[i][0] = i
419
+ for (let j = 0; j <= n; j++) dp[0][j] = j
420
+ for (let i = 1; i <= m; i++)
421
+ for (let j = 1; j <= n; j++)
422
+ dp[i][j] = Math.min(
423
+ dp[i - 1][j] + 1,
424
+ dp[i][j - 1] + 1,
425
+ dp[i - 1][j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0)
426
+ )
427
+ return dp[m][n]
428
+ }
429
+
430
+ function resolveAnimal(name) {
431
+ const resolved = emojiResolve(name)
432
+ let animal = animals[resolved || name]
433
+ if (animal) return animal
434
+
435
+ const input = name.toLowerCase()
436
+ const names = Object.keys(animals)
437
+
438
+ const sub = names.find(n => n.startsWith(input) || input.startsWith(n))
439
+ || names.find(n => n.includes(input) || input.includes(n))
440
+
441
+ if (sub) return animals[sub]
442
+
443
+ let best = null, bestD = Infinity
444
+ for (const n of names) {
445
+ const d = levenshtein(input, n)
446
+ if (d < bestD) { bestD = d; best = n }
447
+ }
448
+ if (best && bestD <= Math.max(2, Math.ceil(input.length / 3))) {
449
+ return animals[best]
450
+ }
451
+ return null
452
+ }
453
+
454
+ function getRandomAnimal() {
455
+ const keys = Object.keys(visible)
456
+ return visible[keys[Math.floor(Math.random() * keys.length)]]
457
+ }
458
+
459
+ function renderAnimal(animal) {
460
+ return flags.tiny && animal.tiny ? animal.tiny() : animal.render()
461
+ }
462
+
463
+ // --- Pipe stdin support ---
464
+ // If stdin is piped, read it as --say message
465
+ function readStdin() {
466
+ if (process.stdin.isTTY) return null
467
+ try {
468
+ const fs = require('fs')
469
+ const input = fs.readFileSync(0, 'utf8').trim()
470
+ return input || null
471
+ } catch {
472
+ return null
473
+ }
474
+ }
475
+
476
+ const stdinMsg = readStdin()
477
+ if (stdinMsg && !flags.say) {
478
+ flags.say = stdinMsg
479
+ }
480
+
481
+ // --- Help ---
482
+ if ((!cmd && Object.keys(flags).length === 0) || cmd === 'help') {
483
+ console.log('')
484
+ console.log(rgb(215, 119, 87, ' meeb') + ' — cute ascii animals for your terminal')
485
+ console.log('')
486
+ console.log(' Usage: meeb <animal> [flags]')
487
+ console.log(' echo "hello" | meeb penguin')
488
+ console.log('')
489
+ console.log(' Animals:')
490
+ for (const name of Object.keys(visible)) {
491
+ console.log(` ${rgb(215, 119, 87, '●')} ${name}`)
492
+ }
493
+ console.log('')
494
+ console.log(' Commands:')
495
+ console.log(' --all show all animals')
496
+ console.log(' --random show a random animal')
497
+ console.log(' --list print names only (for scripting)')
498
+ console.log(' --count print total number of animals')
499
+ console.log(' --browse interactive TUI to browse animals')
500
+ console.log('')
501
+ console.log(' Display:')
502
+ console.log(' --say "msg" speech bubble above the animal')
503
+ console.log(' --tiny compact 4-5 row version')
504
+ console.log(' --hat seasonal hat (auto-detects month)')
505
+ console.log(' --flip mirror the animal horizontally')
506
+ console.log(' --fortune random fortune as speech bubble')
507
+ console.log(' --theme <name> color theme (neon, pastel, retro, mono, matrix)')
508
+ console.log(' --label "text" subtitle text below the animal')
509
+ console.log(' --no-color strip ANSI codes (for piping)')
510
+ console.log('')
511
+ console.log(' Transforms:')
512
+ console.log(' --stretch stretch horizontally (wider)')
513
+ console.log(' --squish squish horizontally (narrower)')
514
+ console.log(' --stretchv stretch vertically (taller)')
515
+ console.log(' --squishv squish vertically (shorter)')
516
+ console.log('')
517
+ console.log(' Animations:')
518
+ console.log(' --delay pixels materialize randomly')
519
+ console.log(' --slide slide in from the right')
520
+ console.log(' --fall drop from the top with a bounce')
521
+ console.log(' --shatter appear then explode with gravity')
522
+ console.log('')
523
+ console.log(' Features:')
524
+ console.log(' --motd message of the day (for .bashrc/.zshrc)')
525
+ console.log(' --pair <animal> show two animals side by side')
526
+ console.log(' --diff <animal> compare two animals side by side')
527
+ console.log(' --animate <animal> terminal animation (3 frames)')
528
+ console.log(' --info <animal> fun fact about the animal')
529
+ console.log(' --calendar animal of the day')
530
+ console.log(' --commit git commit message as speech bubble')
531
+ console.log(' --ci markdown-safe output (no ANSI)')
532
+ console.log('')
533
+ console.log(' Export:')
534
+ console.log(' --gif [path] save as gif (default: meeb.gif)')
535
+ console.log(' --png [path] save as png (default: meeb.png)')
536
+ console.log(' --svg [path] save as svg (default: meeb.svg)')
537
+ console.log(' --white white background for exports')
538
+ console.log('')
539
+ console.log(' Setup:')
540
+ console.log(' --install-completions install shell completions (bash/zsh/fish)')
541
+ console.log('')
542
+ console.log(' Emoji:')
543
+ console.log(' meeb 🐧 looks up the matching animal')
544
+ console.log('')
545
+ process.exit(0)
546
+ }
547
+
548
+ if (cmd === 'list') {
549
+ console.log(Object.keys(visible).join('\n'))
550
+ process.exit(0)
551
+ }
552
+
553
+ if (cmd === 'count') {
554
+ console.log(Object.keys(visible).length)
555
+ process.exit(0)
556
+ }
557
+
558
+ // --- --install-completions ---
559
+ if (flags['install-completions']) {
560
+ const fs = require('fs')
561
+ const path = require('path')
562
+ const os = require('os')
563
+ const shell = (process.env.SHELL || '').split('/').pop()
564
+ const completionsDir = path.join(__dirname, 'completions')
565
+
566
+ if (shell === 'zsh') {
567
+ const src = path.join(completionsDir, '_meeb')
568
+ const dest = path.join(os.homedir(), '.zsh', 'completions', '_meeb')
569
+ const destDir = path.dirname(dest)
570
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
571
+ fs.copyFileSync(src, dest)
572
+ console.log(` Installed zsh completions to ${dest}`)
573
+ console.log(` Add to .zshrc: fpath=(~/.zsh/completions $fpath)`)
574
+ console.log(` Then run: autoload -Uz compinit && compinit`)
575
+ } else if (shell === 'bash') {
576
+ const src = path.join(completionsDir, 'meeb.bash')
577
+ const dest = path.join(os.homedir(), '.bash_completion.d', 'meeb.bash')
578
+ const destDir = path.dirname(dest)
579
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
580
+ fs.copyFileSync(src, dest)
581
+ console.log(` Installed bash completions to ${dest}`)
582
+ console.log(` Add to .bashrc: source ~/.bash_completion.d/meeb.bash`)
583
+ } else if (shell === 'fish') {
584
+ const fishDir = path.join(os.homedir(), '.config', 'fish', 'completions')
585
+ if (!fs.existsSync(fishDir)) fs.mkdirSync(fishDir, { recursive: true })
586
+ const dest = path.join(fishDir, 'meeb.fish')
587
+ // generate fish completions
588
+ const animalNames = Object.keys(visible).join(' ')
589
+ const fishContent = [
590
+ '# meeb fish completions',
591
+ `complete -c meeb -f -a "${animalNames}"`,
592
+ 'complete -c meeb -l all -d "show all animals"',
593
+ 'complete -c meeb -l random -d "show a random animal"',
594
+ 'complete -c meeb -l list -d "print names only"',
595
+ 'complete -c meeb -l count -d "print total number"',
596
+ 'complete -c meeb -l say -d "speech bubble" -r',
597
+ 'complete -c meeb -l tiny -d "compact version"',
598
+ 'complete -c meeb -l hat -d "seasonal hat"',
599
+ 'complete -c meeb -l flip -d "mirror horizontally"',
600
+ 'complete -c meeb -l fortune -d "random fortune"',
601
+ 'complete -c meeb -l theme -d "color theme" -r -a "neon pastel retro mono matrix"',
602
+ 'complete -c meeb -l motd -d "message of the day"',
603
+ 'complete -c meeb -l pair -d "two animals side by side" -r',
604
+ 'complete -c meeb -l diff -d "compare two animals" -r',
605
+ 'complete -c meeb -l animate -d "terminal animation" -r',
606
+ 'complete -c meeb -l info -d "fun animal fact" -r',
607
+ 'complete -c meeb -l calendar -d "animal of the day"',
608
+ 'complete -c meeb -l commit -d "git commit speech bubble"',
609
+ 'complete -c meeb -l ci -d "markdown-safe output"',
610
+ 'complete -c meeb -l gif -d "save as gif"',
611
+ 'complete -c meeb -l png -d "save as png"',
612
+ 'complete -c meeb -l svg -d "save as svg"',
613
+ 'complete -c meeb -l browse -d "interactive TUI"',
614
+ 'complete -c meeb -l no-color -d "strip ANSI codes"',
615
+ 'complete -c meeb -l install-completions -d "install shell completions"',
616
+ ].join('\n') + '\n'
617
+ fs.writeFileSync(dest, fishContent)
618
+ console.log(` Installed fish completions to ${dest}`)
619
+ } else {
620
+ console.log(` Unsupported shell: ${shell || '(unknown)'}`)
621
+ console.log(` Manual install: copy files from ${completionsDir}`)
622
+ }
623
+ process.exit(0)
624
+ }
625
+
626
+ // --fortune implies --say with a random fortune
627
+ if (flags.fortune && !flags.say) {
628
+ flags.say = getFortune()
629
+ }
630
+
631
+ // --- --motd: compact random animal + fortune for shell startup ---
632
+ if (flags.motd) {
633
+ const animal = getRandomAnimal()
634
+ let art = animal.tiny ? animal.tiny() : animal.render()
635
+ art = addSay(art, getFortune())
636
+ if (flags.theme) {
637
+ const { applyTheme } = require('./theme')
638
+ art = applyTheme(art, flags.theme)
639
+ }
640
+ console.log(output(art))
641
+ process.exit(0)
642
+ }
643
+
644
+ // --- --calendar: deterministic animal of the day ---
645
+ if (flags.calendar) {
646
+ const now = new Date()
647
+ const dayOfYear = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 86400000)
648
+ const keys = Object.keys(visible)
649
+ const pick = keys[dayOfYear % keys.length]
650
+ const animal = visible[pick]
651
+ let art = renderAnimal(animal)
652
+ art = applyPipeline(art)
653
+ const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })
654
+ console.log(output(rgb(215, 119, 87, ` ── ${animal.name} ── `) + rgb(120, 120, 120, dateStr)))
655
+ console.log(output(art))
656
+ process.exit(0)
657
+ }
658
+
659
+ // --- --commit: git post-commit hook helper ---
660
+ if (flags.commit) {
661
+ const { execSync } = require('child_process')
662
+ let msg
663
+ try {
664
+ msg = execSync('git log -1 --pretty=%s 2>/dev/null', { encoding: 'utf8' }).trim()
665
+ } catch {
666
+ msg = 'committed!'
667
+ }
668
+ if (!flags.say) flags.say = msg
669
+ const animal = getRandomAnimal()
670
+ let art = renderAnimal(animal)
671
+ art = applyPipeline(art)
672
+ console.log(output(art))
673
+ process.exit(0)
674
+ }
675
+
676
+ // --- --ci: markdown-compatible output (no ANSI) ---
677
+ if (flags.ci) {
678
+ const animalName = typeof flags.ci === 'string' ? flags.ci : (positional[0] || null)
679
+ const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
680
+ if (!animal) {
681
+ console.error(` unknown animal: "${animalName}"`)
682
+ process.exit(1)
683
+ }
684
+ let art = renderAnimal(animal)
685
+ if (flags.hat) art = addHat(art)
686
+ if (flags.flip) art = flipArt(art)
687
+ if (flags.say) art = addSay(art, flags.say)
688
+ // Output as markdown code block with no ANSI
689
+ console.log('```')
690
+ console.log(stripAnsi(art))
691
+ console.log('```')
692
+ process.exit(0)
693
+ }
694
+
695
+ // --- --info: fun animal fact ---
696
+ if (flags.info) {
697
+ const { getInfo } = require('./info')
698
+ const animalName = typeof flags.info === 'string' ? flags.info : (positional[0] || null)
699
+ const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
700
+ if (!animal) {
701
+ console.error(` unknown animal: "${animalName}"`)
702
+ process.exit(1)
703
+ }
704
+ let art = renderAnimal(animal)
705
+ const fact = getInfo(animalName || Object.keys(animals).find(k => animals[k] === animal) || '')
706
+ if (!flags.say) flags.say = fact
707
+ art = applyPipeline(art)
708
+ console.log(output(rgb(215, 119, 87, ` ── ${animal.name} ──`)))
709
+ console.log(output(art))
710
+ process.exit(0)
711
+ }
712
+
713
+ // --- --pair: two animals side by side ---
714
+ if (flags.pair) {
715
+ const animal1Name = positional[0]
716
+ const animal2Name = typeof flags.pair === 'string' ? flags.pair : null
717
+ if (!animal1Name || !animal2Name) {
718
+ console.error(' Usage: meeb <animal1> --pair <animal2>')
719
+ process.exit(1)
720
+ }
721
+ const a1 = resolveAnimal(animal1Name)
722
+ const a2 = resolveAnimal(animal2Name)
723
+ if (!a1) { console.error(` unknown animal: "${animal1Name}"`); process.exit(1) }
724
+ if (!a2) { console.error(` unknown animal: "${animal2Name}"`); process.exit(1) }
725
+
726
+ let art1 = renderAnimal(a1)
727
+ let art2 = renderAnimal(a2)
728
+ if (flags.hat) { art1 = addHat(art1); art2 = addHat(art2) }
729
+ if (flags.theme) {
730
+ const { applyTheme } = require('./theme')
731
+ art1 = applyTheme(art1, flags.theme)
732
+ art2 = applyTheme(art2, flags.theme)
733
+ }
734
+ const combined = sideBySide(art1, art2, 4)
735
+ console.log(output(rgb(215, 119, 87, ` ── ${a1.name} & ${a2.name} ──`)))
736
+ console.log(output(combined))
737
+ process.exit(0)
738
+ }
739
+
740
+ // --- --diff: compare two animals side by side (with labels) ---
741
+ if (flags.diff) {
742
+ const animal1Name = positional[0]
743
+ const animal2Name = typeof flags.diff === 'string' ? flags.diff : null
744
+ if (!animal1Name || !animal2Name) {
745
+ console.error(' Usage: meeb <animal1> --diff <animal2>')
746
+ process.exit(1)
747
+ }
748
+ const a1 = resolveAnimal(animal1Name)
749
+ const a2 = resolveAnimal(animal2Name)
750
+ if (!a1) { console.error(` unknown animal: "${animal1Name}"`); process.exit(1) }
751
+ if (!a2) { console.error(` unknown animal: "${animal2Name}"`); process.exit(1) }
752
+
753
+ let art1 = renderAnimal(a1)
754
+ let art2 = renderAnimal(a2)
755
+ if (flags.theme) {
756
+ const { applyTheme } = require('./theme')
757
+ art1 = applyTheme(art1, flags.theme)
758
+ art2 = applyTheme(art2, flags.theme)
759
+ }
760
+
761
+ // Add labels above each
762
+ const label1 = rgb(215, 119, 87, ` ${a1.name}`)
763
+ const label2 = rgb(215, 119, 87, ` ${a2.name}`)
764
+ art1 = label1 + '\n' + art1
765
+ art2 = label2 + '\n' + art2
766
+
767
+ const combined = sideBySide(art1, art2, 6)
768
+ console.log(output(combined))
769
+ process.exit(0)
770
+ }
771
+
772
+ // --- --animate: terminal animation ---
773
+ if (flags.animate) {
774
+ const animalName = typeof flags.animate === 'string' ? flags.animate : (positional[0] || null)
775
+ const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
776
+ if (!animal) {
777
+ console.error(` unknown animal: "${animalName}"`)
778
+ process.exit(1)
779
+ }
780
+
781
+ if (!process.stdout.isTTY) {
782
+ console.error(' --animate requires an interactive terminal')
783
+ process.exit(1)
784
+ }
785
+
786
+ const clear = '\x1b[2J\x1b[H'
787
+ const hide = '\x1b[?25l'
788
+ const show = '\x1b[?25h'
789
+
790
+ // Generate frames — normal, eyes closed (replace ◉/● with ─), normal
791
+ const base = renderAnimal(animal)
792
+ const blink = base
793
+ .replace(/◉/g, '─')
794
+ .replace(/●/g, '─')
795
+ .replace(/◆/g, '─')
796
+
797
+ let frames = [base, base, base, blink, base, base]
798
+ if (flags.hat) frames = frames.map(f => addHat(f))
799
+ if (flags.theme) {
800
+ const { applyTheme } = require('./theme')
801
+ frames = frames.map(f => applyTheme(f, flags.theme))
802
+ }
803
+
804
+ const label = rgb(215, 119, 87, ` ── ${animal.name} ──`)
805
+
806
+ process.stdout.write(hide)
807
+ let frame = 0
808
+ const interval = setInterval(() => {
809
+ process.stdout.write(clear + label + '\n' + frames[frame % frames.length] + '\n')
810
+ frame++
811
+ }, 400)
812
+
813
+ // Run for 6 seconds then stop
814
+ setTimeout(() => {
815
+ clearInterval(interval)
816
+ process.stdout.write(show)
817
+ process.exit(0)
818
+ }, 6000)
819
+
820
+ // Clean exit on ctrl-c
821
+ process.on('SIGINT', () => {
822
+ clearInterval(interval)
823
+ process.stdout.write(show + clear)
824
+ process.exit(0)
825
+ })
826
+
827
+ // prevent falling through
828
+ return
829
+ }
830
+
831
+ // --browse launches TUI
832
+ if (cmd === 'browse' || flags.browse) {
833
+ const { browse } = require('./tui')
834
+ browse(visible)
835
+ // browse takes over — no further code runs
836
+ }
837
+
838
+ if (cmd === 'random') {
839
+ const animal = getRandomAnimal()
840
+ let art = renderAnimal(animal)
841
+ art = applyPipeline(art)
842
+ exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
843
+ process.exit(0)
844
+ }
845
+
846
+ if (cmd === 'all') {
847
+ for (const animal of Object.values(visible)) {
848
+ let art = renderAnimal(animal)
849
+ if (flags.hat) art = addHat(art)
850
+ if (flags.flip) art = flipArt(art)
851
+ if (flags.theme) {
852
+ const { applyTheme } = require('./theme')
853
+ art = applyTheme(art, flags.theme)
854
+ }
855
+ exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
856
+ }
857
+ process.exit(0)
858
+ }
859
+
860
+ // no command and no animal? pick random if we have modifier flags, else show help
861
+ if (!cmd) {
862
+ if (Object.keys(flags).length > 0) {
863
+ const keys = Object.keys(animals)
864
+ const pick = keys[Math.floor(Math.random() * keys.length)]
865
+ const animal = animals[pick]
866
+ let art = renderAnimal(animal)
867
+ art = applyPipeline(art)
868
+ exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
869
+ process.exit(0)
870
+ }
871
+ process.exit(0)
872
+ }
873
+
874
+ // try emoji lookup, then fuzzy match
875
+ const resolved = emojiResolve(cmd)
876
+ let animal = animals[resolved || cmd]
877
+ if (!animal) {
878
+ const input = cmd.toLowerCase()
879
+ const names = Object.keys(animals)
880
+
881
+ // try substring match first (handles "croc" → "crocodile", "jelly" → "jellyfish")
882
+ const sub = names.find(n => n.startsWith(input) || input.startsWith(n))
883
+ || names.find(n => n.includes(input) || input.includes(n))
884
+
885
+ if (sub) {
886
+ console.error(` did you mean ${rgb(215, 119, 87, sub)}?`)
887
+ animal = animals[sub]
888
+ } else {
889
+ // fuzzy match via levenshtein
890
+ let best = null, bestD = Infinity
891
+ for (const name of names) {
892
+ const d = levenshtein(input, name)
893
+ if (d < bestD) { bestD = d; best = name }
894
+ }
895
+ if (best && bestD <= Math.max(2, Math.ceil(input.length / 3))) {
896
+ console.error(` did you mean ${rgb(215, 119, 87, best)}?`)
897
+ animal = animals[best]
898
+ } else {
899
+ console.error(` unknown animal: "${cmd}"`)
900
+ console.error(` try: meeb --list`)
901
+ process.exit(1)
902
+ }
903
+ }
904
+ }
905
+
906
+ let art = renderAnimal(animal)
907
+ art = applyPipeline(art)
908
+ exportOrPrint(art)
909
+
910
+ // Easter egg: stretched snail is the meeb logo
911
+ const animalKey = Object.keys(animals).find(k => animals[k] === animal)
912
+ if (animalKey === 'snail' && flags.stretch) {
913
+ console.log('')
914
+ console.log(rgb(215, 119, 87, ' AHA! THE MEEB LOGO! WHAT A WONDERFUL SIGHT!'))
915
+ }