casualos 3.5.3-alpha.16326443512 → 3.5.4

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.
Files changed (4) hide show
  1. package/cli.js +479 -39
  2. package/cli.js.map +1 -1
  3. package/dist/cli.js +643 -88
  4. package/package.json +4 -4
package/cli.js CHANGED
@@ -24,7 +24,7 @@ import repl from 'node:repl';
24
24
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
25
  // @ts-ignore
26
26
  import Conf from 'conf';
27
- import { getBotsStateFromStoredAux, getSessionKeyExpiration, isExpired, parseSessionKey, willExpire, } from '@casual-simulation/aux-common';
27
+ import { calculateStringTagValue, createBot, DATE_TAG_PREFIX, DNA_TAG_PREFIX, getBotsStateFromStoredAux, getSessionKeyExpiration, getUploadState, hasValue, isExpired, LIBRARY_SCRIPT_PREFIX, merge, NUMBER_TAG_PREFIX, parseSessionKey, ROTATION_TAG_PREFIX, STRING_TAG_PREFIX, VECTOR_TAG_PREFIX, willExpire, } from '@casual-simulation/aux-common';
28
28
  import { serverConfigSchema } from '@casual-simulation/aux-records';
29
29
  import { PassThrough } from 'node:stream';
30
30
  import { getSchemaMetadata } from '@casual-simulation/aux-common';
@@ -32,8 +32,10 @@ import path from 'path';
32
32
  import { readFile } from 'fs/promises';
33
33
  import { setupInfraCommands } from 'infra';
34
34
  import { z } from 'zod';
35
- import { existsSync, readdirSync, statSync } from 'node:fs';
36
- import { writeFile } from 'node:fs/promises';
35
+ import { existsSync, statSync } from 'node:fs';
36
+ import { mkdir, readdir, stat, writeFile } from 'node:fs/promises';
37
+ import { v4 as uuid } from 'uuid';
38
+ import fastJsonStableStringify from '../fast-json-stable-stringify';
37
39
  const REFRESH_LIFETIME_MS = 1000 * 60 * 60 * 24 * 7; // 1 week
38
40
  const config = new Conf({
39
41
  projectName: 'casualos-cli',
@@ -195,6 +197,53 @@ program
195
197
  break;
196
198
  }
197
199
  });
200
+ program
201
+ .command('unpack-aux')
202
+ .argument('[input]', 'The aux file/directory to convert to a file system.')
203
+ .argument('[dir]', 'The directory to write the file system to.')
204
+ .option('-o, --overwrite', 'Overwrite existing files.')
205
+ .option('-r, --recursive', 'Recursively convert aux files in a directory.')
206
+ .option('--write-systemless-bots', "Write bots that don't have a system tag. By default, these bots are written to the extra.aux file.")
207
+ .option('--omit-extra-bots', 'Prevent writing extra.aux files.')
208
+ .description('Generate a folder from an AUX file.')
209
+ .action(async (input, dir, options) => {
210
+ if (options.overwrite) {
211
+ console.log('Overwriting existing files.');
212
+ }
213
+ if (options.recursive) {
214
+ console.log('Recursively converting aux files in input directory.');
215
+ }
216
+ if (options.writeSystemlessBots) {
217
+ console.log('Writing systemless bots. All bots will be written to the file system.');
218
+ }
219
+ if (options.omitExtraBots) {
220
+ console.log('Omitting extra bots. No extra.aux file(s) will be written.');
221
+ }
222
+ await auxGenFs(input, dir, options);
223
+ });
224
+ program
225
+ .command('pack-aux')
226
+ .argument('[dir]', 'The directory to read the file system from. If the directory does not contain an extra.aux file, then each directory will be read as a separate aux file.')
227
+ .argument('[output]', 'The output file to write the aux file to. This should be the folder that each aux should be written to if the input directory contains multiple aux filesystems.')
228
+ .option('-o, --overwrite', 'Overwrite existing files.')
229
+ .option('-f, --filter', 'The bot filter to apply to the bots being read.')
230
+ .option('--allow-duplicates', 'Whether to allow duplicate bots. If a duplicate is encoutered, then a new bot ID will be generated for the duplicate.')
231
+ .description('Generate an AUX file from a folder.')
232
+ .action(async (dir, output, options) => {
233
+ if (options.overwrite) {
234
+ console.log('Overwriting existing files.');
235
+ }
236
+ if (options.merge) {
237
+ console.log('Merging output AUX file.');
238
+ }
239
+ if (options.recursive) {
240
+ console.log('Recursively reading aux files in directory.');
241
+ }
242
+ if (options.allowDuplicates) {
243
+ console.log('Allowing duplicate bots.');
244
+ }
245
+ await auxReadFs(dir, output, options);
246
+ });
198
247
  program
