@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/.claude/settings.local.json +8 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/eslint.config.js +24 -0
- package/package.json +7 -18
- package/src/cli.js +246 -106
- package/src/cmds/analyze.js +72 -66
- package/src/cmds/delete.js +35 -43
- package/src/cmds/help.js +54 -57
- package/src/cmds/import-export.js +200 -146
- package/src/cmds/index.js +40 -0
- package/src/cmds/list.js +100 -87
- package/src/cmds/new.js +56 -55
- package/src/cmds/notes.js +20 -18
- package/src/cmds/play-try.js +157 -106
- package/src/cmds/puzzles.js +53 -39
- package/src/cmds/select.js +22 -31
- package/src/cmds/suggest.js +28 -37
- package/src/cmds/undo.js +31 -26
- package/src/cmds/view.js +42 -51
- package/src/utils.js +1 -1
- package/src/version.js +1 -1
- package/xo.config.js +10 -0
- package/src/types.d.ts +0 -33
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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,61 @@ 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
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Generate puzzle attribution comment lines for export.
|
|
173
|
+
* @param {CLIContext['GAME']} game - the current game
|
|
174
|
+
* @returns {string} - formatted attribution string, or empty if no puzzle
|
|
175
|
+
*/
|
|
176
|
+
const puzzleAttribution = (game) => {
|
|
177
|
+
if (!game.puzzle) return ''
|
|
178
|
+
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
179
|
+
if (!puzzle) return ''
|
|
180
|
+
const source = sources[puzzle.src]
|
|
181
|
+
let attr = '; Puzzle ' + game.puzzle
|
|
182
|
+
if (source) {
|
|
183
|
+
attr += source.copyright
|
|
184
|
+
? ' ©' + source.copyright + ' by '
|
|
185
|
+
: ' provided courtesy of '
|
|
186
|
+
attr += source.name
|
|
187
|
+
if (source.url) attr += '\n; ' + source.url
|
|
188
|
+
if (source.license) {
|
|
189
|
+
attr += '\n; Licensed under ' + source.license
|
|
190
|
+
attr += source.licenseUrl ? ' ' + source.licenseUrl : ''
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return attr + '\n'
|
|
121
195
|
}
|
|
122
196
|
|
|
123
197
|
export const exportCmd = {
|
|
@@ -130,29 +204,102 @@ export const exportCmd = {
|
|
|
130
204
|
'Export the current game to an external ".trx" file for safe-keeping or',
|
|
131
205
|
'to share. Specify a game id to export a different game.',
|
|
132
206
|
],
|
|
207
|
+
/** @param {CLIContext} CLI @param {string} [id] @param {string} [filename] */
|
|
208
|
+
async fn(CLI, id, filename) {
|
|
209
|
+
cli = CLI
|
|
210
|
+
let game = CLI.GAME
|
|
211
|
+
if (id) {
|
|
212
|
+
const newid = id.replace(/^#/v, '')
|
|
213
|
+
game = CLI.GAMES[newid]
|
|
214
|
+
if (!game) {
|
|
215
|
+
filename = id
|
|
216
|
+
game = CLI.GAME
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!game || !game.id) {
|
|
221
|
+
return CLI.error('No active game. Type "new" to start a game.')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const trax = new Trax(game.rules, game.moves, 'cli')
|
|
225
|
+
|
|
226
|
+
let content = Trax.names[game.rules].replace(' ', '') + '\n'
|
|
227
|
+
content += game.players?.[0] || 'white'
|
|
228
|
+
content += ' vs '
|
|
229
|
+
content += game.players?.[1] || 'black'
|
|
230
|
+
content += '\n; @slugbugblue/trax-cli v' + CLI.version + '\n'
|
|
231
|
+
content += puzzleAttribution(game)
|
|
232
|
+
content += interleaveNotation(trax.moves, game.notes)
|
|
233
|
+
|
|
234
|
+
filename &&= filename.replaceAll(/[^ a-z\d.~\/]/gv, '')
|
|
235
|
+
if (filename) {
|
|
236
|
+
const fname = filename.split('/').pop()
|
|
237
|
+
if (!fname) filename += game.rules + game.id
|
|
238
|
+
if (!fname || !fname.includes('.')) filename += '.trx'
|
|
239
|
+
CLI.out(CLI.color('Exporting #' + game.id + ' to ') + filename)
|
|
240
|
+
try {
|
|
241
|
+
fs.writeFile(expandPath(filename), content)
|
|
242
|
+
} catch (error) {
|
|
243
|
+
fsOuch(error)
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
CLI.out(content)
|
|
247
|
+
}
|
|
248
|
+
},
|
|
133
249
|
}
|
|
134
250
|
|
|
135
251
|
/** Interpret the rules string of a .trx file.
|
|
136
|
-
* @
|
|
252
|
+
* @param {string} r - rules string in lower case
|
|
137
253
|
* @returns {TraxVariant}
|
|
138
254
|
*/
|
|
139
255
|
const getRules = (r) =>
|
|
140
256
|
r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
|
|
141
257
|
|
|
142
|
-
/**
|
|
143
|
-
* @
|
|
144
|
-
* @
|
|
145
|
-
* @
|
|
258
|
+
/** Apply pending moves to the game, or record them as notes if the game is over.
|
|
259
|
+
* @param {string[]} moves - pending move tokens (mutated in place)
|
|
260
|
+
* @param {Trax} trax - the current game instance
|
|
261
|
+
* @param {GameNotes} notes - notes accumulator
|
|
146
262
|
*/
|
|
147
|
-
const
|
|
263
|
+
const applyMoves = (moves, trax, notes) => {
|
|
264
|
+
if (trax.gameOver) {
|
|
265
|
+
notes.push({ note: moves.map((c) => c.trim()).join(' '), move: trax.move })
|
|
266
|
+
} else {
|
|
267
|
+
trax.playMoves(moves.filter(Boolean))
|
|
268
|
+
moves.splice(0)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Record a comment segment as a note, unless it is the generator attribution.
|
|
273
|
+
* @param {string[]} comment - comment tokens from after the # or ; delimiter
|
|
274
|
+
* @param {Trax | undefined} trax - the current game instance, if initialized
|
|
275
|
+
* @param {GameNotes} notes - notes accumulator
|
|
276
|
+
*/
|
|
277
|
+
const collectComment = (comment, trax, notes) => {
|
|
278
|
+
if (comment.length === 0) return
|
|
279
|
+
const note = comment.map((c) => c.trim()).join(' ')
|
|
280
|
+
if (!/^@slugbugblue\/trax-cli v/v.test(note)) {
|
|
281
|
+
notes.push({ note, move: trax?.move || 0 })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Parse a .trx formatted string into structured game data.
|
|
286
|
+
* @param {string} content - the contents of the .trx file
|
|
287
|
+
* @returns {{ rules: TraxVariant, players: string[], trax: Trax, notes: GameNotes } | null} - parsed game data, or null if invalid
|
|
288
|
+
*/
|
|
289
|
+
const parseFile = (content) => {
|
|
290
|
+
/** @type {TraxVariant | undefined} */
|
|
148
291
|
let rules
|
|
292
|
+
/** @type {Trax | undefined} */
|
|
149
293
|
let trax
|
|
294
|
+
/** @type {string[]} */
|
|
150
295
|
let players = []
|
|
296
|
+
/** @type {string[]} */
|
|
151
297
|
const moves = []
|
|
152
|
-
|
|
298
|
+
/** @type {GameNotes} */
|
|
299
|
+
const notes = []
|
|
153
300
|
for (const line of content.split('\n')) {
|
|
154
301
|
if (!line) continue
|
|
155
|
-
const [ln, ...comment] = line.split(/[#;]/)
|
|
302
|
+
const [ln, ...comment] = line.split(/[#;]/v)
|
|
156
303
|
if (ln) {
|
|
157
304
|
const lower = ln.toLowerCase()
|
|
158
305
|
if (!rules && lower.includes('trax')) {
|
|
@@ -169,33 +316,29 @@ const interpretFile = (CLI, content) => {
|
|
|
169
316
|
}
|
|
170
317
|
|
|
171
318
|
if (moves.length > 0) {
|
|
172
|
-
if (!trax) return
|
|
173
|
-
|
|
174
|
-
comments.push({
|
|
175
|
-
note: moves.map((c) => c.trim()).join(' '),
|
|
176
|
-
move: trax.move,
|
|
177
|
-
})
|
|
178
|
-
} else {
|
|
179
|
-
trax.playMoves(moves.filter(Boolean))
|
|
180
|
-
moves.splice(0)
|
|
181
|
-
}
|
|
319
|
+
if (!trax) return null
|
|
320
|
+
applyMoves(moves, trax, notes)
|
|
182
321
|
}
|
|
183
322
|
|
|
184
|
-
|
|
185
|
-
const note = comment.map((c) => c.trim()).join(' ')
|
|
186
|
-
if (!/^@slugbugblue\/trax cli\.js v/.test(note)) {
|
|
187
|
-
comments.push({
|
|
188
|
-
note,
|
|
189
|
-
move: trax?.move || 0,
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
}
|
|
323
|
+
collectComment(comment, trax, notes)
|
|
193
324
|
}
|
|
194
325
|
|
|
195
|
-
if (!trax) return
|
|
326
|
+
if (!trax || !rules) return null
|
|
327
|
+
return { rules, players, trax, notes }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Decode a .trx formatted string into a game, preserving players and comments.
|
|
331
|
+
* @param {CLIContext} CLI - the CLI object
|
|
332
|
+
* @param {string} content - the contents of the .trx file
|
|
333
|
+
* @returns {boolean} - true if a game was created
|
|
334
|
+
*/
|
|
335
|
+
const interpretFile = (CLI, content) => {
|
|
336
|
+
const parsed = parseFile(content)
|
|
337
|
+
if (!parsed) return false
|
|
196
338
|
|
|
339
|
+
const { rules, players, trax, notes } = parsed
|
|
197
340
|
CLI.do('new', rules, ...players)
|
|
198
|
-
CLI.GAMES[String(CLI.GAME.id)].notes =
|
|
341
|
+
CLI.GAMES[String(CLI.GAME.id)].notes = notes
|
|
199
342
|
if (trax.moves.length > 0) {
|
|
200
343
|
CLI.do('play', ...trax.moves)
|
|
201
344
|
} else {
|
|
@@ -205,36 +348,9 @@ const interpretFile = (CLI, content) => {
|
|
|
205
348
|
return true
|
|
206
349
|
}
|
|
207
350
|
|
|
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
351
|
/** Get all the notes for a certain move.
|
|
236
|
-
* @
|
|
237
|
-
* @
|
|
352
|
+
* @param {number} move - The move number
|
|
353
|
+
* @param {GameNotes} [notes] - The notes for a game
|
|
238
354
|
* @returns {string} - Notes formatted for this move, or empty string if none
|
|
239
355
|
*/
|
|
240
356
|
const gameNotes = (move, notes) => {
|
|
@@ -248,18 +364,18 @@ const gameNotes = (move, notes) => {
|
|
|
248
364
|
)
|
|
249
365
|
}
|
|
250
366
|
|
|
251
|
-
/** Join two strings together with
|
|
252
|
-
*
|
|
253
|
-
* @
|
|
254
|
-
* @
|
|
367
|
+
/** Join two strings together with the appropriate joining character
|
|
368
|
+
* depending on the combined length of the strings.
|
|
369
|
+
* @param {string} a - the first string
|
|
370
|
+
* @param {string} b - the second string
|
|
255
371
|
* @returns {string} - both strings correctly joined
|
|
256
372
|
*/
|
|
257
373
|
const join = (a, b) =>
|
|
258
374
|
a.length === 0 ? b : a.length + b.length > 78 ? a + '\n' + b : a + ' ' + b
|
|
259
375
|
|
|
260
376
|
/** Interleave the game moves with any notes, formatted for the export file.
|
|
261
|
-
* @
|
|
262
|
-
* @
|
|
377
|
+
* @param {string[]} moves - an array of move notations
|
|
378
|
+
* @param {GameNotes} [notes] - The notes for a game
|
|
263
379
|
* @returns {string} - A string that can be placed into an export file
|
|
264
380
|
*/
|
|
265
381
|
const interleaveNotation = (moves, notes) => {
|
|
@@ -294,70 +410,10 @@ const interleaveNotation = (moves, notes) => {
|
|
|
294
410
|
return content
|
|
295
411
|
}
|
|
296
412
|
|
|
297
|
-
|
|
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
|
-
|
|
413
|
+
/** @type {Record<string, string[]>} */
|
|
359
414
|
const FOLDERS = {}
|
|
360
415
|
|
|
416
|
+
/** @param {string} [path] @returns {string[]} */
|
|
361
417
|
export const findFiles = (path) => {
|
|
362
418
|
if (typeof path !== 'string') path = ''
|
|
363
419
|
let folder = '.'
|
|
@@ -372,7 +428,6 @@ export const findFiles = (path) => {
|
|
|
372
428
|
if (!FOLDERS[prefix]) {
|
|
373
429
|
FOLDERS[prefix] = []
|
|
374
430
|
fs.readdir(folder, { withFileTypes: true })
|
|
375
|
-
// eslint-disable-next-line promise/prefer-await-to-then
|
|
376
431
|
.then((dir) => {
|
|
377
432
|
// At this point, everything is happening asynchronously,
|
|
378
433
|
// so we have to call findFiles at least twice before we
|
|
@@ -388,7 +443,6 @@ export const findFiles = (path) => {
|
|
|
388
443
|
|
|
389
444
|
FOLDERS[prefix] = entries
|
|
390
445
|
})
|
|
391
|
-
// eslint-disable-next-line promise/prefer-await-to-then
|
|
392
446
|
.catch(() => {
|
|
393
447
|
// Errors are unimportant and should be silently ignored
|
|
394
448
|
})
|
|
@@ -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
|
+
]
|