@slugbugblue/trax-cli 0.12.1 → 0.13.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.
@@ -1,19 +1,14 @@
1
- /* Copyright 2022-2025 Chad Transtrum
2
- *
3
- * Licensed under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License.
5
- * You may obtain a copy of the License at
6
- *
7
- * http://www.apache.org/licenses/LICENSE-2.0
8
- *
9
- * Unless required by applicable law or agreed to in writing, software
10
- * distributed under the License is distributed on an "AS IS" BASIS,
11
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- * See the License for the specific language governing permissions and
13
- * limitations under the License.
1
+ /** CLI import/export commands
2
+ * @copyright 2022-2026
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
14
5
  */
15
6
 
16
- // CLI import/export commands
7
+ /**
8
+ * @typedef {import('../cli.js').CLIContext} CLIContext
9
+ * @typedef {import('../cli.js').TraxVariant} TraxVariant
10
+ * @typedef {import('../cli.js').GameNotes} GameNotes
11
+ */
17
12
 
18
13
  import fs from 'node:fs/promises'
19
14
  import process from 'node:process'
@@ -22,14 +17,17 @@ import { Trax } from '@slugbugblue/trax'
22
17
  import { puzzles, sources } from '@slugbugblue/trax-puzzles'
23
18
 
24
19
  const paths = envPaths('trax', { suffix: '' })
20
+ /** @type {Record<string, string>} */
25
21
  const PATHS = {}
26
22
 
27
23
  // Store a copy of the CLI context object locally
24
+ /** @type {Partial<CLIContext>} */
28
25
  let cli = {}
29
26
 
