@slugbugblue/trax 0.8.0 → 0.9.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/cli.js +377 -748
- package/src/cmds/delete.js +71 -0
- package/src/cmds/help.js +81 -0
- package/src/cmds/import-export.js +250 -0
- package/src/cmds/list.js +155 -0
- package/src/cmds/new.js +131 -0
- package/src/cmds/play-try.js +129 -0
- package/src/cmds/select.js +47 -0
- package/src/cmds/undo.js +40 -0
- package/src/cmds/view.js +53 -0
- package/src/tty.js +15 -8
- package/src/version.js +1 -1
package/src/cli.js
CHANGED
|
@@ -16,47 +16,31 @@
|
|
|
16
16
|
|
|
17
17
|
// CLI for Trax, with a fallback REPL
|
|
18
18
|
|
|
19
|
-
import fs from 'node:fs/promises'
|
|
20
19
|
import process from 'node:process'
|
|
21
20
|
import repl from 'node:repl'
|
|
22
21
|
|
|
23
|
-
import envPaths from 'env-paths'
|
|
24
|
-
import makeDir from 'make-dir'
|
|
25
22
|
import YAML from 'yaml'
|
|
26
23
|
|
|
27
24
|
import { Trax } from './engine.js'
|
|
28
25
|
import { version } from './version.js'
|
|
29
26
|
import * as tty from './tty.js'
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
command: 33,
|
|
48
|
-
default: 39,
|
|
49
|
-
error: 205,
|
|
50
|
-
fatal: 127,
|
|
51
|
-
help: 74,
|
|
52
|
-
id: 195,
|
|
53
|
-
variable: 123,
|
|
54
|
-
optional: 110,
|
|
55
|
-
short: 153,
|
|
56
|
-
white: 250,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let startRepl = false
|
|
28
|
+
// Load all the commands as individual plugins
|
|
29
|
+
import { deleteCmd } from './cmds/delete.js'
|
|
30
|
+
import { helpCmd } from './cmds/help.js'
|
|
31
|
+
import {
|
|
32
|
+
importCmd,
|
|
33
|
+
exportCmd,
|
|
34
|
+
getFile,
|
|
35
|
+
saveFile,
|
|
36
|
+
findFiles,
|
|
37
|
+
} from './cmds/import-export.js'
|
|
38
|
+
import { listCmd } from './cmds/list.js'
|
|
39
|
+
import { newCmd } from './cmds/new.js'
|
|
40
|
+
import { playCmd, tryCmd } from './cmds/play-try.js'
|
|
41
|
+
import { selectCmd } from './cmds/select.js'
|
|
42
|
+
import { undoCmd } from './cmds/undo.js'
|
|
43
|
+
import { viewCmd } from './cmds/view.js'
|
|
60
44
|
|
|
61
45
|
// Because typeof [] and null are both 'object'. is([], 'arr') => true
|
|
62
46
|
const is = (variable, type) =>
|
|
@@ -66,128 +50,80 @@ const is = (variable, type) =>
|
|
|
66
50
|
.toLowerCase()
|
|
67
51
|
.startsWith(type)
|
|
68
52
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const fsOuch = (fsError) => {
|
|
92
|
-
tty.outty(fsError, COLORS.fatal)
|
|
93
|
-
throw fsError
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Hope this works
|
|
97
|
-
const getFile = async (filetype, filename, variable, decode) => {
|
|
98
|
-
const path = await getPath(filetype)
|
|
99
|
-
if (path) {
|
|
100
|
-
try {
|
|
101
|
-
const contents = await fs.readFile(`${path}/${filename}`, 'utf8')
|
|
102
|
-
for (const [key, value] of Object.entries(decode(contents))) {
|
|
103
|
-
variable[key] = value
|
|
104
|
-
}
|
|
105
|
-
} catch (fsError) {
|
|
106
|
-
if (fsError instanceof TypeError || fsError instanceof SyntaxError) {
|
|
107
|
-
error(`Unable to read ${filetype} file ${path}/${filename}`)
|
|
108
|
-
} else if (fsError.code !== 'ENOENT') {
|
|
109
|
-
fsOuch(fsError)
|
|
53
|
+
// Sample command:
|
|
54
|
+
const versionCmd = {
|
|
55
|
+
name: 'version',
|
|
56
|
+
opts: ['--version', '-v'],
|
|
57
|
+
desc: 'display version number',
|
|
58
|
+
help: 'Print out the current version of this program.',
|
|
59
|
+
fn: (CLI) => CLI.out('Trax CLI v' + version),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const quitCmd = {
|
|
63
|
+
name: 'quit',
|
|
64
|
+
alt: 'exit',
|
|
65
|
+
desc: 'press <Ctrl-D> to exit',
|
|
66
|
+
help: 'To exit, press Ctrl-D on a blank line or type "quit".',
|
|
67
|
+
hide: true,
|
|
68
|
+
fn() {
|
|
69
|
+
// I actually have no idea why a broken promise is required here,
|
|
70
|
+
// but I tried a lot of different options, and this is what worked
|
|
71
|
+
return new Promise(() => {
|
|
72
|
+
if (CLI.repl) {
|
|
73
|
+
CLI.repl.close()
|
|
110
74
|
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return variable
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const getConfig = async () => {
|
|
118
|
-
await getFile('config', 'trax.yaml', CONFIG, YAML.parse)
|
|
119
|
-
return CONFIG
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const getData = async () => {
|
|
123
|
-
await getFile('data', 'trax.json', DATA, JSON.parse)
|
|
124
|
-
return DATA
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const saveFile = (filetype, filename, data) => {
|
|
128
|
-
getPath(filetype).then((path) => {
|
|
129
|
-
if (path) {
|
|
130
|
-
const parts = filename.split('.')
|
|
131
|
-
parts.splice(1, 0, hexy())
|
|
132
|
-
const tmpfile = path + '/' + parts.join('.')
|
|
133
|
-
filename = path + '/' + filename
|
|
134
|
-
fs.writeFile(tmpfile, data)
|
|
135
|
-
.then(() => {
|
|
136
|
-
fs.rm(filename, { force: true, maxRetries: 5 })
|
|
137
|
-
.then(() => {
|
|
138
|
-
fs.rename(tmpfile, filename).catch(fsOuch)
|
|
139
|
-
})
|
|
140
|
-
.catch(fsOuch)
|
|
141
|
-
})
|
|
142
|
-
.catch(fsOuch)
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const saveConfig = () => {
|
|
148
|
-
saveFile('config', 'trax.yaml', YAML.stringify(CONFIG))
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const saveData = () => {
|
|
152
|
-
saveFile('data', 'trax.json', JSON.stringify(DATA, null, 2) + '\n')
|
|
75
|
+
})
|
|
76
|
+
},
|
|
153
77
|
}
|
|
154
78
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
79
|
+
const plugins = [
|
|
80
|
+
deleteCmd,
|
|
81
|
+
exportCmd,
|
|
82
|
+
helpCmd,
|
|
83
|
+
importCmd,
|
|
84
|
+
listCmd,
|
|
85
|
+
newCmd,
|
|
86
|
+
playCmd,
|
|
87
|
+
quitCmd,
|
|
88
|
+
selectCmd,
|
|
89
|
+
tryCmd,
|
|
90
|
+
undoCmd,
|
|
91
|
+
versionCmd,
|
|
92
|
+
viewCmd,
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const newId = (DATA) => {
|
|
96
|
+
let max = 0
|
|
97
|
+
let min = Number.POSITIVE_INFINITY
|
|
98
|
+
if (DATA.games) {
|
|
99
|
+
for (const id of Object.keys(DATA.games)) {
|
|
100
|
+
max = Math.max(max, Number(id) || 0)
|
|
101
|
+
min = Math.min(min, Number(id) || 0)
|
|
171
102
|
}
|
|
103
|
+
} else {
|
|
104
|
+
DATA.games = {}
|
|
172
105
|
}
|
|
173
106
|
|
|
174
|
-
|
|
107
|
+
if (max === 0 || min <= 1) return String(max + 1)
|
|
108
|
+
|
|
109
|
+
return String(min - 1)
|
|
175
110
|
}
|
|
176
111
|
|
|
112
|
+
// Colorize text according to simplistic and not very complete patterns
|
|
177
113
|
const color = (text, def) => {
|
|
178
114
|
let out = ''
|
|
179
115
|
let quote = false
|
|
180
116
|
let id = false
|
|
181
117
|
const colors = [def]
|
|
182
|
-
const c = (t) => tty.color(t, colors[colors.length - 1] || COLORS.default)
|
|
183
|
-
const short = (t) => tty.color(t, COLORS.short) + c()
|
|
118
|
+
const c = (t) => tty.color(t, colors[colors.length - 1] || CLI.COLORS.default)
|
|
119
|
+
const short = (t) => tty.color(t, CLI.COLORS.short) + c()
|
|
184
120
|
for (const char of text) {
|
|
185
121
|
switch (char) {
|
|
186
122
|
case '"':
|
|
187
123
|
if (quote) {
|
|
188
124
|
colors.pop()
|
|
189
125
|
} else {
|
|
190
|
-
colors.push(COLORS.command)
|
|
126
|
+
colors.push(CLI.COLORS.command)
|
|
191
127
|
}
|
|
192
128
|
|
|
193
129
|
out += c('')
|
|
@@ -195,14 +131,14 @@ const color = (text, def) => {
|
|
|
195
131
|
break
|
|
196
132
|
case '#':
|
|
197
133
|
id = true
|
|
198
|
-
colors.push(COLORS.id)
|
|
134
|
+
colors.push(CLI.COLORS.id)
|
|
199
135
|
out += c('#')
|
|
200
136
|
break
|
|
201
137
|
case '|':
|
|
202
138
|
out += short('|')
|
|
203
139
|
break
|
|
204
140
|
case '[':
|
|
205
|
-
colors.push(COLORS.optional)
|
|
141
|
+
colors.push(CLI.COLORS.optional)
|
|
206
142
|
out += short('[')
|
|
207
143
|
break
|
|
208
144
|
case ']':
|
|
@@ -210,7 +146,7 @@ const color = (text, def) => {
|
|
|
210
146
|
out += short(']')
|
|
211
147
|
break
|
|
212
148
|
case '<':
|
|
213
|
-
colors.push(COLORS.variable)
|
|
149
|
+
colors.push(CLI.COLORS.variable)
|
|
214
150
|
out += c('')
|
|
215
151
|
break
|
|
216
152
|
case '>':
|
|
@@ -228,726 +164,423 @@ const color = (text, def) => {
|
|
|
228
164
|
}
|
|
229
165
|
}
|
|
230
166
|
|
|
231
|
-
return tty.color(out, def || COLORS.default)
|
|
167
|
+
return tty.color(out, def || CLI.COLORS.default)
|
|
232
168
|
}
|
|
233
169
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
170
|
+
// Internal representations of files
|
|
171
|
+
const CONFIG = { id: 0 }
|
|
172
|
+
const DATA = { games: {} }
|
|
237
173
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
help: [
|
|
244
|
-
'Delete the current game. Or specify a game number to delete that game.',
|
|
245
|
-
'If the game is in progress, you must type "force" to delete it.',
|
|
246
|
-
],
|
|
247
|
-
},
|
|
248
|
-
export: {
|
|
249
|
-
alt: ['save', 'keep', 'share'],
|
|
250
|
-
args: '[#id] [filename]',
|
|
251
|
-
desc: 'save a game to a file',
|
|
252
|
-
help: [
|
|
253
|
-
'Export the current game to an external ".trx" file for safe-keeping or',
|
|
254
|
-
'to share. Specify a game id to export a different game.',
|
|
255
|
-
],
|
|
256
|
-
},
|
|
257
|
-
help: {
|
|
258
|
-
alt: '?',
|
|
259
|
-
args: '[topic]',
|
|
260
|
-
desc: 'display help for a command',
|
|
261
|
-
help: [
|
|
262
|
-
'Type "help <command>" or "<command> help" to see additional information',
|
|
263
|
-
'for a specific command.',
|
|
264
|
-
],
|
|
265
|
-
},
|
|
266
|
-
import: {
|
|
267
|
-
alt: ['load', 'open'],
|
|
268
|
-
args: '<filename>',
|
|
269
|
-
desc: 'open a game file',
|
|
270
|
-
help: [
|
|
271
|
-
'Load a ".trx" game file into memory. Once imported, it will be assigned',
|
|
272
|
-
'a game id and automatically selected for future commands.',
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
list: {
|
|
276
|
-
alt: 'ls',
|
|
277
|
-
args: '[sort|filter...] ["reverse"]',
|
|
278
|
-
desc: 'list all games',
|
|
279
|
-
help: [
|
|
280
|
-
'Valid sort options are:',
|
|
281
|
-
' <id>: sort by game id',
|
|
282
|
-
' <game>: sort by game type',
|
|
283
|
-
' <player1> or <player2>: sort by player name',
|
|
284
|
-
' <moves>: sort by the number of moves',
|
|
285
|
-
'Available filters:',
|
|
286
|
-
' #id: match a game or a certain number of moves',
|
|
287
|
-
' <active> or <over>: whether a game is in progress or not',
|
|
288
|
-
' <loop> or <8x8> or <trax>: filter by game type',
|
|
289
|
-
" other text: match a player's name",
|
|
290
|
-
'Add the "reverse" option to reverse the sort order.',
|
|
291
|
-
],
|
|
292
|
-
},
|
|
293
|
-
new: {
|
|
294
|
-
alt: 'start',
|
|
295
|
-
args: '[variant] [[p1 name] "vs" [p2 name]]',
|
|
296
|
-
desc: 'start a new game',
|
|
297
|
-
help: [
|
|
298
|
-
'Valid variants are <trax>, <loop>, and <8x8>. Defaults to "trax" if not',
|
|
299
|
-
'specified. Use the "vs" keyword to use specific names for the players.',
|
|
300
|
-
],
|
|
301
|
-
},
|
|
302
|
-
play: {
|
|
303
|
-
alt: ['move', 'mv'],
|
|
304
|
-
args: '<move> [...]',
|
|
305
|
-
desc: 'play a move',
|
|
306
|
-
help: [
|
|
307
|
-
'Enter the move in standard Trax notation. The final character of the',
|
|
308
|
-
'notation can be replaced with a letter for ease of entering at the',
|
|
309
|
-
'command line. Use <s> for "/", <b> for "\\", and <p> for "+".',
|
|
310
|
-
'Multiple moves can be submitted at once. Move numbers are optional,',
|
|
311
|
-
'but if included, they will be checked for accuracy.',
|
|
312
|
-
],
|
|
313
|
-
},
|
|
314
|
-
select: {
|
|
315
|
-
alt: ['choose', '#'],
|
|
316
|
-
args: '#id',
|
|
317
|
-
desc: 'select a different game',
|
|
318
|
-
help: 'Make another game the default game of future commands.',
|
|
174
|
+
// CLI context object for plugins
|
|
175
|
+
const CLI = {
|
|
176
|
+
// Quick access to all stored games
|
|
177
|
+
get GAMES() {
|
|
178
|
+
return DATA.games || {}
|
|
319
179
|
},
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
desc: 'see the effects of a move',
|
|
324
|
-
help: [
|
|
325
|
-
'View a move without committing to it. This is useful for seeing the',
|
|
326
|
-
'effects of forced tiles.',
|
|
327
|
-
],
|
|
180
|
+
// And information about the current game
|
|
181
|
+
get GAME() {
|
|
182
|
+
return (CONFIG.id && DATA.games?.[CONFIG.id]) || {}
|
|
328
183
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
help: [
|
|
333
|
-
'Backtrack one or more moves in a game. Type "undo" to take back the last',
|
|
334
|
-
'move, or "undo 2" to undo the last two moves.',
|
|
335
|
-
],
|
|
184
|
+
// And the current game ID
|
|
185
|
+
get ID() {
|
|
186
|
+
return CONFIG.id
|
|
336
187
|
},
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
188
|
+
|
|
189
|
+
// Provide access to the current Trax object
|
|
190
|
+
TRAX: new Trax(),
|
|
191
|
+
|
|
192
|
+
// Color codes
|
|
193
|
+
COLORS: {
|
|
194
|
+
black: 240,
|
|
195
|
+
command: 33,
|
|
196
|
+
default: 39,
|
|
197
|
+
error: 205,
|
|
198
|
+
fatal: 127,
|
|
199
|
+
help: 74,
|
|
200
|
+
id: 195,
|
|
201
|
+
variable: 123,
|
|
202
|
+
optional: 110,
|
|
203
|
+
short: 153,
|
|
204
|
+
white: 250,
|
|
350
205
|
},
|
|
351
|
-
}
|
|
352
206
|
|
|
353
|
-
|
|
207
|
+
// All of the above COLORS will be populated automatically into "color", where
|
|
208
|
+
// they can be called as functions, to apply the given color, eg:
|
|
209
|
+
// CLI.color('hi') and CLI.color.default('hi') are equivalent
|
|
210
|
+
color,
|
|
354
211
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
212
|
+
cmd(command) {
|
|
213
|
+
// Return a command structure object or an empty object
|
|
214
|
+
if (is(command, 'str') && command in commands) {
|
|
215
|
+
return commands[command]
|
|
216
|
+
}
|
|
361
217
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for (const key of cmds) {
|
|
365
|
-
max = Math.max(max, key.length + length(commands[key].args))
|
|
366
|
-
}
|
|
218
|
+
return {}
|
|
219
|
+
},
|
|
367
220
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const space = ' '.repeat(5 + max - length(topic.args) - key.length)
|
|
372
|
-
tty.outty(
|
|
373
|
-
color(` ${key} ${topic.args || ''}`, COLORS.command) +
|
|
374
|
-
color(space + topic.desc, COLORS.short),
|
|
375
|
-
)
|
|
376
|
-
}
|
|
221
|
+
get commands() {
|
|
222
|
+
return Object.keys(commands)
|
|
223
|
+
},
|
|
377
224
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const alts = is(alt, 'arr') ? alt : [alt]
|
|
383
|
-
for (const alias of alts) {
|
|
384
|
-
tty.outty(color(` ${alias}`, COLORS.command))
|
|
385
|
-
}
|
|
386
|
-
}
|
|
225
|
+
delete(id) {
|
|
226
|
+
delete DATA.games[id]
|
|
227
|
+
saveData()
|
|
228
|
+
},
|
|
387
229
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
230
|
+
do(cmd, ...args) {
|
|
231
|
+
// Call another plugin
|
|
232
|
+
if (cmd in commands) {
|
|
233
|
+
commands[cmd].fn(CLI, ...args)
|
|
391
234
|
} else {
|
|
392
|
-
|
|
393
|
-
tty.outty(' ' + color(line, COLORS.help))
|
|
394
|
-
}
|
|
235
|
+
throw new SyntaxError(`${cmd} is not a valid command`)
|
|
395
236
|
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const newId = () => {
|
|
400
|
-
let max = 0
|
|
401
|
-
let min = Number.POSITIVE_INFINITY
|
|
402
|
-
if (DATA.games) {
|
|
403
|
-
for (const id of Object.keys(DATA.games)) {
|
|
404
|
-
max = Math.max(max, Number(id) || 0)
|
|
405
|
-
min = Math.min(min, Number(id) || 0)
|
|
406
|
-
}
|
|
407
|
-
} else {
|
|
408
|
-
DATA.games = {}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (max === 0 || min <= 1) return String(max + 1)
|
|
237
|
+
},
|
|
412
238
|
|
|
413
|
-
|
|
414
|
-
|
|
239
|
+
async doNext(...cmd) {
|
|
240
|
+
// Execute another command line
|
|
241
|
+
await evaluate(cmd.join(' '))
|
|
242
|
+
},
|
|
415
243
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
error(
|
|
419
|
-
|
|
420
|
-
}
|
|
244
|
+
error(text) {
|
|
245
|
+
// Prints an error to stdout
|
|
246
|
+
CLI.out(CLI.color.error(text))
|
|
247
|
+
},
|
|
421
248
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
249
|
+
newGame(rules, players, moves) {
|
|
250
|
+
// Creates a new game
|
|
251
|
+
const id = newId(DATA)
|
|
252
|
+
const trax = new Trax(rules, moves, 'cli')
|
|
253
|
+
DATA.games[id] = {
|
|
254
|
+
id,
|
|
255
|
+
rules: trax.rules,
|
|
256
|
+
name: trax.name,
|
|
257
|
+
turn: trax.turn,
|
|
258
|
+
players,
|
|
259
|
+
moves: trax.moves.join(' '),
|
|
432
260
|
}
|
|
433
261
|
|
|
434
|
-
|
|
435
|
-
|
|
262
|
+
CONFIG.id = id
|
|
263
|
+
CLI.TRAX = trax
|
|
436
264
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
let players = []
|
|
440
|
-
const moves = []
|
|
441
|
-
for (const line of content.split('\n')) {
|
|
442
|
-
if (!line) continue
|
|
443
|
-
const ln = line.split(';')[0]
|
|
444
|
-
if (!ln) continue
|
|
445
|
-
const lower = ln.toLowerCase()
|
|
446
|
-
if (!game && lower.includes('trax')) {
|
|
447
|
-
game = 'trax'
|
|
448
|
-
if (lower.includes('loop')) game = 'traxloop'
|
|
449
|
-
if (lower.includes('8x8')) game = 'trax8'
|
|
450
|
-
} else if (players.length === 0 && lower.includes(' vs ')) {
|
|
451
|
-
players = ln.split(' ').filter((p) => p)
|
|
452
|
-
} else {
|
|
453
|
-
moves.push(...ln.split(' '))
|
|
454
|
-
}
|
|
455
|
-
}
|
|
265
|
+
saveConfig()
|
|
266
|
+
saveData()
|
|
456
267
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
commands.play.fn(...moves.filter((m) => m))
|
|
460
|
-
} else {
|
|
461
|
-
error(filename + ' does not appear to be formatted correctly.')
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
268
|
+
return id
|
|
269
|
+
},
|
|
465
270
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
271
|
+
out(text) {
|
|
272
|
+
// Prints to stdout
|
|
273
|
+
console.info(text + tty.color(''))
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
resolveCommand(word) {
|
|
277
|
+
// Determine if an entered word is a valid command.
|
|
278
|
+
// Returns the command if a single match is found, an array if multiple
|
|
279
|
+
// commands match, or undefined if nothing matches.
|
|
280
|
+
if (is(word, 'str') && word.length > 0) {
|
|
281
|
+
return abbreviations[word.toLowerCase()]
|
|
474
282
|
}
|
|
475
|
-
}
|
|
476
283
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
284
|
+
return undefined
|
|
285
|
+
},
|
|
480
286
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (filename) filename = filename.replace(/[^ a-z\d./]/g, '')
|
|
490
|
-
if (filename) {
|
|
491
|
-
const fname = filename.split('/').pop()
|
|
492
|
-
if (!fname.includes('.')) filename += '.trx'
|
|
493
|
-
tty.outty(color('Exporting #' + game.id + ' to ') + filename)
|
|
494
|
-
fs.writeFile(filename, content).catch(fsOuch)
|
|
495
|
-
} else {
|
|
496
|
-
tty.outty(content)
|
|
497
|
-
}
|
|
498
|
-
}
|
|
287
|
+
setGame(moves) {
|
|
288
|
+
// Updates the moves for the current game
|
|
289
|
+
const game = CLI.GAME
|
|
290
|
+
const trax = new Trax(game.rules, moves, 'cli')
|
|
291
|
+
if (game.over || trax.over) game.over = trax.over
|
|
292
|
+
game.moves = trax.moves.join(' ')
|
|
293
|
+
game.turn = trax.turn
|
|
294
|
+
CLI.TRAX = trax
|
|
499
295
|
|
|
500
|
-
const updateGameData = () => {
|
|
501
|
-
const data = DATA.games[state.game.id]
|
|
502
|
-
const moves = state.trax.moves.join(' ')
|
|
503
|
-
if (data.moves !== moves) {
|
|
504
|
-
data.moves = moves
|
|
505
|
-
data.turn = state.trax.turn
|
|
506
|
-
if (state.trax.over) data.over = true
|
|
507
296
|
saveData()
|
|
508
|
-
}
|
|
509
|
-
}
|
|
297
|
+
},
|
|
510
298
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (names.includes('vs') || names.includes('VS')) {
|
|
520
|
-
// We don't have a variant, we have player names
|
|
521
|
-
names.unshift(variant)
|
|
522
|
-
} else {
|
|
523
|
-
error('Unknown Trax variant.')
|
|
524
|
-
commands.help.fn('new')
|
|
525
|
-
return
|
|
526
|
-
}
|
|
299
|
+
updateGameData() {
|
|
300
|
+
const data = DATA.games[CLI.GAME.id]
|
|
301
|
+
const moves = CLI.TRAX.moves.join(' ')
|
|
302
|
+
if (data.moves !== moves) {
|
|
303
|
+
data.moves = moves
|
|
304
|
+
data.turn = CLI.TRAX.turn
|
|
305
|
+
if (CLI.TRAX.over) data.over = true
|
|
306
|
+
saveData()
|
|
527
307
|
}
|
|
528
|
-
}
|
|
308
|
+
},
|
|
529
309
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
}
|
|
310
|
+
load(id) {
|
|
311
|
+
if (!id) id = Object.keys(DATA.games || {})[0]
|
|
312
|
+
if (id && DATA.games?.[id] && DATA.games[id].rules) {
|
|
313
|
+
if (id !== String(CONFIG.id)) {
|
|
314
|
+
CONFIG.id = id
|
|
315
|
+
saveConfig()
|
|
316
|
+
}
|
|
539
317
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
name: state.trax.name,
|
|
546
|
-
turn: 1,
|
|
547
|
-
players,
|
|
548
|
-
moves: state.trax.moves.join(' '),
|
|
549
|
-
}
|
|
550
|
-
state.game = DATA.games[CONFIG.id]
|
|
318
|
+
const game = CLI.GAME
|
|
319
|
+
if (!game.players) game.players = ['white', 'black']
|
|
320
|
+
CLI.TRAX = new Trax(game.rules, game.moves, 'cli')
|
|
321
|
+
}
|
|
322
|
+
},
|
|
551
323
|
|
|
552
|
-
|
|
553
|
-
|
|
324
|
+
// Pass through functions
|
|
325
|
+
bubble: tty.bubble,
|
|
326
|
+
display: tty.display,
|
|
327
|
+
name: tty.name,
|
|
554
328
|
|
|
555
|
-
|
|
329
|
+
is,
|
|
330
|
+
version,
|
|
556
331
|
}
|
|
557
332
|
|
|
558
|
-
|
|
559
|
-
if (move.endsWith('b')) return move.slice(0, -1) + '\\'
|
|
560
|
-
if (move.endsWith('s')) return move.slice(0, -1) + '/'
|
|
561
|
-
if (move.endsWith('p')) return move.slice(0, -1) + '+'
|
|
562
|
-
return move
|
|
563
|
-
}
|
|
333
|
+
let startRepl = false
|
|
564
334
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
335
|
+
const getConfig = async () => {
|
|
336
|
+
await getFile('config', 'trax.yaml', CONFIG, YAML.parse)
|
|
337
|
+
return CONFIG
|
|
338
|
+
}
|
|
570
339
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
340
|
+
const getData = async () => {
|
|
341
|
+
await getFile('data', 'trax.json', DATA, JSON.parse)
|
|
342
|
+
return DATA
|
|
343
|
+
}
|
|
574
344
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
345
|
+
const saveConfig = () => {
|
|
346
|
+
saveFile('config', 'trax.yaml', YAML.stringify(CONFIG))
|
|
347
|
+
}
|
|
579
348
|
|
|
580
|
-
|
|
581
|
-
|
|
349
|
+
const saveData = () => {
|
|
350
|
+
saveFile('data', 'trax.json', JSON.stringify(DATA, null, 2) + '\n')
|
|
351
|
+
}
|
|
582
352
|
|
|
583
|
-
|
|
584
|
-
if (notationRx.test(move)) {
|
|
585
|
-
const moveNumber = state.trax.move
|
|
586
|
-
move = fixNotation(move)
|
|
587
|
-
if (checkMove) {
|
|
588
|
-
state.trax.playMove(checkMove, move)
|
|
589
|
-
} else {
|
|
590
|
-
state.trax.play(move)
|
|
591
|
-
}
|
|
353
|
+
const OPTS = {}
|
|
592
354
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
355
|
+
const parseCommandLine = () => {
|
|
356
|
+
let cmd = ''
|
|
357
|
+
const args = process.argv.slice(2)
|
|
358
|
+
let dash = true
|
|
359
|
+
for (const arg of args) {
|
|
360
|
+
if (dash && arg.startsWith('-')) {
|
|
361
|
+
if (arg === '--') dash = false
|
|
362
|
+
if (arg === '-i' || arg === '--interactive') startRepl = true
|
|
363
|
+
if (arg in OPTS) {
|
|
364
|
+
cmd = OPTS[arg] + ' ' + cmd
|
|
600
365
|
}
|
|
601
|
-
} else
|
|
602
|
-
|
|
603
|
-
} else if (move) {
|
|
604
|
-
error('Move "' + move + '" is not in the correct notation.')
|
|
366
|
+
} else {
|
|
367
|
+
cmd += arg + ' '
|
|
605
368
|
}
|
|
606
369
|
}
|
|
607
370
|
|
|
608
|
-
|
|
609
|
-
updateGameData()
|
|
610
|
-
commands.view.fn()
|
|
611
|
-
}
|
|
371
|
+
return cmd.slice(0, -1)
|
|
612
372
|
}
|
|
613
373
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
374
|
+
// Auto-populate the color functions
|
|
375
|
+
for (const [name, value] of Object.entries(CLI.COLORS)) {
|
|
376
|
+
CLI.color[name] = (text) => color(text, value)
|
|
377
|
+
}
|
|
619
378
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
379
|
+
const forceArray = (item) =>
|
|
380
|
+
is(item, 'arr') ? item : is(item, 'undef') ? [] : [item]
|
|
623
381
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
382
|
+
const abbreviations = {}
|
|
383
|
+
const commands = {}
|
|
384
|
+
const aliases = []
|
|
385
|
+
const regexes = []
|
|
628
386
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
387
|
+
const addAbbrev = (abbr, full) => {
|
|
388
|
+
for (let abbrev of abbr) {
|
|
389
|
+
while (abbrev.length > 0) {
|
|
390
|
+
const abbrSet = abbreviations[abbrev] || new Set()
|
|
391
|
+
abbrSet.add(full)
|
|
392
|
+
abbreviations[abbrev] = abbrSet
|
|
393
|
+
abbrev = abbrev.slice(0, -1)
|
|
636
394
|
}
|
|
637
|
-
} else {
|
|
638
|
-
error('Move is invalid.')
|
|
639
395
|
}
|
|
640
396
|
}
|
|
641
397
|
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (!state.game?.id) {
|
|
649
|
-
return error('No active game. Type "new" to start a game.')
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
tty.outty(
|
|
653
|
-
color('#' + CONFIG.id + ' ') +
|
|
654
|
-
tty.name(state.trax.rules) +
|
|
655
|
-
' ' +
|
|
656
|
-
tty.bubble('w', state.game.players[0]) +
|
|
657
|
-
color(' vs ') +
|
|
658
|
-
tty.bubble('b', state.game.players[1]),
|
|
659
|
-
)
|
|
660
|
-
tty.display(state.trax, state.game.players)
|
|
398
|
+
const makeComp = (comp) => {
|
|
399
|
+
if (is(comp, 'arr') || is(comp, 'func')) return comp
|
|
400
|
+
if (is(comp, 'str')) return comp.split(' ')
|
|
401
|
+
return []
|
|
661
402
|
}
|
|
662
403
|
|
|
663
|
-
const
|
|
664
|
-
|
|
404
|
+
const processPlugins = () => {
|
|
405
|
+
// Process all the plugin command entries
|
|
406
|
+
for (const cmd of plugins) {
|
|
407
|
+
if (cmd.opts) {
|
|
408
|
+
for (const opt of forceArray(cmd.opts)) {
|
|
409
|
+
OPTS[opt] = cmd.name
|
|
410
|
+
}
|
|
411
|
+
}
|
|
665
412
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return 0
|
|
672
|
-
}
|
|
413
|
+
if (cmd.rx) {
|
|
414
|
+
for (const rx of forceArray(cmd.rx)) {
|
|
415
|
+
regexes.push({ rx, name: cmd.name })
|
|
416
|
+
}
|
|
417
|
+
}
|
|
673
418
|
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if ('turn'.startsWith(sort)) return 'sturn'
|
|
686
|
-
if ('player'.startsWith(sort) && sort.endsWith('1')) return 'p1'
|
|
687
|
-
if ('player'.startsWith(sort) && sort.endsWith('2')) return 'p2'
|
|
688
|
-
if ('reverse'.startsWith(sort)) return 'reverse'
|
|
689
|
-
return sort
|
|
690
|
-
}
|
|
419
|
+
const entry = {
|
|
420
|
+
alt: forceArray(cmd.alt),
|
|
421
|
+
args: cmd.args || '',
|
|
422
|
+
comp: makeComp(cmd.comp),
|
|
423
|
+
desc: cmd.desc,
|
|
424
|
+
help: forceArray(cmd.help),
|
|
425
|
+
hide: cmd.hide,
|
|
426
|
+
fn: cmd.fn,
|
|
427
|
+
}
|
|
428
|
+
addAbbrev([cmd.name, ...entry.alt], cmd.name)
|
|
429
|
+
commands[cmd.name] = entry
|
|
691
430
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
for (const sort of filters) {
|
|
696
|
-
const by = sortBy(sort)
|
|
697
|
-
if (by === 'reverse') {
|
|
698
|
-
reverse = true
|
|
699
|
-
} else if (list[0]?.[by]) {
|
|
700
|
-
list.sort((a, b) => alphaSort(a[by], b[by]))
|
|
701
|
-
} else if (by === 'trax') {
|
|
702
|
-
list = list.filter((g) => g.rules === by)
|
|
703
|
-
} else if (['over', 'active'].includes(by)) {
|
|
704
|
-
list = list.filter((g) => (by === 'over' ? g.over : !g.over))
|
|
705
|
-
} else {
|
|
706
|
-
list = list.filter(
|
|
707
|
-
(g) =>
|
|
708
|
-
g.id.endsWith(by) ||
|
|
709
|
-
Trax.names[g.rules].toLowerCase().includes(by) ||
|
|
710
|
-
g.p1.toLowerCase().includes(by) ||
|
|
711
|
-
g.p2.toLowerCase().includes(by) ||
|
|
712
|
-
g.moves.startsWith(by + ' '),
|
|
713
|
-
)
|
|
431
|
+
aliases.push(cmd.name)
|
|
432
|
+
for (const alias of entry.alt) {
|
|
433
|
+
aliases.push(alias)
|
|
714
434
|
}
|
|
715
435
|
}
|
|
716
436
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
437
|
+
for (const [abbr, value] of Object.entries(abbreviations)) {
|
|
438
|
+
const newValue = [...value]
|
|
439
|
+
abbreviations[abbr] = value.size > 1 ? newValue.sort() : newValue[0]
|
|
440
|
+
}
|
|
720
441
|
}
|
|
721
442
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
443
|
+
const expand = (completions, match) => {
|
|
444
|
+
completions = [...completions] // Do not modify in-place
|
|
445
|
+
if (completions.includes('<cmd>')) {
|
|
446
|
+
// All commands
|
|
447
|
+
completions[completions.indexOf('<cmd>')] = aliases.join('|')
|
|
725
448
|
}
|
|
726
449
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
p2: 1,
|
|
734
|
-
}
|
|
450
|
+
if (completions.includes('<id>')) {
|
|
451
|
+
// Game numbers
|
|
452
|
+
const ids = []
|
|
453
|
+
for (const game of Object.values(DATA.games || {})) {
|
|
454
|
+
if (game.id) ids.push('#' + game.id)
|
|
455
|
+
}
|
|
735
456
|
|
|
736
|
-
|
|
737
|
-
const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
|
|
738
|
-
list.push({
|
|
739
|
-
sid: String(game.id).padStart(9, '0'),
|
|
740
|
-
id: (game.id === state.game.id ? '* #' : '#') + String(game.id),
|
|
741
|
-
name: game.name?.length || 0,
|
|
742
|
-
rules: game.rules,
|
|
743
|
-
moves: plural(moves, 'move'),
|
|
744
|
-
smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
|
|
745
|
-
sturn: String(game.turn),
|
|
746
|
-
p1: game.players?.[0] || 'white',
|
|
747
|
-
p2: game.players?.[1] || 'black',
|
|
748
|
-
turn: game.turn,
|
|
749
|
-
over: game.over,
|
|
750
|
-
})
|
|
457
|
+
completions[completions.indexOf('<id>')] = ids.join('|')
|
|
751
458
|
}
|
|
752
459
|
|
|
753
|
-
|
|
460
|
+
if (completions.includes('<move>')) {
|
|
461
|
+
// Move numbers
|
|
462
|
+
const moves = []
|
|
463
|
+
for (let move = 1; move <= CLI.TRAX.moves.length; move++) {
|
|
464
|
+
moves.push(String(move))
|
|
465
|
+
}
|
|
754
466
|
|
|
755
|
-
|
|
756
|
-
size.id = Math.max(size.id, game.id.length)
|
|
757
|
-
size.name = Math.max(size.name, game.name)
|
|
758
|
-
size.moves = Math.max(size.moves, game.moves.length)
|
|
759
|
-
size.p1 = Math.max(size.p1, game.p1.length)
|
|
760
|
-
size.p2 = Math.max(size.p2, game.p2.length)
|
|
467
|
+
completions[completions.indexOf('<move>')] = moves.join('|')
|
|
761
468
|
}
|
|
762
469
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
' '.repeat(size.name - game.name + 1) +
|
|
768
|
-
tty.bubble(game.turn === 1 ? 'wh' : 'w', game.p1) +
|
|
769
|
-
color('vs '.padStart(size.p1 - game.p1.length + 4)) +
|
|
770
|
-
tty.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
|
|
771
|
-
color(' '.padStart(size.p2 - game.p2.length + 1)) +
|
|
772
|
-
color(game.over ? 'game over' : game.moves),
|
|
773
|
-
)
|
|
470
|
+
if (completions.includes('<play>')) {
|
|
471
|
+
// Moves
|
|
472
|
+
const moves = CLI.TRAX.possibleMoves()
|
|
473
|
+
completions[completions.indexOf('<play>')] = moves.join('|')
|
|
774
474
|
}
|
|
775
|
-
}
|
|
776
475
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (id !== String(CONFIG.id)) {
|
|
784
|
-
CONFIG.id = id
|
|
785
|
-
saveConfig()
|
|
786
|
-
}
|
|
476
|
+
if (completions.includes('<file>')) {
|
|
477
|
+
// Files ... a bit tricky since we don't want to async in the middle of
|
|
478
|
+
// a completion, but we do want to list all the .trx files in the folder
|
|
479
|
+
// we are looking for ... so we cheat and initiate a search on the first
|
|
480
|
+
// call, and maybe have completions by the second or third call ...
|
|
481
|
+
completions[completions.indexOf('<file>')] = findFiles(match).join('|')
|
|
787
482
|
}
|
|
788
|
-
}
|
|
789
483
|
|
|
790
|
-
|
|
791
|
-
if (!id) {
|
|
792
|
-
error('Missing id.')
|
|
793
|
-
return commands.help.fn('select')
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (id.startsWith('#')) id = id.slice(1)
|
|
797
|
-
load(id)
|
|
798
|
-
if (String(state.game.id) === id) {
|
|
799
|
-
tty.outty(color('Game #' + state.game.id + ' selected.'))
|
|
800
|
-
} else {
|
|
801
|
-
error('Invalid id. Type "list" to see available games.')
|
|
802
|
-
}
|
|
484
|
+
return completions
|
|
803
485
|
}
|
|
804
486
|
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
const games = Object.values(DATA.games).sort((a, b) => {
|
|
808
|
-
// Active games first
|
|
809
|
-
if (a.over && !b.over) return 1
|
|
810
|
-
if (b.over && !a.over) return -1
|
|
811
|
-
// Games with more moves first
|
|
812
|
-
return (b.moves?.length || 0) - (a.moves?.length || 0)
|
|
813
|
-
})
|
|
814
|
-
if (games.length > 0) {
|
|
815
|
-
commands.select.fn(String(games[0].id))
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
487
|
+
const completer = (line) => {
|
|
488
|
+
const words = line.split(/\s+/).filter((w) => w)
|
|
819
489
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if ('
|
|
823
|
-
|
|
824
|
-
|
|
490
|
+
let match = words.slice(-1)[0] || ''
|
|
491
|
+
const previous = new Set(words.slice(1, -1).map((s) => s.toLowerCase()))
|
|
492
|
+
if (line.endsWith(' ')) {
|
|
493
|
+
previous.add(match.toLowerCase())
|
|
494
|
+
match = ''
|
|
825
495
|
}
|
|
826
496
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const game = DATA.games?.[id]
|
|
830
|
-
|
|
831
|
-
if (!game || !String(game.id)) {
|
|
832
|
-
return error('Invalid id. Type "list" to see available games.')
|
|
833
|
-
}
|
|
497
|
+
let completions = []
|
|
498
|
+
const cmd = abbreviations[String(words[0]).toLowerCase()]
|
|
834
499
|
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
500
|
+
if (is(cmd, 'str') && (line.endsWith(' ') || words.length > 1)) {
|
|
501
|
+
completions = commands[cmd].comp
|
|
502
|
+
if (is(completions, 'func')) {
|
|
503
|
+
completions = completions(CLI, words.slice(1), aliases)
|
|
504
|
+
}
|
|
839
505
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
state.trax = new Trax()
|
|
844
|
-
selectAnyGame()
|
|
506
|
+
completions = expand(completions, match)
|
|
507
|
+
if (words.length === 1 || (words.length === 2 && !line.endsWith(' '))) {
|
|
508
|
+
completions.push('help', '?')
|
|
845
509
|
}
|
|
846
510
|
} else {
|
|
847
|
-
|
|
511
|
+
completions = aliases
|
|
848
512
|
}
|
|
849
|
-
}
|
|
850
513
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
514
|
+
for (let i = 0; i < completions.length; i++) {
|
|
515
|
+
const word = completions[i]
|
|
516
|
+
if (word.includes('|')) {
|
|
517
|
+
const comps = word.split('|')
|
|
518
|
+
if (comps.some((c) => previous.has(c.toLowerCase()))) {
|
|
519
|
+
completions[i] = ''
|
|
520
|
+
} else {
|
|
521
|
+
completions[i] = comps
|
|
522
|
+
}
|
|
523
|
+
}
|
|
857
524
|
}
|
|
858
525
|
|
|
859
|
-
|
|
526
|
+
completions = completions
|
|
527
|
+
.flat()
|
|
528
|
+
.filter((c) => c && !previous.has(c.toLowerCase()))
|
|
860
529
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
} else {
|
|
866
|
-
if (state.game.over) state.game.over = false
|
|
867
|
-
state.game.moves = undo.join(' ')
|
|
868
|
-
state.trax = new Trax(state.game.rules, undo, 'cli')
|
|
869
|
-
state.game.turn = state.trax.turn
|
|
870
|
-
saveData()
|
|
871
|
-
commands.view.fn()
|
|
530
|
+
if (match && completions.length > 0) {
|
|
531
|
+
completions = completions.filter((c) => {
|
|
532
|
+
return c.toLowerCase().startsWith(match.toLowerCase())
|
|
533
|
+
})
|
|
872
534
|
}
|
|
873
|
-
}
|
|
874
535
|
|
|
875
|
-
|
|
876
|
-
// I actually have no idea why a broken promise is required here,
|
|
877
|
-
// but I tried a lot of different options, and this is what worked
|
|
878
|
-
return new Promise(() => {
|
|
879
|
-
if (state.repl) {
|
|
880
|
-
state.repl.close()
|
|
881
|
-
}
|
|
882
|
-
})
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const abbreviations = {}
|
|
886
|
-
|
|
887
|
-
const addAbbrev = (abbr, full) => {
|
|
888
|
-
const abbrArray = is(abbr, 'str') ? [abbr] : abbr
|
|
889
|
-
for (let abbrev of abbrArray) {
|
|
890
|
-
while (abbrev.length > 0) {
|
|
891
|
-
const abbrSet = abbreviations[abbrev] || new Set()
|
|
892
|
-
abbrSet.add(full)
|
|
893
|
-
abbreviations[abbrev] = abbrSet
|
|
894
|
-
abbrev = abbrev.slice(0, -1)
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
536
|
+
if (!match && completions.length === 1) completions.push(' ')
|
|
898
537
|
|
|
899
|
-
|
|
900
|
-
for (const key of Object.keys(commands)) {
|
|
901
|
-
addAbbrev(key, key)
|
|
902
|
-
if (commands[key].alt) addAbbrev(commands[key].alt, key)
|
|
903
|
-
}
|
|
538
|
+
completions = completions.map((c) => c + ' ').sort()
|
|
904
539
|
|
|
905
|
-
|
|
906
|
-
const newValue = [...value]
|
|
907
|
-
abbreviations[abbr] = value.size > 1 ? newValue.sort() : newValue[0]
|
|
908
|
-
}
|
|
540
|
+
return [completions, match]
|
|
909
541
|
}
|
|
910
542
|
|
|
911
543
|
const evaluate = async (command) => {
|
|
912
|
-
const [CMD, ...words] = command.split(/\s+/)
|
|
544
|
+
const [CMD, ...words] = command.split(/\s+/).filter((c) => c)
|
|
913
545
|
const cmdLower = CMD.toLowerCase()
|
|
546
|
+
CLI.CMD = cmdLower
|
|
914
547
|
const cmd = abbreviations[cmdLower]
|
|
915
548
|
if (is(cmd, 'str')) {
|
|
916
549
|
if (words.length > 0 && abbreviations[words[0].toLowerCase()] === 'help') {
|
|
917
|
-
commands.help.fn(cmd)
|
|
550
|
+
commands.help.fn(CLI, cmd)
|
|
918
551
|
} else {
|
|
919
|
-
await commands[cmd].fn(...words)
|
|
552
|
+
await commands[cmd].fn(CLI, ...words)
|
|
920
553
|
}
|
|
921
554
|
} else if (is(cmd, 'arr')) {
|
|
922
|
-
error('Ambiguous command. Perhaps you meant one of the following:')
|
|
555
|
+
CLI.error('Ambiguous command. Perhaps you meant one of the following:')
|
|
923
556
|
const maybe = new Set()
|
|
924
557
|
|
|
925
558
|
for (const alias of cmd) {
|
|
926
559
|
if (alias.startsWith(cmdLower)) maybe.add(alias)
|
|
927
560
|
|
|
928
|
-
let
|
|
929
|
-
|
|
561
|
+
let alts = commands[alias].alt
|
|
562
|
+
alts = is(alts, 'arr') ? alts : [alts || '']
|
|
930
563
|
|
|
931
|
-
for (const alt of
|
|
564
|
+
for (const alt of alts) {
|
|
932
565
|
if (alt.startsWith(cmdLower)) maybe.add(alt)
|
|
933
566
|
}
|
|
934
567
|
}
|
|
935
568
|
|
|
936
569
|
if (maybe.size > 0) {
|
|
937
570
|
for (const alias of [...maybe].sort()) {
|
|
938
|
-
tty.outty(color(' ' + alias
|
|
571
|
+
tty.outty(CLI.color.command(' ' + alias))
|
|
939
572
|
}
|
|
940
573
|
}
|
|
941
574
|
} else if (CMD) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
) {
|
|
948
|
-
commands.
|
|
575
|
+
let rxCmd
|
|
576
|
+
for (const regex of regexes) {
|
|
577
|
+
if (regex.rx.test(CMD)) rxCmd = regex.name
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (rxCmd) {
|
|
581
|
+
await commands[rxCmd].fn(CLI, CMD, ...words)
|
|
949
582
|
} else {
|
|
950
|
-
error('Unknown command. Type "help" for a list of valid commands.')
|
|
583
|
+
CLI.error('Unknown command. Type "help" for a list of valid commands.')
|
|
951
584
|
}
|
|
952
585
|
}
|
|
953
586
|
|
|
@@ -955,11 +588,11 @@ const evaluate = async (command) => {
|
|
|
955
588
|
}
|
|
956
589
|
|
|
957
590
|
const main = async () => {
|
|
591
|
+
processPlugins()
|
|
958
592
|
const command = parseCommandLine()
|
|
959
593
|
if (is(command, 'str')) {
|
|
960
594
|
await Promise.all([getConfig(), getData()])
|
|
961
|
-
load(CONFIG.id)
|
|
962
|
-
addAbbreviations()
|
|
595
|
+
CLI.load(CONFIG.id)
|
|
963
596
|
if (command) await evaluate(command)
|
|
964
597
|
if (startRepl || !command) {
|
|
965
598
|
if (!command) {
|
|
@@ -967,25 +600,21 @@ const main = async () => {
|
|
|
967
600
|
tty.outty(color('Type "help" for more information.'))
|
|
968
601
|
}
|
|
969
602
|
|
|
970
|
-
|
|
603
|
+
CLI.repl = repl.start({
|
|
971
604
|
prompt: 'trax❯ ',
|
|
972
605
|
async eval(cmd, ctx, fn, cb) {
|
|
973
606
|
try {
|
|
974
607
|
await evaluate(cmd)
|
|
975
608
|
} catch (error) {
|
|
976
|
-
tty.outty(error, COLORS.fatal)
|
|
609
|
+
tty.outty(error, CLI.COLORS.fatal)
|
|
977
610
|
throw error
|
|
978
611
|
}
|
|
979
612
|
|
|
980
613
|
cb(null, undefined)
|
|
981
614
|
},
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
return ''
|
|
988
|
-
},
|
|
615
|
+
ignoreUndefined: true,
|
|
616
|
+
completer,
|
|
617
|
+
preview: true,
|
|
989
618
|
})
|
|
990
619
|
}
|
|
991
620
|
}
|