@slugbugblue/trax-cli 0.12.1 → 0.14.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/src/cli.js CHANGED
@@ -1,51 +1,141 @@
1
1
  #!/usr/bin/env node
2
- /* Copyright 2022-2023 Chad Transtrum
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
2
+ /** CLI for Trax, with a fallback REPL
3
+ * @copyright 2022-2026
4
+ * @author Chad Transtrum <chad@transtrum.net>
5
+ * @license Apache-2.0
15
6
  */
16
7
 
17
- // CLI for Trax, with a fallback REPL
18
-
19
8
  import process from 'node:process'
20
9
  import repl from 'node:repl'
21
10
  import YAML from 'yaml'
22
11
  import { Trax } from '@slugbugblue/trax'
23
- import { version as traxVersion } from '@slugbugblue/trax/version'
24
12
  import { version as analystVersion } from '@slugbugblue/trax-analyst'
25
13
  import * as tty from '@slugbugblue/trax-tty'
26
- // Load all the commands as individual plugins
27
- import { analyzeCmd } from './cmds/analyze.js'
28
- import { deleteCmd } from './cmds/delete.js'
29
- import { helpCmd } from './cmds/help.js'
30
- import {
31
- importCmd,
32
- exportCmd,
33
- getFile,
34
- saveFile,
35
- findFiles,
36
- } from './cmds/import-export.js'
37
- import { listCmd } from './cmds/list.js'
38
- import { newCmd } from './cmds/new.js'
39
- import { notesCmd } from './cmds/notes.js'
40
- import { playCmd, tryCmd } from './cmds/play-try.js'
41
- import { puzzlesCmd } from './cmds/puzzles.js'
42
- import { selectCmd } from './cmds/select.js'
43
- import { suggestCmd } from './cmds/suggest.js'
44
- import { undoCmd } from './cmds/undo.js'
45
- import { viewCmd } from './cmds/view.js'
14
+ import { allCmds } from './cmds/index.js'
15
+ import { getFile, saveFile, findFiles } from './cmds/import-export.js'
46
16
  import { version } from './version.js'
47
17
 