27
+ /** @param {unknown} fsError */
30
28
  const fsOuch = (fsError) => {
31
29
  if (cli.out) {
32
- cli.out(fsError, cli.COLORS.fatal)
30
+ cli.out(String(fsError))
33
31
  } else {
34
32
  console.log(fsError)
35
33
  }
@@ -37,18 +35,23 @@ const fsOuch = (fsError) => {
37
35
  throw fsError
38
36
  }
39
37
 
38
+ /** @param {string} name @returns {string} */
40
39
  const expandPath = (name) => {
41
40
  // Expand home directory ... anything else?
42
- if (name.includes('~')) name = name.replace('~', process.env.HOME)
41
+ if (name.includes('~')) name = name.replace('~', process.env.HOME ?? '~')
43
42
  return name
44
43
  }
45
44
 
46
45
  // Return the actual path on disk, creating it if necessary.
47
46
  // Path is one of cache, config, data, log, temp.
47
+ /** @param {string} path @returns {Promise<string | false>} */
48
48
  const getPath = async (path) => {
49
49
  if (path in PATHS) return PATHS[path]
50
50
  if (path in paths) {
51
- const realPath = paths[path]
51
+ const pathsRecord = /** @type {Record<string, string>} */ (
52
+ /** @type {unknown} */ (paths)
53
+ )
54
+ const realPath = pathsRecord[path]
52
55
  await fs.mkdir(realPath, { recursive: true })
53
56
  PATHS[path] = realPath
54
57
  return realPath
@@ -57,6 +60,7 @@ const getPath = async (path) => {
57
60
  return false
58
61
  }
59
62
 
63
+ /** @returns {string} */
60
64
  const hexy = (length = 6) => {
61
65
  let hex = ''
62
66
  while (hex.length < length) {
@@ -66,6 +70,13 @@ const hexy = (length = 6) => {
66
70
  return hex
67
71
  }
68
72
 
73
+ /**
74
+ * @param {string} filetype
75
+ * @param {string} filename
76
+ * @param {Record<string, unknown>} variable
77
+ * @param {(s: string) => Record<string, unknown>} decode
78
+ * @returns {Promise<Record<string, unknown>>}
79
+ */
69
80
  export const getFile = async (filetype, filename, variable, decode) => {
70
81
  const path = await getPath(filetype)
71
82
  if (path) {
@@ -82,7 +93,9 @@ export const getFile = async (filetype, filename, variable, decode) => {
82
93
  } else {
83
94
  console.log(error)
84
95
  }
85
- } else if (fsError.code !== 'ENOENT') {
96
+ } else if (
97
+ /** @type {NodeJS.ErrnoException} */ (fsError).code !== 'ENOENT'
98
+ ) {
86
99
  fsOuch(fsError)
87
100
  }
88
101
  }
@@ -91,6 +104,12 @@ export const getFile = async (filetype, filename, variable, decode) => {
91
104
  return variable
92
105
  }
93
106
 
107
+ /**
108
+ * @param {string} filetype
109
+ * @param {string} filename
110
+ * @param {string} data
111
+ * @returns {Promise<void>}
112
+ */
94
113
  export const saveFile = async (filetype, filename, data) => {
95
114
  const path = await getPath(filetype)
96
115
  if (path) {
@@ -118,6 +137,36 @@ export const importCmd = {
118
137
  'Load a ".trx" game file into memory. Once imported, it will be assigned',
119
138
  'a game id and automatically selected for future commands.',
120
139
  ],
140
+ /** @param {CLIContext} CLI @param {string} [filename] */
141
+ async fn(CLI, filename) {
142
+ cli = CLI
143
+ if (!filename) {
144
+ CLI.error('Missing filename.')
145
+ return CLI.do('help', 'import')
146
+ }
147
+
148
+ let content
149
+ try {
150
+ content = await fs.readFile(expandPath(filename), 'utf8')
151
+ } catch (fsError) {
152
+ if (
153
+ /** @type {NodeJS.ErrnoException} */ (fsError).code === 'ENOENT' &&
154
+ !filename.endsWith('.trx')
155
+ ) {
156
+ try {
157
+ content = await fs.readFile(expandPath(filename + '.trx'), 'utf8')
158
+ } catch {
159
+ content = null
160
+ }
161
+ }
162
+
163
+ if (!content) return CLI.error(String(fsError))
164
+ }
165
+
166
+ if (!interpretFile(CLI, content)) {
167
+ CLI.error(filename + ' does not appear to be formatted correctly.')
168
+ }
169
+ },
121
170
  }
122
171
 
123
172
  export const exportCmd = {
@@ -130,29 +179,95 @@ export const exportCmd = {
130
179
  'Export the current game to an external ".trx" file for safe-keeping or',
131
180
  'to share. Specify a game id to export a different game.',
132
181
  ],
182
+ /** @param {CLIContext} CLI @param {string} [id] @param {string} [filename] */
183
+ async fn(CLI, id, filename) {
184
+ cli = CLI
185
+ let game = CLI.GAME
186
+ if (id) {
187
+ const newid = id.replace(/^#/v, '')
188
+ game = CLI.GAMES[newid]
189
+ if (!game) {
190
+ filename = id
191
+ game = CLI.GAME
192
+ }
193
+ }
194
+
195
+ if (!game || !game.id) {
196
+ return CLI.error('No active game. Type "new" to start a game.')
197
+ }
198
+
199
+ const trax = new Trax(game.rules, game.moves, 'cli')
200
+
201
+ let content = Trax.names[game.rules].replace(' ', '') + '\n'
202
+ content += game.players?.[0] || 'white'
203
+ content += ' vs '
204
+ content += game.players?.[1] || 'black'
205
+ content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
206
+ if (game.puzzle) {
207
+ const puzzle = puzzles.find((p) => p.id === game.puzzle)
208
+ if (puzzle) {
209
+ const source = sources[puzzle.src]
210
+ content += '; Puzzle ' + game.puzzle
211
+ if (source) {
212
+ content += source.copyright
213
+ ? ' ©' + source.copyright + ' by '
214
+ : ' provided courtesy of '
215
+ content += source.name
216
+ if (source.url) content += '\n; ' + source.url
217
+ if (source.license) {
218
+ content += '\n; Licensed under ' + source.license
219
+ content += source.licenseUrl ? ' ' + source.licenseUrl : ''
220
+ }
221
+ }
222
+
223
+ content += '\n'
224
+ }
225
+ }
226
+
227
+ content += interleaveNotation(trax.moves, game.notes)
228
+
229
+ filename &&= filename.replaceAll(/[^ a-z\d.~\/]/gv, '')
230
+ if (filename) {
231
+ const fname = filename.split('/').pop()
232
+ if (!fname) filename += game.rules + game.id
233
+ if (!fname || !fname.includes('.')) filename += '.trx'
234
+ CLI.out(CLI.color('Exporting #' + game.id + ' to ') + filename)
235
+ try {
236
+ fs.writeFile(expandPath(filename), content)
237
+ } catch (error) {
238
+ fsOuch(error)
239
+ }
240
+ } else {
241
+ CLI.out(content)
242
+ }
243
+ },
133
244
  }
134
245
 
135
246
  /** Interpret the rules string of a .trx file.
136
- * @arg {string} r - rules string in lower case
247
+ * @param {string} r - rules string in lower case
137
248
  * @returns {TraxVariant}
138
249
  */
139
250
  const getRules = (r) =>
140
251
  r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
141
252
 
142
253
  /** Decode a .trx formatted string into a game, preserving players and comments.
143
- * @arg {any} CLI - the CLI object
144
- * @arg {string} content - the contents of the .trx file
254
+ * @param {CLIContext} CLI - the CLI object
255
+ * @param {string} content - the contents of the .trx file
145
256
  * @returns {boolean} - true if a game was created
146
257
  */
147
258
  const interpretFile = (CLI, content) => {
259
+ /** @type {TraxVariant | undefined} */
148
260
  let rules
149
261
  let trax
262
+ /** @type {string[]} */
150
263
  let players = []
264
+ /** @type {string[]} */
151
265
  const moves = []
266
+ /** @type {GameNotes} */
152
267
  const comments = []
153
268
  for (const line of content.split('\n')) {
154
269
  if (!line) continue
155
- const [ln, ...comment] = line.split(/[#;]/)
270
+ const [ln, ...comment] = line.split(/[#;]/v)
156
271
  if (ln) {
157
272
  const lower = ln.toLowerCase()
158
273
  if (!rules && lower.includes('trax')) {
@@ -183,7 +298,7 @@ const interpretFile = (CLI, content) => {
183
298
 
184
299
  if (comment.length > 0) {
185
300
  const note = comment.map((c) => c.trim()).join(' ')
186
- if (!/^@slugbugblue\/trax cli\.js v/.test(note)) {
301
+ if (!/^@slugbugblue\/trax cli\.js v/v.test(note)) {
187
302
  comments.push({
188
303
  note,
189
304
  move: trax?.move || 0,
@@ -205,36 +320,9 @@ const interpretFile = (CLI, content) => {
205
320
  return true
206
321
  }
207
322
 
208
- importCmd.fn = async (CLI, filename) => {
209
- cli = CLI
210
- if (!filename) {
211
- CLI.error('Missing filename.')
212
- return CLI.do('help', 'import')
213
- }
214
-
215
- let content
216
- try {
217
- content = await fs.readFile(expandPath(filename), 'utf8')
218
- } catch (fsError) {
219
- if (fsError.code === 'ENOENT' && !filename.endsWith('.trx')) {
220
- try {
221
- content = await fs.readFile(expandPath(filename + '.trx'), 'utf8')
222
- } catch {
223
- content = null
224
- }
225
- }
226
-
227
- if (!content) return CLI.error(fsError.toString())
228
- }
229
-
230
- if (!interpretFile(CLI, content)) {
231
- CLI.error(filename + ' does not appear to be formatted correctly.')
232
- }
233
- }
234
-
235
323
  /** Get all the notes for a certain move.
236
- * @arg {number} move - The move number
237
- * @arg {GameNotes} notes? - The notes for a game
324
+ * @param {number} move - The move number
325
+ * @param {GameNotes} [notes] - The notes for a game
238
326
  * @returns {string} - Notes formatted for this move, or empty string if none
239
327
  */
240
328
  const gameNotes = (move, notes) => {
@@ -248,18 +336,18 @@ const gameNotes = (move, notes) => {
248
336
  )
249
337
  }
250
338
 
251
- /** Join two strings together with a character with the appropriate joining
252
- * character depending on the length of the strings.
253
- * @arg {string} a - the first string
254
- * @arg {string} b - the second string
339
+ /** Join two strings together with the appropriate joining character
340
+ * depending on the combined length of the strings.
341
+ * @param {string} a - the first string
342
+ * @param {string} b - the second string
255
343
  * @returns {string} - both strings correctly joined
256
344
  */
257
345
  const join = (a, b) =>
258
346
  a.length === 0 ? b : a.length + b.length > 78 ? a + '\n' + b : a + ' ' + b
259
347
 
260
348
  /** Interleave the game moves with any notes, formatted for the export file.
261
- * @arg {string[]} moves - an array of move notations
262
- * @arg {GameNotes} notes? - The notes for a game
349
+ * @param {string[]} moves - an array of move notations
350
+ * @param {GameNotes} [notes] - The notes for a game
263
351
  * @returns {string} - A string that can be placed into an export file
264
352
  */
265
353
  const interleaveNotation = (moves, notes) => {
@@ -294,70 +382,10 @@ const interleaveNotation = (moves, notes) => {
294
382
  return content
295
383
  }
296
384
 
297
- exportCmd.fn = async (CLI, id, filename) => {
298
- cli = CLI
299
- let game = CLI.GAME
300
- if (id) {
301
- const newid = id.replace(/^#/, '')
302
- game = CLI.GAMES[newid]
303
- if (!game) {
304
- filename = id
305
- game = CLI.GAME
306
- }
307
- }
308
-
309
- if (!game || !game.id) {
310
- return CLI.error('No active game. Type "new" to start a game.')
311
- }
312
-
313
- const trax = new Trax(game.rules, game.moves, 'cli')
314
-
315
- let content = Trax.names[trax.rules].replace(' ', '') + '\n'
316
- content += game.players?.[0] || 'white'
317
- content += ' vs '
318
- content += game.players?.[1] || 'black'
319
- content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
320
- if (game.puzzle) {
321
- const puzzle = puzzles.find((p) => p.id === game.puzzle)
322
- if (puzzle) {
323
- const source = sources[puzzle.src]
324
- content += '; Puzzle ' + game.puzzle
325
- if (source) {
326
- content += source.copyright
327
- ? ' ©' + source.copyright + ' by '
328
- : ' provided courtesy of '
329
- content += source.name
330
- if (source.url) content += '\n; ' + source.url
331
- if (source.license) {
332
- content += '\n; Licensed under ' + source.license
333
- content += source.licenseUrl ? ' ' + source.licenseUrl : ''
334
- }
335
- }
336
-
337
- content += '\n'
338
- }
339
- }
340
-
341
- content += interleaveNotation(trax.moves, game.notes)
342
-
343
- filename &&= filename.replaceAll(/[^ a-z\d.~/]/g, '')
344
- if (filename) {
345
- const fname = filename.split('/').pop()
346
- if (!fname) filename += game.rules + game.id
347
- if (!fname.includes('.')) filename += '.trx'
348
- CLI.out(CLI.color('Exporting #' + game.id + ' to ') + filename)
349
- try {
350
- fs.writeFile(expandPath(filename), content)
351
- } catch (error) {
352
- fsOuch(error)
353
- }
354
- } else {
355
- CLI.out(content)
356
- }
357
- }
358
-
385
+ /** @type {Record<string, string[]>} */
359
386
  const FOLDERS = {}
360
387
 
388
+ /** @param {string} [path] @returns {string[]} */
361
389
  export const findFiles = (path) => {
362
390
  if (typeof path !== 'string') path = ''
363
391
  let folder = '.'
@@ -372,7 +400,6 @@ export const findFiles = (path) => {
372
400
  if (!FOLDERS[prefix]) {
373
401
  FOLDERS[prefix] = []
374
402
  fs.readdir(folder, { withFileTypes: true })
375
- // eslint-disable-next-line promise/prefer-await-to-then
376
403
  .then((dir) => {
377
404
  // At this point, everything is happening asynchronously,
378
405
  // so we have to call findFiles at least twice before we
@@ -388,7 +415,6 @@ export const findFiles = (path) => {
388
415
 
389
416
  FOLDERS[prefix] = entries
390
417
  })
391
- // eslint-disable-next-line promise/prefer-await-to-then
392
418
  .catch(() => {
393
419
  // Errors are unimportant and should be silently ignored
394
420
  })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @copyright 2025-2026
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
5
+ */
6
+
7
+ // Load all the commands here and put them in a list.
8
+ // This just keeps the main program a tiny bit cleaner.
9
+
10
+ import { analyzeCmd } from './analyze.js'
11
+ import { deleteCmd } from './delete.js'
12
+ import { helpCmd } from './help.js'
13
+ import { importCmd, exportCmd } from './import-export.js'
14
+ import { listCmd } from './list.js'
15
+ import { newCmd } from './new.js'
16
+ import { notesCmd } from './notes.js'
17
+ import { playCmd, tryCmd } from './play-try.js'
18
+ import { puzzlesCmd } from './puzzles.js'
19
+ import { selectCmd } from './select.js'
20
+ import { suggestCmd } from './suggest.js'
21
+ import { undoCmd } from './undo.js'
22
+ import { viewCmd } from './view.js'
23
+
24
+ export const allCmds = [
25
+ analyzeCmd,
26
+ deleteCmd,
27
+ helpCmd,
28
+ importCmd,
29
+ exportCmd,
30
+ listCmd,
31
+ newCmd,
32
+ notesCmd,
33
+ playCmd,
34
+ tryCmd,
35
+ puzzlesCmd,
36
+ selectCmd,
37
+ suggestCmd,
38
+ undoCmd,
39
+ viewCmd,
40
+ ]
package/src/cmds/list.js CHANGED
@@ -1,19 +1,10 @@
1
- /* Copyright 2022 Chad Transtrum
2
- *
3
- * Licensed under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License.
5
- * You may obtain a copy of the License at
6
- *
7
- * http://www.apache.org/licenses/LICENSE-2.0
8
- *
9
- * Unless required by applicable law or agreed to in writing, software
10
- * distributed under the License is distributed on an "AS IS" BASIS,
11
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- * See the License for the specific language governing permissions and
13
- * limitations under the License.
1
+ /** CLI list command
2
+ * @copyright 2022-2026
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
14
5
  */
15
6
 
16
- // CLI list command
7
+ /** @typedef {import('../cli.js').CLIContext} CLIContext */
17
8
 
18
9
  import process from 'node:process'
19
10
  import { Trax } from '@slugbugblue/trax'
@@ -39,8 +30,75 @@ export const listCmd = {
39
30
  " <other text>: match a player's name or the latest game note",
40
31
  'Add the "reverse" option to reverse the sort order.',
41
32
  ],
33
+ /** @param {CLIContext} CLI @param {...string} filters */
34
+ fn(CLI, ...filters) {
35
+ if (Object.keys(CLI.GAMES).length === 0) {
36
+ return CLI.error('No games. Type "new" to start a new game.')
37
+ }
38
+
39
+ const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
40
+ if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
41
+
42
+ /** @type {Array<Record<string, string | number | boolean | undefined>>} */
43
+ let list = []
44
+ const size = {
45
+ spacing: 13, // Amount of space taken up by fixed-length and between columns
46
+ id: 1,
47
+ name: 1,
48
+ moves: 1,
49
+ p1: 1,
50
+ p2: 1,
51
+ }
52
+ let noteSize = Number.POSITIVE_INFINITY
53
+
54
+ for (const game of Object.values(CLI.GAMES)) {
55
+ const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
56
+ list.push({
57
+ sid: String(game.id).padStart(9, '0'),
58
+ id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
59
+ name: game.name?.length || 0,
60
+ rules: game.rules,
61
+ moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
62
+ smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
63
+ sturn: String(game.turn),
64
+ p1: game.players?.[0] || 'white',
65
+ p2: game.players?.[1] || 'black',
66
+ turn: game.turn,
67
+ over: game.over,
68
+ note: game.notes?.at(-1)?.note || '',
69
+ })
70
+ }
71
+
72
+ list = listSort(list, filters)
73
+
74
+ for (const game of list) {
75
+ size.id = Math.max(size.id, String(game.id).length)
76
+ size.name = Math.max(size.name, Number(game.name))
77
+ size.moves = Math.max(size.moves, String(game.moves).length)
78
+ size.p1 = Math.max(size.p1, String(game.p1).length)
79
+ size.p2 = Math.max(size.p2, String(game.p2).length)
80
+ noteSize = Math.min(noteSize, leftover(size))
81
+ }
82
+
83
+ for (const game of list) {
84
+ CLI.out(
85
+ CLI.color(String(game.id).padStart(size.id) + ' ') +
86
+ Trax.names[
87
+ /** @type {import('../cli.js').TraxVariant} */ (String(game.rules))
88
+ ] +
89
+ ' '.repeat(size.name - Number(game.name) + 1) +
90
+ CLI.bubble(game.turn === 1 ? 'wh' : 'w', String(game.p1)) +
91
+ CLI.color('vs '.padStart(size.p1 - String(game.p1).length + 4)) +
92
+ CLI.bubble(game.turn === 2 ? 'bh' : 'b', String(game.p2)) +
93
+ CLI.color(' '.padStart(size.p2 - String(game.p2).length + 1)) +
94
+ CLI.color(String(game.moves).padEnd(size.moves)) +
95
+ CLI.color.help(' ' + String(game.note).slice(0, noteSize)),
96
+ )
97
+ }
98
+ },
42
99
  }
43
100
 
101
+ /** @param {string} a @param {string} b @returns {number} */
44
102
  const alphaSort = (a, b) => {
45
103
  a = String(a).toLowerCase()
46
104
  b = String(b).toLowerCase()
@@ -49,6 +107,7 @@ const alphaSort = (a, b) => {
49
107
  return 0
50
108
  }
51
109
 
110
+ /** @param {string | undefined} sort @returns {string} */
52
111
  const sortBy = (sort) => {
53
112
  if (!sort) return 'id'
54
113
  sort = sort.toLowerCase()
@@ -67,6 +126,11 @@ const sortBy = (sort) => {
67
126
  return sort
68
127
  }
69
128
 
129
+ /**
130
+ * @param {Array<Record<string, string | number | boolean | undefined>>} list
131
+ * @param {string[]} filters
132
+ * @returns {Array<Record<string, string | number | boolean | undefined>>}
133
+ */
70
134
  const listSort = (list, filters) => {
71
135
  if (list.length === 0) return list
72
136
  let reverse = false
@@ -75,7 +139,7 @@ const listSort = (list, filters) => {
75
139
  if (by === 'reverse') {
76
140
  reverse = true
77
141
  } else if (list[0]?.[by]) {
78
- list.sort((a, b) => alphaSort(a[by], b[by]))
142
+ list.sort((a, b) => alphaSort(String(a[by]), String(b[by])))
79
143
  } else if (by === 'trax') {
80
144
  list = list.filter((g) => g.rules === by)
81
145
  } else if (['over', 'active'].includes(by)) {
@@ -83,12 +147,16 @@ const listSort = (list, filters) => {
83
147
  } else {
84
148
  list = list.filter((g) => {
85
149
  return (
86
- g.id.endsWith(by) ||
87
- Trax.names[g.rules].toLowerCase().includes(by) ||
88
- g.p1.toLowerCase().includes(by) ||
89
- g.p2.toLowerCase().includes(by) ||
90
- g.moves.startsWith(by + ' ') ||
91
- g.note.includes(by)
150
+ String(g.id).endsWith(by) ||
151
+ Trax.names[
152
+ /** @type {import('../cli.js').TraxVariant} */ (String(g.rules))
153
+ ]
154
+ .toLowerCase()
155
+ .includes(by) ||
156
+ String(g.p1).toLowerCase().includes(by) ||
157
+ String(g.p2).toLowerCase().includes(by) ||
158
+ String(g.moves).startsWith(by + ' ') ||
159
+ String(g.note).includes(by)
92
160
  )
93
161
  })
94
162
  }
@@ -99,9 +167,9 @@ const listSort = (list, filters) => {
99
167
  return list
100
168
  }
101
169
 
102
- /** Find how many columns are left over after all the other columns arefilled.
103
- * @arg {Record<string, number>} sizes
104
- * @returns {number} number of columns left over
170
+ /** Find how many columns are left over after all the other columns are filled.
171
+ * @param {Record<string, number>} sizes
172
+ * @returns {number}
105
173
  */
106
174
  const leftover = (sizes) => {
107
175
  let left = process.stdout.columns
@@ -111,66 +179,3 @@ const leftover = (sizes) => {
111
179
 
112
180
  return left
113
181
  }
114
-
115
- listCmd.fn = (CLI, ...filters) => {
116
- if (Object.keys(CLI.GAMES).length === 0) {
117
- return CLI.error('No games. Type "new" to start a new game.')
118
- }
119
-
120
- const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
121
- if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
122
-
123
- let list = []
124
- const size = {
125
- spacing: 13, // Amount of space taken up by fixed-length and between columns
126
- id: 1,
127
- name: 1,
128
- moves: 1,
129
- p1: 1,
130
- p2: 1,
131
- }
132
- let noteSize = Number.POSITIVE_INFINITY
133
-
134
- for (const game of Object.values(CLI.GAMES)) {
135
- const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
136
- list.push({
137
- sid: String(game.id).padStart(9, '0'),
138
- id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
139
- name: game.name?.length || 0,
140
- rules: game.rules,
141
- moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
142
- smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
143
- sturn: String(game.turn),
144
- p1: game.players?.[0] || 'white',
145
- p2: game.players?.[1] || 'black',
146
- turn: game.turn,
147
- over: game.over,
148
- note: game.notes?.[game.notes.length - 1]?.note || '',
149
- })
150
- }
151
-
152
- list = listSort(list, filters)
153
-
154
- for (const game of list) {
155
- size.id = Math.max(size.id, game.id.length)
156
- size.name = Math.max(size.name, game.name)
157
- size.moves = Math.max(size.moves, game.moves.length)
158
- size.p1 = Math.max(size.p1, game.p1.length)
159
- size.p2 = Math.max(size.p2, game.p2.length)
160
- noteSize = Math.min(noteSize, leftover(size))
161
- }
162
-
163
- for (const game of list) {
164
- CLI.out(
165
- CLI.color(game.id.padStart(size.id) + ' ') +
166
- Trax.names[game.rules] +
167
- ' '.repeat(size.name - game.name + 1) +
168
- CLI.bubble(game.turn === 1 ? 'wh' : 'w', game.p1) +
169
- CLI.color('vs '.padStart(size.p1 - game.p1.length + 4)) +
170
- CLI.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
171
- CLI.color(' '.padStart(size.p2 - game.p2.length + 1)) +
172
- CLI.color(game.moves.padEnd(size.moves)) +
173
- CLI.color.help(' ' + game.note.slice(0, noteSize)),
174
- )
175
- }
176
- }