@slugbugblue/trax-cli 0.12.0 → 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-2023 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,24 @@ 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 = await fs.mkdir(paths[path], { recursive: true })
51
+ const pathsRecord = /** @type {Record<string, string>} */ (
52
+ /** @type {unknown} */ (paths)
53
+ )
54
+ const realPath = pathsRecord[path]
55
+ await fs.mkdir(realPath, { recursive: true })
52
56
  PATHS[path] = realPath
53
57
  return realPath
54
58
  }
@@ -56,6 +60,7 @@ const getPath = async (path) => {
56
60
  return false
57
61
  }
58
62
 
63
+ /** @returns {string} */
59
64
  const hexy = (length = 6) => {
60
65
  let hex = ''
61
66
  while (hex.length < length) {
@@ -65,6 +70,13 @@ const hexy = (length = 6) => {
65
70
  return hex
66
71
  }
67
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
+ */
68
80
  export const getFile = async (filetype, filename, variable, decode) => {
69
81
  const path = await getPath(filetype)
70
82
  if (path) {
@@ -81,7 +93,9 @@ export const getFile = async (filetype, filename, variable, decode) => {
81
93
  } else {
82
94
  console.log(error)
83
95
  }
84
- } else if (fsError.code !== 'ENOENT') {
96
+ } else if (
97
+ /** @type {NodeJS.ErrnoException} */ (fsError).code !== 'ENOENT'
98
+ ) {
85
99
  fsOuch(fsError)
86
100
  }
87
101
  }
@@ -90,6 +104,12 @@ export const getFile = async (filetype, filename, variable, decode) => {
90
104
  return variable
91
105
  }
92
106
 
107
+ /**
108
+ * @param {string} filetype
109
+ * @param {string} filename
110
+ * @param {string} data
111
+ * @returns {Promise<void>}
112
+ */
93
113
  export const saveFile = async (filetype, filename, data) => {
94
114
  const path = await getPath(filetype)
95
115
  if (path) {
@@ -117,6 +137,36 @@ export const importCmd = {
117
137
  'Load a ".trx" game file into memory. Once imported, it will be assigned',
118
138
  'a game id and automatically selected for future commands.',
119
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
+ },
120
170
  }
121
171
 
122
172
  export const exportCmd = {
@@ -129,29 +179,95 @@ export const exportCmd = {
129
179
  'Export the current game to an external ".trx" file for safe-keeping or',
130
180
  'to share. Specify a game id to export a different game.',
131
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
+ },
132
244
  }
133
245
 
134
246
  /** Interpret the rules string of a .trx file.
135
- * @arg {string} r - rules string in lower case
247
+ * @param {string} r - rules string in lower case
136
248
  * @returns {TraxVariant}
137
249
  */
138
250
  const getRules = (r) =>
139
251
  r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
140
252
 
141
253
  /** Decode a .trx formatted string into a game, preserving players and comments.
142
- * @arg {any} CLI - the CLI object
143
- * @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
144
256
  * @returns {boolean} - true if a game was created
145
257
  */
146
258
  const interpretFile = (CLI, content) => {
259
+ /** @type {TraxVariant | undefined} */
147
260
  let rules
148
261
  let trax
262
+ /** @type {string[]} */
149
263
  let players = []
264
+ /** @type {string[]} */
150
265
  const moves = []
266
+ /** @type {GameNotes} */
151
267
  const comments = []
152
268
  for (const line of content.split('\n')) {
153
269
  if (!line) continue
154
- const [ln, ...comment] = line.split(/[#;]/)
270
+ const [ln, ...comment] = line.split(/[#;]/v)
155
271
  if (ln) {
156
272
  const lower = ln.toLowerCase()
157
273
  if (!rules && lower.includes('trax')) {
@@ -182,7 +298,7 @@ const interpretFile = (CLI, content) => {
182
298
 
183
299
  if (comment.length > 0) {
184
300
  const note = comment.map((c) => c.trim()).join(' ')
185
- if (!/^@slugbugblue\/trax cli\.js v/.test(note)) {
301
+ if (!/^@slugbugblue\/trax cli\.js v/v.test(note)) {
186
302
  comments.push({
187
303
  note,
188
304
  move: trax?.move || 0,
@@ -204,36 +320,9 @@ const interpretFile = (CLI, content) => {
204
320
  return true
205
321
  }
206
322
 
207
- importCmd.fn = async (CLI, filename) => {
208
- cli = CLI
209
- if (!filename) {
210
- CLI.error('Missing filename.')
211
- return CLI.do('help', 'import')
212
- }
213
-
214
- let content
215
- try {
216
- content = await fs.readFile(expandPath(filename), 'utf8')
217
- } catch (fsError) {
218
- if (fsError.code === 'ENOENT' && !filename.endsWith('.trx')) {
219
- try {
220
- content = await fs.readFile(expandPath(filename + '.trx'), 'utf8')
221
- } catch {
222
- content = null
223
- }
224
- }
225
-
226
- if (!content) return CLI.error(fsError.toString())
227
- }
228
-
229
- if (!interpretFile(CLI, content)) {
230
- CLI.error(filename + ' does not appear to be formatted correctly.')
231
- }
232
- }
233
-
234
323
  /** Get all the notes for a certain move.
235
- * @arg {number} move - The move number
236
- * @arg {GameNotes} notes? - The notes for a game
324
+ * @param {number} move - The move number
325
+ * @param {GameNotes} [notes] - The notes for a game
237
326
  * @returns {string} - Notes formatted for this move, or empty string if none
238
327
  */
239
328
  const gameNotes = (move, notes) => {
@@ -247,18 +336,18 @@ const gameNotes = (move, notes) => {
247
336
  )
248
337
  }
249
338
 
250
- /** Join two strings together with a character with the appropriate joining
251
- * character depending on the length of the strings.
252
- * @arg {string} a - the first string
253
- * @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
254
343
  * @returns {string} - both strings correctly joined
255
344
  */
256
345
  const join = (a, b) =>
257
346
  a.length === 0 ? b : a.length + b.length > 78 ? a + '\n' + b : a + ' ' + b
258
347
 
259
348
  /** Interleave the game moves with any notes, formatted for the export file.
260
- * @arg {string[]} moves - an array of move notations
261
- * @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
262
351
  * @returns {string} - A string that can be placed into an export file
263
352
  */
264
353
  const interleaveNotation = (moves, notes) => {
@@ -293,70 +382,10 @@ const interleaveNotation = (moves, notes) => {
293
382
  return content
294
383
  }
295
384
 
296
- exportCmd.fn = async (CLI, id, filename) => {
297
- cli = CLI
298
- let game = CLI.GAME
299
- if (id) {
300
- const newid = id.replace(/^#/, '')
301
- game = CLI.GAMES[newid]
302
- if (!game) {
303
- filename = id
304
- game = CLI.GAME
305
- }
306
- }
307
-
308
- if (!game || !game.id) {
309
- return CLI.error('No active game. Type "new" to start a game.')
310
- }
311
-
312
- const trax = new Trax(game.rules, game.moves, 'cli')
313
-
314
- let content = Trax.names[trax.rules].replace(' ', '') + '\n'
315
- content += game.players?.[0] || 'white'
316
- content += ' vs '
317
- content += game.players?.[1] || 'black'
318
- content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
319
- if (game.puzzle) {
320
- const puzzle = puzzles.find((p) => p.id === game.puzzle)
321
- if (puzzle) {
322
- const source = sources[puzzle.src]
323
- content += '; Puzzle ' + game.puzzle
324
- if (source) {
325
- content += source.copyright
326
- ? ' ©' + source.copyright + ' by '
327
- : ' provided courtesy of '
328
- content += source.name
329
- if (source.url) content += '\n; ' + source.url
330
- if (source.license) {
331
- content += '\n; Licensed under ' + source.license
332
- content += source.licenseUrl ? ' ' + source.licenseUrl : ''
333
- }
334
- }
335
-
336
- content += '\n'
337
- }
338
- }
339
-
340
- content += interleaveNotation(trax.moves, game.notes)
341
-
342
- filename &&= filename.replaceAll(/[^ a-z\d.~/]/g, '')
343
- if (filename) {
344
- const fname = filename.split('/').pop()
345
- if (!fname) filename += game.rules + game.id
346
- if (!fname.includes('.')) filename += '.trx'
347
- CLI.out(CLI.color('Exporting #' + game.id + ' to ') + filename)
348
- try {
349
- fs.writeFile(expandPath(filename), content)
350
- } catch (error) {
351
- fsOuch(error)
352
- }
353
- } else {
354
- CLI.out(content)
355
- }
356
- }
357
-
385
+ /** @type {Record<string, string[]>} */
358
386
  const FOLDERS = {}
359
387
 
388
+ /** @param {string} [path] @returns {string[]} */
360
389
  export const findFiles = (path) => {
361
390
  if (typeof path !== 'string') path = ''
362
391
  let folder = '.'
@@ -371,7 +400,6 @@ export const findFiles = (path) => {
371
400
  if (!FOLDERS[prefix]) {
372
401
  FOLDERS[prefix] = []
373
402
  fs.readdir(folder, { withFileTypes: true })
374
- // eslint-disable-next-line promise/prefer-await-to-then
375
403
  .then((dir) => {
376
404
  // At this point, everything is happening asynchronously,
377
405
  // so we have to call findFiles at least twice before we
@@ -387,7 +415,6 @@ export const findFiles = (path) => {
387
415
 
388
416
  FOLDERS[prefix] = entries
389
417
  })
390
- // eslint-disable-next-line promise/prefer-await-to-then
391
418
  .catch(() => {
392
419
  // Errors are unimportant and should be silently ignored
393
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
- }