48
- // Because typeof [] and null are both 'object'. is([], 'arr') => true
18
+ /**
19
+ * @callback Colorize
20
+ * @param {string} text
21
+ * @param {string | number} [defaultColor]
22
+ * @returns {string}
23
+ */
24
+
25
+ /**
26
+ * @typedef {Colorize & {
27
+ * black: Colorize
28
+ * command: Colorize
29
+ * default: Colorize
30
+ * error: Colorize
31
+ * fatal: Colorize
32
+ * help: Colorize
33
+ * id: Colorize
34
+ * optional: Colorize
35
+ * short: Colorize
36
+ * success: Colorize
37
+ * variable: Colorize
38
+ * white: Colorize
39
+ * }} Colorer
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} GameNote
44
+ * @property {number} move - the move number
45
+ * @property {string} note - the note text
46
+ */
47
+
48
+ /** @typedef {GameNote[]} GameNotes */
49
+
50
+ /** @typedef {'trax' | 'trax8' | 'traxloop'} TraxVariant */
51
+
52
+ /**
53
+ * @typedef {object} GameData
54
+ * @property {string} id - game identifier
55
+ * @property {TraxVariant} rules - game variant
56
+ * @property {string} name - display name of the game variant
57
+ * @property {number} turn - current player (1 or 2)
58
+ * @property {string[]} players - player names [white, black]
59
+ * @property {string} moves - space-separated move notation
60
+ * @property {boolean} [over] - whether the game has ended
61
+ * @property {string} [puzzle] - puzzle ID if this is a puzzle game
62
+ * @property {number} [max] - max moves for puzzle win condition
63
+ * @property {GameNotes} [notes] - annotations
64
+ */
65
+
66
+ /**
67
+ * @typedef {object} PuzzleStats
68
+ * @property {number} attempts - number of times started
69
+ * @property {number} solved - number of times solved
70
+ */
71
+
72
+ /**
73
+ * @typedef {object} Command
74
+ * @property {string[]} alt - alternative names
75
+ * @property {string} args - argument syntax string
76
+ * @property {string[] | Function} comp - tab-completion hints
77
+ * @property {string} desc - short description
78
+ * @property {string[]} help - detailed help lines
79
+ * @property {boolean} [hide] - whether to hide from the help listing
80
+ * @property {Function} fn - command handler
81
+ */
82
+
83
+ /**
84
+ * Plugin definition as supplied by a command module (looser than Command).
85
+ * @typedef {object} PluginDef
86
+ * @property {string} name
87
+ * @property {string | string[]} [alt]
88
+ * @property {string} [args]
89
+ * @property {string | string[] | Function} [comp]
90
+ * @property {string} desc
91
+ * @property {string | string[]} [help]
92
+ * @property {boolean} [hide]
93
+ * @property {string | string[]} [opts]
94
+ * @property {RegExp | RegExp[]} [rx]
95
+ * @property {Function} fn
96
+ */
97
+
98
+ /**
99
+ * @typedef {object} CLIContext
100
+ * @property {Record<string, GameData>} GAMES - all stored games
101
+ * @property {GameData} GAME - currently selected game
102
+ * @property {string} ID - current game ID
103
+ * @property {number} puzzleLevel - current puzzle level
104
+ * @property {import('@slugbugblue/trax').Trax} TRAX - current Trax instance
105
+ * @property {Record<string, number>} COLORS - ANSI color codes by name
106
+ * @property {Colorer} color - colorizer function
107
+ * @property {(command: string) => Command} cmd - get a command object by name
108
+ * @property {string[]} commands - all registered command names
109
+ * @property {(id: string) => void} delete - delete a game by ID
110
+ * @property {(cmd: string, ...args: unknown[]) => void} do - invoke a command
111
+ * @property {(...parts: string[]) => Promise<void>} doNext - execute a command line
112
+ * @property {(text: string) => void} error - print an error to stdout
113
+ * @property {(move: string) => string} fixNotation - normalize move notation
114
+ * @property {(id?: string) => void} load - load a game by ID
115
+ * @property {(rules: string, players: string[], moves: string) => string} newGame - create a new game
116
+ * @property {(puzzle: object) => string} newPuzzle - start a new puzzle game
117
+ * @property {(text: string) => void} out - print to stdout
118
+ * @property {(n: number, noun: string, nouns?: string) => string} plural - pluralize a noun
119
+ * @property {(id: string) => PuzzleStats} puzzle - get puzzle stats
120
+ * @property {(id: string) => void} puzzleSolved - record a puzzle as solved
121
+ * @property {(word?: string) => string | string[] | undefined} resolveCommand - resolve abbreviation
122
+ * @property {() => void} save - persist data to disk
123
+ * @property {(moves: string[]) => void} setGame - update the current game's moves
124
+ * @property {() => void} updateGameData - sync TRAX state into stored game data
125
+ * @property {Function} bubble - tty bubble renderer
126
+ * @property {Function} display - tty board display
127
+ * @property {Function} name - tty name formatter
128
+ * @property {(variable: unknown, type: string) => boolean} is - type checker
129
+ * @property {string} version - CLI version string
130
+ * @property {string} CMD - command currently being evaluated
131
+ * @property {import('node:repl').REPLServer | undefined} repl - active REPL instance
132
+ */
133
+
134
+ /** Check the type of a value, correctly handling arrays and null.
135
+ * @param {unknown} variable - value to check
136
+ * @param {string} type - expected type prefix (e.g. 'str', 'arr', 'func', 'undef')
137
+ * @returns {boolean}
138
+ */
49
139
  const is = (variable, type) =>
50
140
  Object.prototype.toString
51
141
  .call(variable)
