@cyberismo/cli 0.0.1

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/src/index.ts ADDED
@@ -0,0 +1,745 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ Cyberismo
4
+ Copyright © Cyberismo Ltd and contributors 2024
5
+ This program is free software: you can redistribute it and/or modify it under
6
+ the terms of the GNU Affero General Public License version 3 as published by
7
+ the Free Software Foundation.
8
+ This program is distributed in the hope that it will be useful, but WITHOUT
9
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
+ details. You should have received a copy of the GNU Affero General Public
12
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
13
+ */
14
+
15
+ import { Argument, Command } from 'commander';
16
+ import confirm from '@inquirer/confirm';
17
+ import {
18
+ type CardsOptions,
19
+ Cmd,
20
+ Commands,
21
+ ExportFormats,
22
+ type requestStatus,
23
+ type UpdateOperations,
24
+ } from '@cyberismo/data-handler';
25
+ import { ResourceTypeParser as Parser } from './resource-type-parser.js';
26
+ import { startServer } from '@cyberismo/backend';
27
+ // How many validation errors are shown when staring app, if any.
28
+ const VALIDATION_ERROR_ROW_LIMIT = 10;
29
+
30
+ // To avoid duplication, fetch description and version from package.json file.
31
+ // Importing dynamically allows filtering of warnings in cli/bin/run.
32
+ const packageDef = (await import('../package.json', { with: { type: 'json' } }))
33
+ .default;
34
+
35
+ // Truncates a multi-row message to an array of items.
36
+ // Logs maximum of 'limit' items to console. If there are more items than
37
+ // 'limit', the last element is replaced with "..." to indicate truncation.
38
+ // Returns the potentially truncated array.
39
+ function truncateMessage(
40
+ messages: string,
41
+ limit: number = VALIDATION_ERROR_ROW_LIMIT,
42
+ ): string[] {
43
+ const array = messages.split('\n');
44
+ if (array.length < limit) {
45
+ return [...array];
46
+ }
47
+ if (limit <= 0) {
48
+ return [];
49
+ }
50
+ if (limit === 1) {
51
+ return ['...'];
52
+ }
53
+ return [...array.slice(0, limit - 1), '...'];
54
+ }
55
+
56
+ // Handle the response object from data-handler
57
+ function handleResponse(response: requestStatus) {
58
+ if (response.statusCode === 200) {
59
+ if (response.payload) {
60
+ if (response.message) {
61
+ console.log(response.message);
62
+ }
63
+ console.log(JSON.stringify(response.payload, null, 2));
64
+ } else if (response.message) {
65
+ console.log(response.message);
66
+ } else {
67
+ console.log('Done');
68
+ }
69
+ } else {
70
+ if (response.message) {
71
+ program.error(response.message);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Commander
77
+ const program = new Command();
78
+
79
+ // CLI command handler
80
+ const commandHandler = new Commands();
81
+
82
+ // Ensure that all names have the same guideline.
83
+ const nameGuideline =
84
+ 'Name can contain letters (a-z|A-Z), spaces, underscores or hyphens.';
85
+ const pathGuideline =
86
+ 'Path to the project root. Mandatory if not running inside a project tree.';
87
+
88
+ const additionalHelpForCreate = `Sub-command help:
89
+ create attachment <cardKey> <filename>, where
90
+ <cardKey> is card key of a card to have the attachment,
91
+ <filename> is attachment filename.
92
+
93
+ create card <template> [cardKey], where
94
+ <template> Template to use. You can list the templates in a project with "show templates" command.
95
+ [cardKey] Parent card's card key. If defined, new card will be created as a child card to that card.
96
+
97
+ create cardType <name> <workflow>, where
98
+ <name> Name for cardType. ${nameGuideline}
99
+ <workflow> Workflow for the card type. You can list workflows in a project with "show workflows" command.
100
+
101
+ create fieldType <name> <dataType>, where
102
+ <name> Name for fieldType. ${nameGuideline}
103
+ <dataType> Type of field. You can list field types in a project with "show fieldTypes" command.
104
+
105
+ create graphModel <name>, where
106
+ <name> Name for graph model. ${nameGuideline}
107
+
108
+ create graphView <name>, where
109
+ <name> Name for graph view. ${nameGuideline}
110
+
111
+ create label <cardKey> <labelName>, where
112
+ <cardKey> Card key of the label
113
+ <labelName> Name for the new label
114
+
115
+ create link <source> <destination> <linkType> [description], where
116
+ <source> Source card key of the link
117
+ <destination> Destination card key of the link
118
+ <linkType> Link type to create
119
+ [description] Link description
120
+
121
+ create linkType <name>, where
122
+ <name> Name for linkType. ${nameGuideline}
123
+
124
+ create project <name> <prefix> <path>, where
125
+ <name> Name of the project.
126
+ <prefix> Prefix for the project.
127
+ <path> Path where to create the project
128
+
129
+ create report <name>, where
130
+ <name> Name for report. ${nameGuideline}
131
+
132
+ create template <name> [content], where
133
+ <name> Name for template. ${nameGuideline}
134
+ [content] If empty, template is created with default values. Template content must conform to schema "templateSchema.json"
135
+
136
+ create workflow <name> [content], where
137
+ <name> Name for workflow. ${nameGuideline}
138
+ [content] If empty, workflow is created with default values. Workflow content must conform to schema "workflowSchema.json"
139
+
140
+ create <resourceName> [content], where
141
+ <resourceName> Name of the resource (e.g. <prefix>/<type>/<identifier>)
142
+ [content] If empty, resource is created with default values. Content must conform to its resource schema.`;
143
+
144
+ const additionalHelpForRemove = `Sub-command help:
145
+ remove attachment <cardKey> <filename>, where
146
+ <cardKey> is card key of the owning card,
147
+ <filename> is attachment filename.
148
+
149
+ remove card <cardKey>, where
150
+ <cardKey> Card key of card to remove
151
+
152
+ remove label <cardKey> [label], where
153
+ <cardKey> Card key of the label
154
+ [label] Label being removed
155
+
156
+ remove link <source> <destination> <linkType>, where
157
+ <source> Source card key of the link
158
+ <destination> Destination card key of the link
159
+ <linkType> Link type to remove
160
+
161
+ remove module <name>, where
162
+ <name> Name of the module to remove
163
+
164
+ remove <resourceName>, where
165
+ <resourceName> is <project prefix>/<type>/<identifier>, where
166
+ <project prefix> Prefix for the project.
167
+ <type> is plural form of supported types; e.g. "workflows"
168
+ <identifier> name of a specific resource.
169
+ Note that you cannot remove resources from imported modules.`;
170
+
171
+ // Main CLI program.
172
+ program
173
+ .name('cyberismo')
174
+ .description(packageDef.description)
175
+ .version(packageDef.version);
176
+
177
+ // Add card to a template
178
+ program
179
+ .command('add')
180
+ .description('Add card to a template')
181
+ .argument(
182
+ '<template>',
183
+ 'Template for a new card. \nYou can list the templates in a project with "show templates" command.',
184
+ )
185
+ .argument(
186
+ '<cardType>',
187
+ 'Card type to use for the new card. \nYou can list the card types in a project with "show cardTypes" command.',
188
+ )
189
+ .argument('[cardKey]', "Parent card's card key")
190
+ .option('-p, --project-path [path]', `${pathGuideline}`)
191
+ .option('-r, --repeat <quantity>', 'Add multiple cards to a template')
192
+ .action(
193
+ async (
194
+ template: string,
195
+ cardType: string,
196
+ cardKey: string,
197
+ options: CardsOptions,
198
+ ) => {
199
+ const result = await commandHandler.command(
200
+ Cmd.add,
201
+ [template, cardType, cardKey],
202
+ options,
203
+ );
204
+ handleResponse(result);
205
+ },
206
+ );
207
+
208
+ const calculate = program
209
+ .command('calc')
210
+ .description('Used for running logic programs');
211
+
212
+ calculate
213
+ .command('generate')
214
+ .description('DEPRECATED: Generate a logic program')
215
+ .argument('[cardKey]', 'If given, calculates on the subtree of the card')
216
+ .option('-p, --project-path [path]', `${pathGuideline}`)
217
+ .action(async () => {
218
+ console.warn(
219
+ 'This command is deprecated. Use "cyberismo calc run" directly instead.',
220
+ );
221
+ });
222
+
223
+ calculate
224
+ .command('run')
225
+ .description('Run a logic program')
226
+ .argument('<filePath>', 'Path to the logic program')
227
+ .option('-p, --project-path [path]', `${pathGuideline}`)
228
+ .action(async (filePath: string, options: CardsOptions) => {
229
+ const result = await commandHandler.command(
230
+ Cmd.calc,
231
+ ['run', filePath],
232
+ options,
233
+ );
234
+ handleResponse(result);
235
+ });
236
+
237
+ program
238
+ .command('create')
239
+ .argument(
240
+ '<type>',
241
+ `types to create: '${Parser.listTargets('create').join("', '")}', or resource name (e.g. <prefix>/<type>/<identifier>)`,
242
+ Parser.parseCreateTypes,
243
+ )
244
+ .argument(
245
+ '[target]',
246
+ 'Name to create, or in some operations cardKey to create data to a specific card. See below',
247
+ )
248
+ .argument(
249
+ '[parameter1]',
250
+ 'Depends on context; see below for specific remove operation',
251
+ )
252
+ .argument(
253
+ '[parameter2]',
254
+ 'Depends on context; see below for specific remove operation',
255
+ )
256
+ .argument(
257
+ '[parameter3]',
258
+ 'Depends on context; see below for specific remove operation',
259
+ )
260
+ .addHelpText('after', additionalHelpForCreate)
261
+ .option('-p, --project-path [path]', `${pathGuideline}`)
262
+ .action(
263
+ async (
264
+ type: string,
265
+ target: string,
266
+ parameter1: string,
267
+ parameter2: string,
268
+ parameter3: string,
269
+ options: CardsOptions,
270
+ ) => {
271
+ if (!type) {
272
+ program.error(`missing required argument <type>`);
273
+ }
274
+
275
+ const resourceName: boolean = type.split('/').length === 3;
276
+
277
+ function nameOfFirstArgument(type: string) {
278
+ if (type === 'attachment' || type === 'label') return 'cardKey';
279
+ if (type === 'card') return 'template';
280
+ if (type === 'link') return 'source';
281
+ return 'name';
282
+ }
283
+
284
+ function nameOfSecondArgument(type: string) {
285
+ if (type === 'attachment') return 'fileName';
286
+ if (type === 'cardType') return 'workflow';
287
+ if (type === 'fieldType') return 'dataType';
288
+ if (type === 'label') return 'labelName';
289
+ if (type === 'link') return 'destination';
290
+ if (type === 'project') return 'prefix';
291
+ return type;
292
+ }
293
+
294
+ if (!target && !resourceName) {
295
+ program.error(
296
+ `missing required argument <${nameOfFirstArgument(type)}>`,
297
+ );
298
+ }
299
+
300
+ if (
301
+ !resourceName &&
302
+ !parameter1 &&
303
+ type !== 'card' &&
304
+ type !== 'graphModel' &&
305
+ type !== 'graphView' &&
306
+ type !== 'linkType' &&
307
+ type !== 'report' &&
308
+ type !== 'template' &&
309
+ type !== 'workflow'
310
+ ) {
311
+ program.error(
312
+ `missing required argument <${nameOfSecondArgument(type)}>`,
313
+ );
314
+ }
315
+
316
+ if (
317
+ resourceName &&
318
+ (type.includes('cardTypes') || type.includes('fieldTypes')) &&
319
+ !target
320
+ ) {
321
+ program.error(
322
+ `missing required argument <${nameOfSecondArgument(type)}>`,
323
+ );
324
+ }
325
+
326
+ if (type === 'project') {
327
+ if (!parameter2) {
328
+ program.error(`missing required argument <path>`);
329
+ }
330
+ // Project path must be set to 'options' when creating a project.
331
+ options.projectPath = parameter2;
332
+ }
333
+
334
+ const result = await commandHandler.command(
335
+ Cmd.create,
336
+ [type, target, parameter1, parameter2, parameter3],
337
+ options,
338
+ );
339
+ handleResponse(result);
340
+ },
341
+ );
342
+
343
+ // Edit command
344
+ program
345
+ .command('edit')
346
+ .description('Edit a card')
347
+ .argument('<cardKey>', 'Card key of card')
348
+ .option('-p, --project-path [path]', `${pathGuideline}`)
349
+ .action(async (cardKey: string, options: CardsOptions) => {
350
+ const result = await commandHandler.command(Cmd.edit, [cardKey], options);
351
+ handleResponse(result);
352
+ });
353
+
354
+ // Export command
355
+ program
356
+ .command('export')
357
+ .description('Export a project or a card')
358
+ .addArgument(
359
+ new Argument('<format>', 'Export format').choices(
360
+ Object.values(ExportFormats),
361
+ ),
362
+ )
363
+ .argument('<output>', 'Output path')
364
+ .argument(
365
+ '[cardKey]',
366
+ 'Path to a card. If defined will export only that card and its children instead of whole project.',
367
+ )
368
+ .option('-p, --project-path [path]', `${pathGuideline}`)
369
+ .action(
370
+ async (
371
+ format: string,
372
+ output: ExportFormats,
373
+ cardKey: string,
374
+ options: CardsOptions,
375
+ ) => {
376
+ const result = await commandHandler.command(
377
+ Cmd.export,
378
+ [format, output, cardKey],
379
+ options,
380
+ );
381
+ handleResponse(result);
382
+ },
383
+ );
384
+
385
+ const importCmd = program.command('import');
386
+
387
+ // Import module
388
+ importCmd
389
+ .command('module')
390
+ .description(
391
+ 'Imports another project to this project as a module. Source can be local relative file path, or git HTTPS URL.',
392
+ )
393
+ .argument('<source>', 'Path to import from')
394
+ .argument('[branch]', 'When using git URL defines the branch. Default: main')
395
+ .argument(
396
+ '[useCredentials]',
397
+ 'When using git URL uses credentials for cloning. Default: false',
398
+ )
399
+ .option('-p, --project-path [path]', `${pathGuideline}`)
400
+ .action(
401
+ async (
402
+ source: string,
403
+ branch: string,
404
+ useCredentials: boolean,
405
+ options: CardsOptions,
406
+ ) => {
407
+ const result = await commandHandler.command(
408
+ Cmd.import,
409
+ ['module', source, branch, String(useCredentials)],
410
+ options,
411
+ );
412
+ handleResponse(result);
413
+ },
414
+ );
415
+
416
+ // import csv
417
+ importCmd
418
+ .command('csv')
419
+ .description('Imports cards from a csv file')
420
+ .argument('<csvFile>', 'File to import from')
421
+ .argument(
422
+ '[cardKey]',
423
+ 'Card key of the parent. If defined, cards are created as children of this card',
424
+ )
425
+ .option('-p, --project-path [path]', `${pathGuideline}`)
426
+ .action(async (csvFile: string, cardKey: string, options: CardsOptions) => {
427
+ const result = await commandHandler.command(
428
+ Cmd.import,
429
+ ['csv', csvFile, cardKey],
430
+ options,
431
+ );
432
+ handleResponse(result);
433
+ });
434
+
435
+ // Move command
436
+ program
437
+ .command('move')
438
+ .description(
439
+ 'Moves a card from root to under another card, from under another card to root, or from under a one card to another.',
440
+ )
441
+ .argument('[source]', 'Source Card key that needs to be moved')
442
+ .argument(
443
+ '[destination]',
444
+ 'Destination Card key where "source" is moved to. If moving to root, use "root"',
445
+ )
446
+ .option('-p, --project-path [path]', `${pathGuideline}`)
447
+ .action(
448
+ async (source: string, destination: string, options: CardsOptions) => {
449
+ const result = await commandHandler.command(
450
+ Cmd.move,
451
+ [source, destination],
452
+ options,
453
+ );
454
+ handleResponse(result);
455
+ },
456
+ );
457
+
458
+ const rank = program.command('rank');
459
+
460
+ rank
461
+ .command('card')
462
+ .description(
463
+ 'Set the rank of a card. Ranks define the order in which cards are shown.',
464
+ )
465
+ .argument('<cardKey>', 'Card key of the card to be moved')
466
+ .argument(
467
+ '<afterCardKey>',
468
+ 'Card key of the card that the card should be after. Use "first" to rank the card first.',
469
+ )
470
+ .option('-p, --project-path [path]', `${pathGuideline}`)
471
+ .action(
472
+ async (cardKey: string, afterCardKey: string, options: CardsOptions) => {
473
+ const result = await commandHandler.command(
474
+ Cmd.rank,
475
+ ['card', cardKey, afterCardKey],
476
+ options,
477
+ );
478
+ handleResponse(result);
479
+ },
480
+ );
481
+
482
+ rank
483
+ .command('rebalance')
484
+ .description(
485
+ 'Rebalance the rank of all cards in the project. Can be also used, if ranks do not exist',
486
+ )
487
+ .argument(
488
+ '[parentCardKey]',
489
+ 'if null, rebalance the whole project, otherwise rebalance only the direct children of the card key',
490
+ )
491
+ .option('-p, --project-path [path]', `${pathGuideline}`)
492
+ .action(async (cardKey: string, options: CardsOptions) => {
493
+ const result = await commandHandler.command(
494
+ Cmd.rank,
495
+ ['rebalance', cardKey],
496
+ options,
497
+ );
498
+ handleResponse(result);
499
+ });
500
+
501
+ // Remove command
502
+ program
503
+ .command('remove')
504
+ .argument(
505
+ '<type>',
506
+ `removable types: '${Parser.listTargets('remove').join("', '")}', or resource name (e.g. <prefix>/<type>/<identifier>)`,
507
+ Parser.parseRemoveTypes,
508
+ )
509
+ .argument(
510
+ '[parameter1]',
511
+ 'Depends on context; see below for specific remove operation',
512
+ )
513
+ .argument(
514
+ '[parameter2]',
515
+ 'Depends on context; see below for specific remove operation',
516
+ )
517
+ .argument(
518
+ '[parameter3]',
519
+ 'Depends on context; see below for specific remove operation',
520
+ )
521
+ .addHelpText('after', additionalHelpForRemove)
522
+ .option('-p, --project-path [path]', `${pathGuideline}`)
523
+ .action(
524
+ async (
525
+ type: string,
526
+ parameter1: string,
527
+ parameter2: string,
528
+ parameter3: string,
529
+ options: CardsOptions,
530
+ ) => {
531
+ if (type) {
532
+ if (!parameter1) {
533
+ if (type === 'attachment' || type === 'card' || type === 'label') {
534
+ program.error('error: missing argument <cardKey>');
535
+ } else if (type === 'link') {
536
+ program.error('error: missing argument <source>');
537
+ } else if (type === 'module') {
538
+ program.error('error: missing argument <moduleName>');
539
+ } else {
540
+ if (Parser.listTargets('remove').includes(type)) {
541
+ program.error('error: missing argument <resourceName>');
542
+ }
543
+ }
544
+ }
545
+ if (!parameter2 && type === 'attachment') {
546
+ program.error('error: missing argument <filename>');
547
+ }
548
+ if (!parameter2 && type === 'link') {
549
+ program.error('error: missing argument <destination>');
550
+ }
551
+ if (!parameter3 && type === 'link') {
552
+ program.error('error: missing argument <linkType>');
553
+ }
554
+
555
+ const result = await commandHandler.command(
556
+ Cmd.remove,
557
+ [type, parameter1, parameter2, parameter3],
558
+ options,
559
+ );
560
+ handleResponse(result);
561
+ }
562
+ },
563
+ );
564
+
565
+ // Rename command
566
+ program
567
+ .command('rename')
568
+ .description(
569
+ 'Change project prefix and rename all the content with the new prefix',
570
+ )
571
+ .argument('<to>', 'New project prefix')
572
+ .option('-p, --project-path [path]', `${pathGuideline}`)
573
+ .action(async (to: string, options: CardsOptions) => {
574
+ const result = await commandHandler.command(Cmd.rename, [to], options);
575
+ handleResponse(result);
576
+ });
577
+
578
+ // Report command
579
+ program
580
+ .command('report')
581
+ .description('Runs a report')
582
+ .argument(
583
+ '<parameters>',
584
+ 'Path to parameters file. This file defines which report to run and what parameters to use.',
585
+ )
586
+ .argument(
587
+ '[output]',
588
+ 'Optional output file; if omitted output will be directed to stdout',
589
+ )
590
+ .option('-p, --project-path [path]', `${pathGuideline}`)
591
+ .action(async (parameters: string, output: string, options: CardsOptions) => {
592
+ const result = await commandHandler.command(
593
+ Cmd.report,
594
+ [parameters, output],
595
+ options,
596
+ );
597
+ handleResponse(result);
598
+ });
599
+
600
+ // Show command
601
+ program
602
+ .command('show')
603
+ .description('Shows details from a project')
604
+ .argument(
605
+ '<type>',
606
+ `details can be seen from: ${Parser.listTargets('show').join(', ')}`,
607
+ Parser.parseShowTypes,
608
+ )
609
+ .argument(
610
+ '[typeDetail]',
611
+ 'additional information about the requested type; for example a card key',
612
+ )
613
+ .option(
614
+ '-d --details',
615
+ 'Certain types (such as cards) can have additional details',
616
+ )
617
+ .option('-p, --project-path [path]', `${pathGuideline}`)
618
+ .option(
619
+ '-u --show-use',
620
+ 'Show where resource is used. Only used with resources, otherwise will be ignored.',
621
+ )
622
+ .action(async (type: string, typeDetail, options: CardsOptions) => {
623
+ if (type !== '') {
624
+ const result = await commandHandler.command(
625
+ Cmd.show,
626
+ [type, typeDetail],
627
+ options,
628
+ );
629
+ handleResponse(result);
630
+ }
631
+ });
632
+
633
+ // Transition command
634
+ program
635
+ .command('transition')
636
+ .description('Transition a card to the specified state')
637
+ .argument('<cardKey>', 'card key of a card')
638
+ .argument(
639
+ '<transition>',
640
+ 'Workflow state transition that is done.\nYou can list the workflows in a project with "show workflows" command.\nYou can see the available transitions with "show workflow <name>" command.',
641
+ )
642
+ .option('-p, --project-path [path]', `${pathGuideline}`)
643
+ .action(
644
+ async (cardKey: string, transition: string, options: CardsOptions) => {
645
+ const result = await commandHandler.command(
646
+ Cmd.transition,
647
+ [cardKey, transition],
648
+ options,
649
+ );
650
+ handleResponse(result);
651
+ },
652
+ );
653
+
654
+ // Update command
655
+ program
656
+ .command('update')
657
+ .description('Update resource details')
658
+ .argument('<resourceName>', 'Resource name')
659
+ .argument(
660
+ '<operation>',
661
+ 'Type of change, either "add", "change", "rank" or "remove" ',
662
+ )
663
+ .argument('<key>', 'Detail to be changed')
664
+ .argument('<value>', 'Value for a detail')
665
+ .argument(
666
+ '[newValue]',
667
+ 'When using "change" define new value for detail.\nWhen using "remove" provide optional replacement value for removed value',
668
+ )
669
+ .action(
670
+ async (
671
+ resourceName: string,
672
+ key: string,
673
+ operation: UpdateOperations,
674
+ value: string,
675
+ newValue: string,
676
+ options: CardsOptions,
677
+ ) => {
678
+ const result = await commandHandler.command(
679
+ Cmd.update,
680
+ [resourceName, key, operation, value, newValue],
681
+ options,
682
+ );
683
+ handleResponse(result);
684
+ },
685
+ );
686
+
687
+ // Updates all modules in the project.
688
+ program
689
+ .command('update-modules')
690
+ .description('Updates all imported modules with latest versions')
691
+ .option('-p, --project-path [path]', `${pathGuideline}`)
692
+ .action(async (options: CardsOptions) => {
693
+ const result = await commandHandler.command(Cmd.updateModules, [], options);
694
+ handleResponse(result);
695
+ });
696
+
697
+ // Validate command
698
+ program
699
+ .command('validate')
700
+ .description('Validate project structure')
701
+ .option('-p, --project-path [path]', `${pathGuideline}`)
702
+ .action(async (options: CardsOptions) => {
703
+ const result = await commandHandler.command(Cmd.validate, [], options);
704
+ handleResponse(result);
705
+ });
706
+
707
+ // Start app command.
708
+ // If there are validation errors, user is prompted to continue or not.
709
+ // There is 10 sec timeout on the prompt. If user does not reply, then
710
+ // it is assumed that validation errors do not matter and application
711
+ // start is resumed.
712
+ program
713
+ .command('app')
714
+ .description(
715
+ 'Starts the cyberismo app, accessible with a web browser at http://localhost:3000',
716
+ )
717
+ .option('-p, --project-path [path]', `${pathGuideline}`)
718
+ .action(async (options: CardsOptions) => {
719
+ // validate project
720
+ const result = await commandHandler.command(Cmd.validate, [], options);
721
+ if (!result.message) {
722
+ program.error('Expected validation result, but got none');
723
+ return;
724
+ }
725
+ if (result.message !== 'Project structure validated') {
726
+ truncateMessage(result.message).forEach((item) => console.error(item));
727
+ console.error('\n'); // The output looks nicer with one extra row.
728
+ result.message = '';
729
+ const userConfirmation = await confirm(
730
+ {
731
+ message: 'There are validation errors. Do you want to continue?',
732
+ },
733
+ { signal: AbortSignal.timeout(10000), clearPromptOnDone: true },
734
+ ).catch((error) => {
735
+ return error.name === 'AbortPromptError';
736
+ });
737
+ if (!userConfirmation) {
738
+ handleResponse(result);
739
+ return;
740
+ }
741
+ }
742
+ startServer(await commandHandler.getProjectPath(options.projectPath));
743
+ });
744
+
745
+ export default program;