199
248
  .command('generate-server-config')
200
249
  .option('-p, --pretty', 'Pretty print the output.')
@@ -241,59 +290,450 @@ program
241
290
  }
242
291
  });
243
292
  setupInfraCommands(program.command('infra'), config);
244
- async function auxConvert() {
245
- const targetFD = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), 'target file or directory containing files (path)'));
246
- if (existsSync(targetFD)) {
247
- const targetStat = statSync(targetFD);
248
- const auxFiles = [];
249
- if (targetStat.isDirectory()) {
250
- for (let file of readdirSync(targetFD)) {
251
- if (file.slice(-4) === '.aux') {
252
- auxFiles.push(file);
293
+ /**
294
+ * Validates the given file is a proper aux file.
295
+ * This function checks if the file exists, is a file, and has the correct extension.
296
+ * If the file is valid, it returns the parsed bot state from the aux file.
297
+ * @param filePath The path to the file whose to be validated.
298
+ * @param opts Optional options to skip parsing or contents validation.
299
+ */
300
+ async function loadAuxFile(filePath) {
301
+ const targetStat = await stat(filePath);
302
+ if (!targetStat.isFile()) {
303
+ return { success: false, error: 'Path is not a file.' };
304
+ }
305
+ try {
306
+ const contents = JSON.parse(await readFile(filePath, { encoding: 'utf-8' }));
307
+ const botsState = getBotsStateFromStoredAux(contents);
308
+ if (!botsState) {
309
+ return {
310
+ success: false,
311
+ error: `Aux file at ${filePath} is not a valid (or supported) aux file.`,
312
+ };
313
+ }
314
+ return { success: true, botsState };
315
+ }
316
+ catch (err) {
317
+ return {
318
+ success: false,
319
+ error: `Could not read or parse aux file at ${filePath}.\n\n${err}`,
320
+ };
321
+ }
322
+ }
323
+ async function requestFiles(opts) {
324
+ opts = {
325
+ query: 'target file or directory containing files (path)',
326
+ allowedExtensions: new Set(['.aux']),
327
+ ...opts,
328
+ };
329
+ const targetFD = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), opts.query));
330
+ if (!existsSync(targetFD))
331
+ return { directory: null, files: [] };
332
+ const targetStat = statSync(targetFD);
333
+ const files = [];
334
+ if (targetStat.isDirectory()) {
335
+ for (let file of await readdir(targetFD)) {
336
+ if (opts.allowedExtensions.has(path.extname(file).toLowerCase())) {
337
+ files.push(file);
338
+ }
339
+ }
340
+ }
341
+ else if (targetStat.isFile()) {
342
+ if (opts.allowedExtensions.has(path.extname(targetFD).toLowerCase())) {
343
+ files.push(path.basename(targetFD));
344
+ }
345
+ else {
346
+ console.warn(`Invalid file type provided.\nExpected one of ${Array.from(opts.allowedExtensions).join(' | ')}.\nGot: ${path.extname(targetFD).toLowerCase()}`);
347
+ return;
348
+ }
349
+ }
350
+ else {
351
+ console.error('Unknown item at path.');
352
+ return;
353
+ }
354
+ return { directory: getDir(targetFD), files };
355
+ }
356
+ async function requestOutputDirectory(query = 'output directory to write files to') {
357
+ const outDir = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), query));
358
+ if (existsSync(outDir) && statSync(outDir).isDirectory())
359
+ return outDir;
360
+ console.error(`Directory does not exist or is not a directory.`);
361
+ return null;
362
+ }
363
+ const fileTagPrefixes = [
364
+ ['@', '.tsx'],
365
+ [LIBRARY_SCRIPT_PREFIX, '.tsm'],
366
+ [DNA_TAG_PREFIX, '.json'],
367
+ [DATE_TAG_PREFIX, '.date.text'],
368
+ [STRING_TAG_PREFIX, '.text'],
369
+ [NUMBER_TAG_PREFIX, '.number.text'],
370
+ [VECTOR_TAG_PREFIX, '.vector.text'],
371
+ [ROTATION_TAG_PREFIX, '.rotation.text'],
372
+ ];
373
+ const fileExtensions = [
374
+ ['.tsx', '@'],
375
+ ['.tsm', LIBRARY_SCRIPT_PREFIX],
376
+ ['.json', DNA_TAG_PREFIX],
377
+ ['.date.text', DATE_TAG_PREFIX],
378
+ ['.txt', ''],
379
+ ['.text', STRING_TAG_PREFIX],
380
+ ['.number.text', NUMBER_TAG_PREFIX],
381
+ ['.vector.text', VECTOR_TAG_PREFIX],
382
+ ['.rotation.text', ROTATION_TAG_PREFIX],
383
+ ];
384
+ async function auxGenFs(input, output, options) {
385
+ var _a;
386
+ if (!input) {
387
+ input = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the AUX file to convert to a file system');
388
+ }
389
+ input = path.resolve(input);
390
+ if (!existsSync(input)) {
391
+ throw new Error(`The provided path does not exist: ${input}`);
392
+ }
393
+ let files = [];
394
+ let extraDirectories = [];
395
+ const inputStat = await stat(input);
396
+ if (inputStat.isDirectory()) {
397
+ const paths = await readdir(input);
398
+ for (let fileOrFolder of paths) {
399
+ const fileOrFolderPath = path.resolve(input, fileOrFolder);
400
+ const stats = await stat(fileOrFolderPath);
401
+ if (stats.isFile() && fileOrFolder.endsWith('.aux')) {
402
+ files.push(fileOrFolderPath);
403
+ }
404
+ else if (options.recursive &&
405
+ !fileOrFolder.startsWith('.') &&
406
+ stats.isDirectory()) {
407
+ extraDirectories.push(fileOrFolderPath);
408
+ }
409
+ }
410
+ }
411
+ else {
412
+ files.push(input);
413
+ }
414
+ if (!output) {
415
+ output = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The directory to write the file system to');
416
+ }
417
+ output = path.resolve(output);
418
+ // make the directory if it doesn't exist
419
+ await mkdir(output, {
420
+ recursive: true,
421
+ });
422
+ const flag = options.overwrite ? 'w' : 'wx';
423
+ for (const file of files) {
424
+ const fileData = await loadAuxFile(file);
425
+ if (!fileData.success) {
426
+ throw new Error(`Invalid aux file: ${file}.\n\n${fileData.error}`);
427
+ }
428
+ const auxName = path.parse(file).name;
429
+ const botsState = fileData.botsState;
430
+ const extraBotsState = {};
431
+ for (let id in botsState) {
432
+ const bot = botsState[id];
433
+ if (!options.writeSystemlessBots && !hasValue(bot.tags.system)) {
434
+ console.warn(`Adding ${id} to extra.aux.`);
435
+ extraBotsState[id] = bot;
436
+ continue;
437
+ }
438
+ const system = (_a = bot.tags.system) !== null && _a !== void 0 ? _a : id;
439
+ const dirName = system.replace(/\./g, path.sep);
440
+ const dir = path.resolve(output, auxName, dirName);
441
+ const botJson = {
442
+ id,
443
+ tags: {},
444
+ };
445
+ if (hasValue(bot.space)) {
446
+ botJson.space = bot.space;
447
+ }
448
+ if (hasValue(bot.tags.system)) {
449
+ botJson.tags.system = bot.tags.system;
450
+ }
451
+ // make the directory if it doesn't exist
452
+ await mkdir(dir, {
453
+ recursive: true,
454
+ });
455
+ // Don't track tag masks
456
+ for (const tag of Object.keys(bot.tags)) {
457
+ const value = calculateStringTagValue(null, bot, tag, null);
458
+ let written = false;
459
+ if (hasValue(value)) {
460
+ for (let [prefix, ext] of fileTagPrefixes) {
461
+ if (value.startsWith(prefix)) {
462
+ // write the tag value to its own file
463
+ const filePath = path.resolve(dir, `${tag}${ext}`);
464
+ const fileContent = value.slice(prefix.length);
465
+ try {
466
+ await writeFile(filePath, fileContent, {
467
+ encoding: 'utf-8',
468
+ flag,
469
+ });
470
+ written = true;
471
+ }
472
+ catch (err) {
473
+ console.error(`Could not write file: ${filePath}.\n\n${err}\n`);
474
+ }
475
+ }
476
+ }
477
+ if (!written && value.indexOf('\n') >= 0) {
478
+ // string has a newline, so write it to a text file
479
+ const filePath = path.resolve(dir, `${tag}.txt`);
480
+ try {
481
+ await writeFile(filePath, value, {
482
+ encoding: 'utf-8',
483
+ flag,
484
+ });
485
+ written = true;
486
+ }
487
+ catch (err) {
488
+ console.error(`Could not write file: ${filePath}.\n\n${err}\n`);
489
+ }
490
+ }
491
+ }
492
+ if (!written) {
493
+ botJson.tags[tag] = bot.tags[tag];
253
494
  }
254
495
  }
496
+ // write the bot.json file
497
+ const botAuxName = `${system}.bot.aux`;
498
+ const botJsonPath = path.resolve(dir, botAuxName);
499
+ try {
500
+ const botAux = {
501
+ version: 1,
502
+ state: {
503
+ [id]: botJson,
504
+ },
505
+ };
506
+ await writeFile(botJsonPath, fastJsonStableStringify(botAux, {
507
+ space: 2,
508
+ }), {
509
+ encoding: 'utf-8',
510
+ flag,
511
+ });
512
+ }
513
+ catch (err) {
514
+ console.error(`Could not write ${botAuxName} file: ${botJsonPath}.\n\n${err}\n`);
515
+ }
516
+ console.log(`Created: ${system}`);
255
517
  }
256
- else if (targetStat.isFile()) {
257
- if (targetFD.slice(-4) === '.aux') {
258
- auxFiles.push(path.basename(targetFD));
518
+ // Always write the extra bots file so that we can do the reverse operation
519
+ // and produce the original aux file.
520
+ if (!options.omitExtraBots) {
521
+ // write a aux file for the extra bots to the output directory
522
+ const extraBotsFilePath = path.resolve(output, auxName, `extra.aux`);
523
+ try {
524
+ const aux = {
525
+ version: 1,
526
+ state: extraBotsState,
527
+ };
528
+ await writeFile(extraBotsFilePath, JSON.stringify(aux, null, 2), {
529
+ encoding: 'utf-8',
530
+ flag,
531
+ });
532
+ }
533
+ catch (err) {
534
+ console.error(`Could not write extra.aux file: ${extraBotsFilePath}.\n\n${err}\n`);
535
+ }
536
+ }
537
+ }
538
+ for (let extraDir of extraDirectories) {
539
+ // Only allow one level of recursion
540
+ await auxGenFs(extraDir, output, {
541
+ ...options,
542
+ recursive: false,
543
+ });
544
+ }
545
+ }
546
+ async function auxReadFs(input, output, options) {
547
+ const { overwrite } = options;
548
+ const failOnDuplicate = !options.allowDuplicates;
549
+ if (!input) {
550
+ input = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the directory to read into an AUX file.');
551
+ }
552
+ input = path.resolve(input);
553
+ if (!existsSync(input)) {
554
+ console.error(`The provided path does not exist: ${input}`);
555
+ return;
556
+ }
557
+ if (!output) {
558
+ output = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the output AUX file.');
559
+ }
560
+ output = path.resolve(output);
561
+ let filterFunc = null;
562
+ if (options.filter) {
563
+ filterFunc = Function('$', options.filter);
564
+ }
565
+ const inputFiles = await readdir(input);
566
+ const hasExtra = inputFiles.includes('extra.aux');
567
+ if (!hasExtra) {
568
+ await mkdir(output, {
569
+ recursive: true,
570
+ });
571
+ for (let file of inputFiles) {
572
+ const filePath = path.resolve(input, file);
573
+ const outputPath = path.resolve(output, `${file}.aux`);
574
+ const stats = await stat(filePath);
575
+ if (stats.isDirectory()) {
576
+ await auxReadFs(filePath, outputPath, options);
577
+ }
578
+ }
579
+ }
580
+ else {
581
+ console.log('Reading aux files from directory:', input);
582
+ console.log('Output will be written to:', output);
583
+ // folder represents a single aux
584
+ const botsState = await auxReadFsCore(input, filterFunc, failOnDuplicate);
585
+ const storedAux = {
586
+ version: 1,
587
+ state: botsState,
588
+ };
589
+ const outputFolder = path.dirname(output);
590
+ if (outputFolder) {
591
+ await mkdir(path.resolve(outputFolder), {
592
+ recursive: true,
593
+ });
594
+ }
595
+ await writeFile(output, fastJsonStableStringify(storedAux, {
596
+ space: 2,
597
+ }), { encoding: 'utf-8', flag: overwrite ? 'w' : 'wx' });
598
+ }
599
+ }
600
+ async function readAuxFile(filePath) {
601
+ const targetStat = await stat(filePath);
602
+ if (!targetStat.isFile()) {
603
+ throw new Error(`Path is not a file: ${filePath}`);
604
+ }
605
+ const contents = JSON.parse(await readFile(filePath, { encoding: 'utf-8' }));
606
+ const botsState = getBotsStateFromStoredAux(contents);
607
+ if (!botsState) {
608
+ throw new Error(`Aux file at ${filePath} is not a valid (or supported) aux file.`);
609
+ }
610
+ return getUploadState(botsState);
611
+ }
612
+ async function assignBots(state, added, failOnDuplicate) {
613
+ for (let id in added) {
614
+ const b = added[id];
615
+ if (!hasValue(b)) {
616
+ continue;
617
+ }
618
+ if (id in state && hasValue(state[id])) {
619
+ if (failOnDuplicate) {
620
+ throw new Error(`Bot ${id} already exists in the bots state.`);
259
621
  }
260
622
  else {
261
- console.warn(`Invalid file type provided.\nExpected ".aux"`);
262
- return;
623
+ console.warn(`Bot ${id} already exists in the bots state. Generating new ID.`);
624
+ id = uuid();
625
+ b.id = id;
263
626
  }
264
627
  }
265
- else {
266
- console.error('Unknown item at path.');
267
- return;
628
+ state[id] = b;
629
+ }
630
+ }
631
+ async function auxReadFsCore(input, filter, failOnDuplicate) {
632
+ const botsState = {};
633
+ console.log('Reading directory:', input);
634
+ const inputFiles = await readdir(input);
635
+ let tags = {};
636
+ let hasBot = false;
637
+ let botId = null;
638
+ let botState = {};
639
+ for (let file of inputFiles) {
640
+ const filePath = path.join(input, file);
641
+ const fileStat = await stat(filePath);
642
+ if (fileStat.isDirectory()) {
643
+ // If the file is a directory, we need to read its contents recursively
644
+ const subState = await auxReadFsCore(filePath, filter, failOnDuplicate);
645
+ assignBots(botsState, subState, failOnDuplicate);
268
646
  }
269
- if (auxFiles.length < 1)
270
- return;
271
- const outDir = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), 'output directory to write files to'));
272
- if (existsSync(outDir) && statSync(outDir).isDirectory()) {
273
- let converted = 0;
274
- const prefix = outDir === getDir(targetFD) ? '_' : '';
275
- for (let file of auxFiles) {
276
- try {
277
- await writeFile(path.join(outDir, `${prefix}${file}`), JSON.stringify(getBotsStateFromStoredAux(JSON.parse(await readFile(replaceWithBasename(targetFD, file), { encoding: 'utf-8' })))));
278
- converted++;
647
+ else {
648
+ if (file.endsWith('.aux')) {
649
+ console.log(`Reading aux: ${file}`);
650
+ const isSystemBotFile = file.endsWith('.bot.aux');
651
+ const auxBotsState = await readAuxFile(filePath);
652
+ // Get the first bot Id from the aux file
653
+ if (isSystemBotFile && !botId) {
654
+ for (let id in auxBotsState) {
655
+ if (hasValue(id)) {
656
+ console.log(`Found bot ID: ${id}`);
657
+ botId = id;
658
+ hasBot = true;
659
+ break;
660
+ }
661
+ }
279
662
  }
280
- catch (err) {
281
- console.error(`Could not convert: ${file}.\n\n${err}\n`);
663
+ else if (!isSystemBotFile) {
664
+ console.log('Reading extra aux file.\n\n');
282
665
  }
666
+ assignBots(botState, auxBotsState, failOnDuplicate);
283
667
  }
284
- console.log(`\nšŸµ Converted ${converted}/${auxFiles.length} Files.\n--------------------------\n${auxFiles
285
- .map((f) => `|āœ”ļø | ${f}`)
286
- .join('\n')}\n`);
668
+ else {
669
+ for (let [ext, prefix] of fileExtensions) {
670
+ if (file.endsWith(ext)) {
671
+ const tagName = file.slice(0, -ext.length);
672
+ console.log(`Reading tag: ${tagName}`);
673
+ // If the file has a known extension, we can read it and add its contents to the bots state
674
+ const fileContents = prefix +
675
+ (await readFile(filePath, { encoding: 'utf-8' }));
676
+ tags[tagName] = fileContents;
677
+ hasBot = true;
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ if (!botId && hasBot) {
684
+ console.warn('No bot ID found for folder:', input);
685
+ console.warn('Generating a random bot ID.');
686
+ botId = uuid();
687
+ }
688
+ if (botId) {
689
+ const existingBot = botState[botId];
690
+ if (existingBot) {
691
+ existingBot.tags = merge(existingBot.tags, tags);
287
692
  }
288
693
  else {
289
- console.error(`Invalid path provided for output directory.`);
290
- return;
694
+ // If the bot does not exist, we create a new bot with the tags
695
+ botState[botId] = createBot(botId, tags);
291
696
  }
292
697
  }
293
- else {
294
- console.warn(`Invalid directory or file at path: ${targetFD}`);
698
+ if (filter) {
699
+ for (let id in botState) {
700
+ const b = botState[id];
701
+ if (!filter(b)) {
702
+ console.log(`Bot ${id} does not match filter, skipping.`);
703
+ delete botState[id];
704
+ }
705
+ }
706
+ }
707
+ assignBots(botsState, botState, failOnDuplicate);
708
+ return botsState;
709
+ }
710
+ async function auxConvert() {
711
+ const { directory: targetFD, files: auxFiles } = await requestFiles({
712
+ allowedExtensions: new Set(['.aux']),
713
+ });
714
+ if (auxFiles.length < 1) {
715
+ console.error(`No aux file found at/in the provided path.`);
716
+ return;
717
+ }
718
+ const outDir = await requestOutputDirectory();
719
+ if (!outDir) {
720
+ console.error(`Invalid output directory provided.`);
295
721
  return;
296
722
  }
723
+ let converted = 0;
724
+ const prefix = outDir === targetFD ? '_' : '';
725
+ for (let file of auxFiles) {
726
+ try {
727
+ await writeFile(path.join(outDir, `${prefix}${file}`), JSON.stringify(getBotsStateFromStoredAux(JSON.parse(await readFile(replaceWithBasename(targetFD, file), { encoding: 'utf-8' })))));
728
+ converted++;
729
+ }
730
+ catch (err) {
731
+ console.error(`Could not convert: ${file}.\n\n${err}\n`);
732
+ }
733
+ }
734
+ console.log(`\nšŸµ Converted ${converted}/${auxFiles.length} Files.\n--------------------------\n${auxFiles
735
+ .map((f) => `|āœ”ļø | ${f}`)
736
+ .join('\n')}\n`);
297
737
  }
298
738
  async function query(client, procedure, input, shouldConfirm = true, isJavaScriptInput = false, repl = null) {
299
739
  const availableOperations = await client.listProcedures({});