@@ -59,10 +149,11 @@ const versionCmd = {
59
149
  opts: ['--version', '-v'],
60
150
  desc: 'display version number',
61
151
  help: 'Print out the current version of this program.',
152
+ /** @param {CLIContext} CLI */
62
153
  fn: (CLI) =>
63
154
  CLI.out(
64
155
  [
65
- `Trax: engine v${traxVersion}`,
156
+ `Trax: engine v${Trax.version}`,
66
157
  `analyst v${analystVersion}`,
67
158
  `tty v${tty.version}`,
68
159
  `cli v${version}`,
@@ -87,26 +178,12 @@ const quitCmd = {
87
178
  },
88
179
  }
89
180
 
90
- const plugins = [
91
- analyzeCmd,
92
- deleteCmd,
93
- exportCmd,
94
- helpCmd,
95
- importCmd,
96
- listCmd,
97
- newCmd,
98
- notesCmd,
99
- playCmd,
100
- puzzlesCmd,
101
- quitCmd,
102
- selectCmd,
103
- suggestCmd,
104
- tryCmd,
105
- undoCmd,
106
- versionCmd,
107
- viewCmd,
108
- ]
181
+ const plugins = [...allCmds, quitCmd, versionCmd]
109
182
 
183
+ /** Generate a new unique game ID, filling gaps from the low end.
184
+ * @param {{ games?: Record<string, GameData> }} DATA
185
+ * @returns {string}
186
+ */
110
187
  const newId = (DATA) => {
111
188
  let max = 0
112
189
  let min = Number.POSITIVE_INFINITY
@@ -133,7 +210,9 @@ const color = (text, defaultColor) => {
133
210
  let quote = false
134
211
  let id = false
135
212
  const colors = [defaultColor]
136
- const c = (t) => tty.color(t, colors.at(-1) || CLI.COLORS.default)
213
+ /** @param {string} [t] @returns {string} */
214
+ const c = (t = '') => tty.color(t, colors.at(-1) || CLI.COLORS.default)
215
+ /** @param {string} t @returns {string} */
137
216
  const short = (t) => tty.color(t, CLI.COLORS.short) + c()
138
217
  for (const char of text) {
139
218
  switch (char) {
@@ -186,7 +265,7 @@ const color = (text, defaultColor) => {
186
265
  }
187
266
 
188
267
  default: {
189
- if (id && !/[\da-z]/.test(char)) {
268
+ if (id && !/[\da-z]/v.test(char)) {
190
269
  id = false
191
270
  colors.pop()
192
271
  out += c(char)
@@ -201,7 +280,9 @@ const color = (text, defaultColor) => {
201
280
  }
202
281
 
203
282
  // Internal representations of files
283
+ /** @type {{ id: string, puzzleLevel: number }} */
204
284
  const CONFIG = { id: '', puzzleLevel: 0 }
285
+ /** @type {{ games: Record<string, GameData>, puzzles: Record<string, PuzzleStats> }} */
205
286
  const DATA = { games: {}, puzzles: {} }
206
287
 
207
288
  // CLI context object for plugins
@@ -247,24 +328,27 @@ const CLI = {
247
328
  // CLI.color('hi') and CLI.color.default('hi') are equivalent
248
329
  color,
249
330
 
331
+ /** @param {string} command @returns {Command} */
250
332
  cmd(command) {
251
333
  // Return a command structure object or an empty object
252
334
  if (is(command, 'str') && command in commands) {
253
335
  return commands[command]
254
336
  }
255
337
 
256
- return {}
338
+ return /** @type {Command} */ ({})
257
339
  },
258
340
 
259
341
  get commands() {
260
342
  return Object.keys(commands)
261
343
  },
262
344
 
345
+ /** @param {string} id */
263
346
  delete(id) {
264
347
  delete DATA.games[id]
265
348
  saveData()
266
349
  },
267
350
 
351
+ /** @param {string} cmd @param {...unknown} args */
268
352
  do(cmd, ...args) {
269
353
  // Call another plugin
270
354
  if (cmd in commands) {
@@ -274,17 +358,20 @@ const CLI = {
274
358
  }
275
359
  },
276
360
 
361
+ /** @param {...string} cmd */
277
362
  async doNext(...cmd) {
278
363
  // Execute another command line
279
364
  await evaluate(cmd.join(' '))
280
365
  },
281
366
 
367
+ /** @param {string} text */
282
368
  error(text) {
283
369
  // Prints an error to stdout
284
370
  CLI.out(CLI.color.error(text))
285
371
  },
286
372
 
287
373
  // Notation manipulation
374
+ /** @param {string} move @returns {string} */
288
375
  fixNotation(move) {
289
376
  if (move.endsWith('b')) return move.slice(0, -1) + '\\'
290
377
  if (move.endsWith('s')) return move.slice(0, -1) + '/'
@@ -292,6 +379,7 @@ const CLI = {
292
379
  return move
293
380
  },
294
381
 
382
+ /** @param {string} [id] */
295
383
  load(id) {
296
384
  id ||= Object.keys(DATA.games || {})[0]
297
385
  if (id && DATA.games?.[id] && DATA.games[id].rules) {
@@ -300,12 +388,18 @@ const CLI = {
300
388
  saveConfig()
301
389
  }
302
390
 
303
- const game = CLI.GAME
391
+ const game = DATA.games[id]
304
392
  game.players ||= ['white', 'black']
305
393
  CLI.TRAX = new Trax(game.rules, game.moves, 'cli')
306
394
  }
307
395
  },
308
396
 
397
+ /**
398
+ * @param {TraxVariant} rules
399
+ * @param {string[]} players
400
+ * @param {string} moves
401
+ * @returns {string}
402
+ */
309
403
  newGame(rules, players, moves) {
310
404
  // Creates a new game
311
405
  const id = newId(DATA)
@@ -328,6 +422,7 @@ const CLI = {
328
422
  return id
329
423
  },
330
424
 
425
+ /** @param {import('@slugbugblue/trax-puzzles').Puzzle} puzzle @returns {string} */
331
426
  newPuzzle(puzzle) {
332
427
  // Starts a new puzzle
333
428
  const id = newId(DATA)
@@ -348,7 +443,7 @@ const CLI = {
348
443
  name: trax.name,
349
444
  turn: trax.turn,
350
445
  max: puzzle.max,
351
- players: trax.turn === 1 ? players : players.reverse(),
446
+ players: trax.turn === 1 ? players : players.toReversed(),
352
447
  moves: trax.moves.join(' '),
353
448
  notes,
354
449
  }
@@ -369,30 +464,40 @@ const CLI = {
369
464
  return id
370
465
  },
371
466
 
467
+ /** @param {string} text */
372
468
  out(text) {
373
469
  // Prints to stdout
374
470
  console.info(text + tty.color(''))
375
471
  },
376
472
 
473
+ /**
474
+ * @param {number} n
475
+ * @param {string} noun
476
+ * @param {string} [nouns]
477
+ * @returns {string}
478
+ */
377
479
  plural(n, noun, nouns) {
378
480
  return String(n) + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
379
481
  },
380
482
 
483
+ /** @param {string} id @returns {PuzzleStats} */
381
484
  puzzle(id) {
382
485
  return DATA.puzzles?.[id] || { attempts: 0, solved: 0 }
383
486
  },
384
487
 
488
+ /** @param {string} id */
385
489
  puzzleSolved(id) {
386
490
  DATA.puzzles ||= {}
387
491
  DATA.puzzles[id] ||= { attempts: 1, solved: 0 }
388
492
  DATA.puzzles[id].solved += 1
389
493
  },
390
494
 
495
+ /** @param {string} [word] @returns {string | string[] | undefined} */
391
496
  resolveCommand(word) {
392
497
  // Determine if an entered word is a valid command.
393
498
  // Returns the command if a single match is found, an array if multiple
394
499
  // commands match, or undefined if nothing matches.
395
- if (is(word, 'str') && word.length > 0) {
500
+ if (word && is(word, 'str')) {
396
501
  return abbreviations[word.toLowerCase()]
397
502
  }
398
503
 
@@ -404,9 +509,10 @@ const CLI = {
404
509
  saveData()
405
510
  },
406
511
 
512
+ /** @param {string[]} moves */
407
513
  setGame(moves) {
408
514
  // Updates the moves for the current game
409
- const game = CLI.GAME
515
+ const game = DATA.games[CLI.ID]
410
516
  const trax = new Trax(game.rules, moves, 'cli')
411
517
  if (game.over || trax.over) game.over = trax.over
412
518
  game.moves = trax.moves.join(' ')
@@ -417,7 +523,7 @@ const CLI = {
417
523
  },
418
524
 
419
525
  updateGameData() {
420
- const data = DATA.games[CLI.GAME.id]
526
+ const data = DATA.games[CLI.ID]
421
527
  const moves = CLI.TRAX.moves.join(' ')
422
528
  if (data.moves !== moves) {
423
529
  data.moves = moves
@@ -434,30 +540,41 @@ const CLI = {
434
540
 
435
541
  is,
436
542
  version,
543
+
544
+ // Dynamically assigned during evaluate() and main()
545
+ CMD: '',
546
+ /** @type {import('node:repl').REPLServer | undefined} */
547
+ repl: undefined,
437
548
  }
438
549
 
439
550
  let startRepl = false
440
551
 
552
+ /** @returns {Promise<typeof CONFIG>} */
441
553
  const getConfig = async () => {
442
554
  await getFile('config', 'trax.yaml', CONFIG, YAML.parse)
443
555
  return CONFIG
444
556
  }
445
557
 
558
+ /** @returns {Promise<typeof DATA>} */
446
559
  const getData = async () => {
447
560
  await getFile('data', 'trax.json', DATA, JSON.parse)
448
561
  return DATA
449
562
  }
450
563
 
564
+ /** @returns {void} */
451
565
  const saveConfig = () => {
452
566
  saveFile('config', 'trax.yaml', YAML.stringify(CONFIG))
453
567
  }
454
568
 
569
+ /** @returns {void} */
455
570
  const saveData = () => {
456
571
  saveFile('data', 'trax.json', JSON.stringify(DATA, null, 2) + '\n')
457
572
  }
458
573
 
574
+ /** @type {Record<string, string>} */
459
575
  const OPTS = {}
460
576
 
577
+ /** @returns {string} */
461
578
  const parseCommandLine = () => {
462
579
  let cmd = ''
463
580
  const args = process.argv.slice(2)
@@ -479,37 +596,61 @@ const parseCommandLine = () => {
479
596
 
480
597
  // Auto-populate the color functions
481
598
  for (const [name, value] of Object.entries(CLI.COLORS)) {
482
- CLI.color[name] = (text) => color(text, value)
599
+ // @ts-ignore dynamic string-keyed assignment onto the Colorer type
600
+ CLI.color[name] = (/** @type {string} */ text) => color(text, value)
483
601
  }
484
602
 
603
+ /**
604
+ * @template T
605
+ * @param {T | T[] | undefined} item
606
+ * @returns {T[]}
607
+ */
485
608
  const forceArray = (item) =>
486
- is(item, 'arr') ? item : is(item, 'undef') ? [] : [item]
609
+ Array.isArray(item) ? item : item === undefined ? [] : [item]
487
610
 
611
+ /** @type {Record<string, string | string[]>} */
488
612
  const abbreviations = {}
613
+ /** @type {Record<string, Command>} */
489
614
  const commands = {}
615
+ /** @type {string[]} */
490
616
  const aliases = []
617
+ /** @type {{rx: RegExp, name: string}[]} */
491
618
  const regexes = []
492
619
 
620
+ /**
621
+ * @param {string[]} abbr
622
+ * @param {string} full
623
+ */
493
624
  const addAbbrev = (abbr, full) => {
494
625
  for (let abbrev of abbr) {
495
626
  while (abbrev.length > 0) {
496
- const abbrSet = abbreviations[abbrev] || new Set()
497
- abbrSet.add(full)
498
- abbreviations[abbrev] = abbrSet
627
+ const existing = abbreviations[abbrev]
628
+ if (!existing) {
629
+ abbreviations[abbrev] = full
630
+ } else if (typeof existing === 'string') {
631
+ if (existing !== full) abbreviations[abbrev] = [existing, full]
632
+ } else if (!existing.includes(full)) {
633
+ existing.push(full)
634
+ }
635
+
499
636
  abbrev = abbrev.slice(0, -1)
500
637
  }
501
638
  }
502
639
  }
503
640
 
641
+ /**
642
+ * @param {string | string[] | Function | undefined} comp
643
+ * @returns {string[] | Function}
644
+ */
504
645
  const makeComp = (comp) => {
505
- if (is(comp, 'arr') || is(comp, 'func')) return comp
506
- if (is(comp, 'str')) return comp.split(' ')
646
+ if (Array.isArray(comp) || typeof comp === 'function') return comp
647
+ if (typeof comp === 'string') return comp.split(' ')
507
648
  return []
508
649
  }
509
650
 
510
651
  const processPlugins = () => {
511
652
  // Process all the plugin command entries
512
- for (const cmd of plugins) {
653
+ for (const cmd of /** @type {PluginDef[]} */ (plugins)) {
513
654
  if (cmd.opts) {
514
655
  for (const opt of forceArray(cmd.opts)) {
515
656
  OPTS[opt] = cmd.name
@@ -541,11 +682,15 @@ const processPlugins = () => {
541
682
  }
542
683
 
543
684
  for (const [abbr, value] of Object.entries(abbreviations)) {
544
- const newValue = [...value]
545
- abbreviations[abbr] = value.size > 1 ? newValue.sort() : newValue[0]
685
+ if (Array.isArray(value)) abbreviations[abbr] = value.toSorted()
546
686
  }
547
687
  }
548
688
 
689
+ /**
690
+ * @param {string[]} completions
691
+ * @param {string} match
692
+ * @returns {string[]}
693
+ */
549
694
  const expand = (completions, match) => {
550
695
  completions = [...completions] // Do not modify in-place
551
696
  if (completions.includes('<cmd>')) {
@@ -590,8 +735,9 @@ const expand = (completions, match) => {
590
735
  return completions
591
736
  }
592
737
 
738
+ /** @param {string} line @returns {[string[], string]} */
593
739
  const completer = (line) => {
594
- const words = line.split(/\s+/).filter(Boolean)
740
+ const words = line.split(/\s+/v).filter(Boolean)
595
741
 
596
742
  let match = words.at(-1) || ''
597
743
  const previous = new Set(words.slice(1, -1).map((s) => s.toLowerCase()))
@@ -600,14 +746,16 @@ const completer = (line) => {
600
746
  match = ''
601
747
  }
602
748
 
749
+ /** @type {string[]} */
603
750
  let completions = []
604
751
  const cmd = abbreviations[String(words[0]).toLowerCase()]
605
752
 
606
- if (is(cmd, 'str') && (line.endsWith(' ') || words.length > 1)) {
607
- completions = commands[cmd].comp
608
- if (is(completions, 'func')) {
609
- completions = completions(CLI, words.slice(1), aliases)
610
- }
753
+ if (typeof cmd === 'string' && (line.endsWith(' ') || words.length > 1)) {
754
+ const { comp } = commands[cmd]
755
+ completions =
756
+ typeof comp === 'function'
757
+ ? /** @type {string[]} */ (comp(CLI, words.slice(1), aliases))
758
+ : comp
611
759
 
612
760
  completions = expand(completions, match)
613
761
  if (words.length === 1 || (words.length === 2 && !line.endsWith(' '))) {
@@ -617,18 +765,12 @@ const completer = (line) => {
617
765
  completions = aliases
618
766
  }
619
767
 
620
- for (let i = 0; i < completions.length; i++) {
621
- const word = completions[i]
622
- if (word.includes('|')) {
623
- const comps = word.split('|')
624
- completions[i] = comps.some((c) => previous.has(c.toLowerCase()))
625
- ? ''
626
- : comps
627
- }
628
- }
629
-
630
768
  completions = completions
631
- .flat()
769
+ .flatMap((word) => {
770
+ if (!word.includes('|')) return word
771
+ const comps = word.split('|')
772
+ return comps.some((c) => previous.has(c.toLowerCase())) ? '' : comps
773
+ })
632
774
  .filter((c) => c && !previous.has(c.toLowerCase()))
633
775
 
634
776
  if (match && completions.length > 0) {
@@ -639,20 +781,17 @@ const completer = (line) => {
639
781
 
640
782
  if (!match && completions.length === 1) completions.push(' ')
641
783
 
642
- completions = completions.map((c) => c + ' ').sort()
784
+ completions = completions.map((c) => c + ' ').toSorted()
643
785
 
644
786
  return [completions, match]
645
787
  }
646
788
 
789
+ /** @param {string[]} cmd @returns {Set<string>} */
647
790
  const ambiguousSuggestions = (cmd) => {
648
- const maybe = new Set()
791
+ const maybe = /** @type {Set<string>} */ (new Set())
649
792
  for (const alias of cmd) {
650
793
  if (alias.startsWith(CLI.CMD)) maybe.add(alias)
651
-
652
- let alts = commands[alias].alt
653
- alts = is(alts, 'arr') ? alts : [alts || '']
654
-
655
- for (const alt of alts) {
794
+ for (const alt of commands[alias].alt) {
656
795
  if (alt.startsWith(CLI.CMD)) maybe.add(alt)
657
796
  }
658
797
  }
@@ -660,30 +799,31 @@ const ambiguousSuggestions = (cmd) => {
660
799
  return maybe
661
800
  }
662
801
 
802
+ /** @param {string} command */
663
803
  const evaluate = async (command) => {
664
- const [CMD, ...words] = command.split(/\s+/).filter(Boolean)
804
+ const [CMD, ...words] = command.split(/\s+/v).filter(Boolean)
665
805
  const cmdLower = CMD?.toLowerCase() || ''
666
806
  CLI.CMD = cmdLower
667
807
  let cmd = abbreviations[cmdLower]
668
808
 
669
809
  // If we are not in a REPL, let's remove hidden commands
670
- if (!CLI.repl && is(cmd, 'arr')) {
810
+ if (!CLI.repl && Array.isArray(cmd)) {
671
811
  cmd = cmd.filter((c) => !commands[c].hide)
672
812
  if (cmd.length === 1) cmd = cmd[0]
673
813
  }
674
814
 
675
- if (is(cmd, 'str')) {
815
+ if (typeof cmd === 'string') {
676
816
  if (words.length > 0 && abbreviations[words[0].toLowerCase()] === 'help') {
677
817
  commands.help.fn(CLI, cmd)
678
818
  } else {
679
819
  await commands[cmd].fn(CLI, ...words)
680
820
  }
681
- } else if (is(cmd, 'arr')) {
821
+ } else if (Array.isArray(cmd)) {
682
822
  CLI.error('Ambiguous command. Perhaps you meant one of the following:')
683
823
  const maybe = ambiguousSuggestions(cmd)
684
824
 
685
825
  if (maybe.size > 0) {
686
- for (const alias of [...maybe].sort()) {
826
+ for (const alias of [...maybe].toSorted()) {
687
827
  tty.outty(CLI.color.command(' ' + alias))
688
828
  }
689
829
  }
@@ -716,11 +856,11 @@ const main = async () => {
716
856
 
717
857
  CLI.repl = repl.start({
718
858
  prompt: 'trax❯ ',
719
- async eval(cmd, ctx, fn, cb) {
859
+ async eval(cmd, _ctx, _fn, cb) {
720
860
  try {
721
861
  await evaluate(cmd)
722
862
  } catch (error) {
723
- tty.outty(error, CLI.COLORS.fatal)
863
+ tty.outty(String(error), CLI.COLORS.fatal)
724
864
  throw error
725
865
  }
726
866