@cyberismo/cli 0.0.18 → 0.0.20

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/dist/index.js CHANGED
@@ -11,7 +11,8 @@
11
11
  details. You should have received a copy of the GNU Affero General Public
12
12
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
13
13
  */
14
- import { readFile } from 'node:fs/promises';
14
+ import { constants, existsSync } from 'node:fs';
15
+ import { access, lstat, readFile } from 'node:fs/promises';
15
16
  import { resolve } from 'node:path';
16
17
  import { Argument, Command, Option } from 'commander';
17
18
  import confirm from '@inquirer/confirm';
@@ -81,8 +82,7 @@ async function importableModules(options) {
81
82
  const importableModules = await commandHandler.command(Cmd.show, ['importableModules'], copyOptions);
82
83
  if (!importableModules?.payload ||
83
84
  importableModules.payload.length === 0) {
84
- console.error('No modules available');
85
- process.exit(1);
85
+ program.error('No modules available');
86
86
  }
87
87
  // return potential importable modules
88
88
  const choices = importableModules.payload?.map((module) => ({
@@ -100,61 +100,6 @@ const program = new Command();
100
100
  // Ensure that all names have the same guideline.
101
101
  const nameGuideline = 'Name can contain letters (a-z|A-Z), spaces, underscores or hyphens.';
102
102
  const pathGuideline = 'Path to the project root. Mandatory if not running inside a project tree.';
103
- const additionalHelpForCreate = `Sub-command help:
104
- create attachment <cardKey> <filename>, where
105
- <cardKey> is card key of a card to have the attachment,
106
- <filename> is attachment filename.
107
-
108
- create card <template> [cardKey], where
109
- <template> Template to use. You can list the templates in a project with "show templates" command.
110
- [cardKey] Parent card's card key. If defined, new card will be created as a child card to that card.
111
-
112
- create cardType <name> <workflow>, where
113
- <name> Name for cardType. ${nameGuideline}
114
- <workflow> Workflow for the card type. You can list workflows in a project with "show workflows" command.
115
-
116
- create fieldType <name> <dataType>, where
117
- <name> Name for fieldType. ${nameGuideline}
118
- <dataType> Type of field. You can list field types in a project with "show fieldTypes" command.
119
-
120
- create graphModel <name>, where
121
- <name> Name for graph model. ${nameGuideline}
122
-
123
- create graphView <name>, where
124
- <name> Name for graph view. ${nameGuideline}
125
-
126
- create label <cardKey> <labelName>, where
127
- <cardKey> Card key of the label
128
- <labelName> Name for the new label
129
-
130
- create link <source> <destination> <linkType> [description], where
131
- <source> Source card key of the link
132
- <destination> Destination card key of the link
133
- <linkType> Link type to create
134
- [description] Link description
135
-
136
- create linkType <name>, where
137
- <name> Name for linkType. ${nameGuideline}
138
-
139
- create project <name> <prefix> <path>, where
140
- <name> Name of the project.
141
- <prefix> Prefix for the project.
142
- <path> Path where to create the project
143
-
144
- create report <name>, where
145
- <name> Name for report. ${nameGuideline}
146
-
147
- create template <name> [content], where
148
- <name> Name for template. ${nameGuideline}
149
- [content] If empty, template is created with default values. Template content must conform to schema "templateSchema.json"
150
-
151
- create workflow <name> [content], where
152
- <name> Name for workflow. ${nameGuideline}
153
- [content] If empty, workflow is created with default values. Workflow content must conform to schema "workflowSchema.json"
154
-
155
- create <resourceName> [content], where
156
- <resourceName> Name of the resource (e.g. <prefix>/<type>/<identifier>)
157
- [content] If empty, resource is created with default values. Content must conform to its resource schema.`;
158
103
  const additionalHelpForRemove = `Sub-command help:
159
104
  remove attachment <cardKey> <filename>, where
160
105
  <cardKey> is card key of the owning card,
@@ -211,6 +156,17 @@ const additionalHelpForUpdate = `Sub-command help:
211
156
  const contextOption = new Option('-c, --context [context]', 'Context to run the logic programs in.')
212
157
  .choices(validContexts)
213
158
  .default('app');
159
+ const pathOption = new Option('-p, --project-path <path>', pathGuideline);
160
+ // Custom Command class with pathOption pre-configured
161
+ class CommandWithPath extends Command {
162
+ createCommand(name) {
163
+ return new CommandWithPath(name);
164
+ }
165
+ constructor(name) {
166
+ super(name);
167
+ this.addOption(pathOption);
168
+ }
169
+ }
214
170
  // Main CLI program.
215
171
  program
216
172
  .name('cyberismo')
@@ -220,7 +176,8 @@ program
220
176
  .addOption(new Option('-L, --log-level <level>', 'Set the log level')
221
177
  .choices(['trace', 'debug', 'info', 'warn', 'error', 'fatal'])
222
178
  .default('fatal'));
223
- const addCmd = program.command('add').description('Add items to the project');
179
+ const addCmd = new CommandWithPath('add').description('Add items to the project');
180
+ program.addCommand(addCmd);
224
181
  // Add card to a template
225
182
  addCmd
226
183
  .command('card')
@@ -228,7 +185,6 @@ addCmd
228
185
  .argument('<template>', 'Template for a new card. \nYou can list the templates in a project with "show templates" command.')
229
186
  .argument('<cardType>', 'Card type to use for the new card. \nYou can list the card types in a project with "show cardTypes" command.')
230
187
  .argument('[cardKey]', "Parent card's card key")
231
- .option('-p, --project-path [path]', `${pathGuideline}`)
232
188
  .option('-r, --repeat <quantity>', 'Add multiple cards to a template')
233
189
  .action(async (template, cardType, cardKey, options) => {
234
190
  const result = await commandHandler.command(Cmd.add, ['card', template, cardType, cardKey], Object.assign({}, options, program.opts()));
@@ -238,7 +194,6 @@ addCmd
238
194
  .command('hub')
239
195
  .description('Add a hub to the project')
240
196
  .argument('<location>', 'Hub URL. Default hub can be added by using "default"')
241
- .option('-p, --project-path [path]', `${pathGuideline}`)
242
197
  .action(async (location, options) => {
243
198
  if (location === 'default') {
244
199
  location = DEFAULT_HUB;
@@ -246,15 +201,13 @@ addCmd
246
201
  const result = await commandHandler.command(Cmd.add, ['hub', location], Object.assign({}, options, program.opts()));
247
202
  handleResponse(result);
248
203
  });
249
- const calculate = program
250
- .command('calc')
251
- .description('Used for running logic programs');
204
+ const calculate = new CommandWithPath('calc').description('Used for running logic programs');
205
+ program.addCommand(calculate);
252
206
  calculate
253
207
  .command('generate')
254
208
  .description('Generate a logic program')
255
209
  .argument('<destination>', 'Path to an output file. Command writes the logic program to this file.')
256
210
  .argument('[query]', 'Query to run')
257
- .option('-p, --project-path [path]', `${pathGuideline}`)
258
211
  .action(async (destination, query, options) => {
259
212
  const result = await commandHandler.command(Cmd.calc, ['generate', destination, query], Object.assign({}, options, program.opts()));
260
213
  handleResponse(result);
@@ -264,88 +217,138 @@ calculate
264
217
  .description('Run a logic program')
265
218
  .argument('<filePath>', 'Path to the logic program')
266
219
  .addOption(contextOption)
267
- .option('-p, --project-path [path]', `${pathGuideline}`)
268
220
  .action(async (filePath, options) => {
269
221
  const result = await commandHandler.command(Cmd.calc, ['run', filePath], Object.assign({}, options, program.opts()));
270
222
  handleResponse(result);
271
223
  });
272
- program
273
- .command('create')
274
- .description('Create cards, resources and other project items')
275
- .argument('<type>', `types to create: '${Parser.listTargets('create').join("', '")}', or resource name (e.g. <prefix>/<type>/<identifier>)`, Parser.parseCreateTypes)
276
- .argument('[target]', 'Name to create, or in some operations cardKey to create data to a specific card. See below')
277
- .argument('[parameter1]', 'Depends on context; see below for specific remove operation')
278
- .argument('[parameter2]', 'Depends on context; see below for specific remove operation')
279
- .argument('[parameter3]', 'Depends on context; see below for specific remove operation')
280
- .addHelpText('after', additionalHelpForCreate)
281
- .option('-p, --project-path [path]', `${pathGuideline}`)
224
+ const createCmd = new CommandWithPath('create').description('Create cards, resources and other project items');
225
+ program.addCommand(createCmd);
226
+ // Create attachment subcommand
227
+ createCmd
228
+ .command('attachment')
229
+ .description('Create an attachment for a card')
230
+ .argument('<cardKey>', 'Card key of the card to attach to')
231
+ .argument('<filename>', 'Path to the file to attach')
232
+ .action(async (cardKey, filename, options) => {
233
+ const result = await commandHandler.command(Cmd.create, ['attachment', cardKey, filename], Object.assign({}, options, program.opts()));
234
+ handleResponse(result);
235
+ });
236
+ // Create card subcommand
237
+ createCmd
238
+ .command('card')
239
+ .description('Create a card from a template')
240
+ .argument('<template>', 'Template to use. You can list templates with "show templates" command')
241
+ .argument('[parentCardKey]', "Parent card's card key. If defined, new card will be created as a child")
242
+ .action(async (template, parentCardKey, options) => {
243
+ const result = await commandHandler.command(Cmd.create, ['card', template, parentCardKey].filter(Boolean), Object.assign({}, options, program.opts()));
244
+ handleResponse(result);
245
+ });
246
+ // Create cardType subcommand
247
+ createCmd
248
+ .command('cardType')
249
+ .description('Create a new card type')
250
+ .argument('<name>', `Name for card type. ${nameGuideline}`)
251
+ .argument('<workflow>', 'Workflow for the card type. You can list workflows with "show workflows" command')
252
+ .action(async (name, workflow, options) => {
253
+ const result = await commandHandler.command(Cmd.create, ['cardType', name, workflow], Object.assign({}, options, program.opts()));
254
+ handleResponse(result);
255
+ });
256
+ // Create fieldType subcommand
257
+ createCmd
258
+ .command('fieldType')
259
+ .description('Create a new field type')
260
+ .argument('<name>', `Name for field type. ${nameGuideline}`)
261
+ .argument('<dataType>', 'Type of field. You can list field types with "show fieldTypes" command')
262
+ .action(async (name, dataType, options) => {
263
+ const result = await commandHandler.command(Cmd.create, ['fieldType', name, dataType], Object.assign({}, options, program.opts()));
264
+ handleResponse(result);
265
+ });
266
+ // Create graphModel subcommand
267
+ createCmd
268
+ .command('graphModel')
269
+ .description('Create a new graph model')
270
+ .argument('<name>', `Name for graph model. ${nameGuideline}`)
271
+ .action(async (name, options) => {
272
+ const result = await commandHandler.command(Cmd.create, ['graphModel', name], Object.assign({}, options, program.opts()));
273
+ handleResponse(result);
274
+ });
275
+ // Create graphView subcommand
276
+ createCmd
277
+ .command('graphView')
278
+ .description('Create a new graph view')
279
+ .argument('<name>', `Name for graph view. ${nameGuideline}`)
280
+ .action(async (name, options) => {
281
+ const result = await commandHandler.command(Cmd.create, ['graphView', name], Object.assign({}, options, program.opts()));
282
+ handleResponse(result);
283
+ });
284
+ // Create calculation subcommand
285
+ createCmd
286
+ .command('calculation')
287
+ .description('Create a new calculation')
288
+ .argument('<name>', `Name for calculation. ${nameGuideline}`)
289
+ .action(async (name, options) => {
290
+ const result = await commandHandler.command(Cmd.create, ['calculation', name], Object.assign({}, options, program.opts()));
291
+ handleResponse(result);
292
+ });
293
+ // Create label subcommand
294
+ createCmd
295
+ .command('label')
296
+ .description('Create a label on a card')
297
+ .argument('<cardKey>', 'Card key')
298
+ .argument('<labelName>', 'Name for the new label')
299
+ .action(async (cardKey, labelName, options) => {
300
+ const result = await commandHandler.command(Cmd.create, ['label', cardKey, labelName], Object.assign({}, options, program.opts()));
301
+ handleResponse(result);
302
+ });
303
+ // Create link subcommand
304
+ createCmd
305
+ .command('link')
306
+ .description('Create a link between two cards')
307
+ .argument('<source>', 'Source card key')
308
+ .argument('<destination>', 'Destination card key')
309
+ .argument('<linkType>', 'Link type to create')
310
+ .argument('[description]', 'Optional link description')
311
+ .action(async (source, destination, linkType, description, options) => {
312
+ const result = await commandHandler.command(Cmd.create, ['link', source, destination, linkType, description].filter(Boolean), Object.assign({}, options, program.opts()));
313
+ handleResponse(result);
314
+ });
315
+ // Create linkType subcommand
316
+ createCmd
317
+ .command('linkType')
318
+ .description('Create a new link type')
319
+ .argument('<name>', `Name for link type. ${nameGuideline}`)
320
+ .action(async (name, options) => {
321
+ const result = await commandHandler.command(Cmd.create, ['linkType', name], Object.assign({}, options, program.opts()));
322
+ handleResponse(result);
323
+ });
324
+ // Create project subcommand
325
+ createCmd
326
+ .command('project')
327
+ .description('Create a new project')
328
+ .argument('<name>', 'Project name')
329
+ .argument('<prefix>', 'Project prefix')
330
+ .argument('<path>', 'Path where to create the project')
331
+ .argument('[category]', 'Project category (optional)')
332
+ .argument('[description]', 'Project description (optional)')
282
333
  .option('-s, --skipModuleImport', 'Skip importing modules when creating a project')
283
- .action(async (type, target, parameter1, parameter2, parameter3, options) => {
284
- if (!type) {
285
- program.error(`missing required argument <type>`);
286
- }
287
- const resourceName = type.split('/').length === 3;
288
- function nameOfFirstArgument(type) {
289
- if (type === 'attachment' || type === 'label')
290
- return 'cardKey';
291
- if (type === 'card')
292
- return 'template';
293
- if (type === 'link')
294
- return 'source';
295
- return 'name';
296
- }
297
- function nameOfSecondArgument(type) {
298
- if (type === 'attachment')
299
- return 'fileName';
300
- if (type === 'cardType')
301
- return 'workflow';
302
- if (type === 'fieldType')
303
- return 'dataType';
304
- if (type === 'label')
305
- return 'labelName';
306
- if (type === 'link')
307
- return 'destination';
308
- if (type === 'project')
309
- return 'prefix';
310
- return type;
311
- }
312
- if (!target && !resourceName) {
313
- program.error(`missing required argument <${nameOfFirstArgument(type)}>`);
314
- }
315
- if (!resourceName &&
316
- !parameter1 &&
317
- type !== 'card' &&
318
- type !== 'graphModel' &&
319
- type !== 'graphView' &&
320
- type !== 'linkType' &&
321
- type !== 'report' &&
322
- type !== 'template' &&
323
- type !== 'workflow') {
324
- program.error(`missing required argument <${nameOfSecondArgument(type)}>`);
325
- }
326
- if (resourceName &&
327
- (type.includes('cardTypes') || type.includes('fieldTypes')) &&
328
- !target) {
329
- program.error(`missing required argument <${nameOfSecondArgument(type)}>`);
330
- }
331
- if (type === 'project') {
332
- if (!parameter2) {
333
- program.error(`missing required argument <path>`);
334
- }
335
- // Project path must be set to 'options' when creating a project.
336
- options.projectPath = parameter2;
337
- }
334
+ .action(async (name, prefix, path, category, description, options) => {
335
+ // Project path must be set to 'options' when creating a project
336
+ options.projectPath = path;
338
337
  const commandOptions = Object.assign({}, options, program.opts());
339
- const result = await commandHandler.command(Cmd.create, [type, target, parameter1, parameter2, parameter3], commandOptions);
340
- // Post-handling after creating a new project.
341
- if (type === 'project' &&
342
- !commandOptions.skipModuleImport &&
343
- result.statusCode === 200) {
338
+ const result = await commandHandler.command(Cmd.create, ['project', name, prefix, path, category || '', description || ''], commandOptions);
339
+ // Post-handling after creating a new project
340
+ if (!commandOptions.skipModuleImport && result.statusCode === 200) {
344
341
  try {
345
342
  // add default hub
346
- await commandHandler.command(Cmd.add, ['hub', DEFAULT_HUB], commandOptions);
343
+ const addHubResult = await commandHandler.command(Cmd.add, ['hub', DEFAULT_HUB], commandOptions);
344
+ if (addHubResult.statusCode !== 200) {
345
+ program.error(`Project creation failed: could not add default hub - ${addHubResult.message || 'Unknown error'}`);
346
+ }
347
347
  // fetch modules from default hub
348
- await commandHandler.command(Cmd.fetch, ['hubs'], commandOptions);
348
+ const fetchResult = await commandHandler.command(Cmd.fetch, ['hubs'], commandOptions);
349
+ if (fetchResult.statusCode !== 200) {
350
+ program.error(`Project creation failed: could not fetch hub data - ${fetchResult.message || 'Unknown error'}`);
351
+ }
349
352
  // show importable modules
350
353
  const choices = await importableModules(commandOptions);
351
354
  const selectedModules = await checkbox({
@@ -354,13 +357,21 @@ program
354
357
  choices,
355
358
  });
356
359
  // finally, import the selected modules
360
+ const failedModules = [];
357
361
  for (const module of selectedModules) {
358
- await commandHandler.command(Cmd.import, [
362
+ const importResult = await commandHandler.command(Cmd.import, [
359
363
  'module',
360
364
  module.location,
361
365
  module.branch ?? '',
362
366
  module.private ? 'true' : 'false',
363
367
  ], { ...commandOptions, skipMigrationLog: true });
368
+ if (importResult.statusCode !== 200) {
369
+ console.warn(`Failed to import module '${module.name}':`, importResult.message || 'Unknown error');
370
+ failedModules.push(module.name);
371
+ }
372
+ }
373
+ if (failedModules.length > 0) {
374
+ console.warn(`\nSome modules failed to import: ${failedModules.join(', ')}`);
364
375
  }
365
376
  }
366
377
  catch (error) {
@@ -372,24 +383,61 @@ program
372
383
  }
373
384
  handleResponse(result);
374
385
  });
386
+ // Create report subcommand
387
+ createCmd
388
+ .command('report')
389
+ .description('Create a new report')
390
+ .argument('<name>', `Name for report. ${nameGuideline}`)
391
+ .action(async (name, options) => {
392
+ const result = await commandHandler.command(Cmd.create, ['report', name], Object.assign({}, options, program.opts()));
393
+ handleResponse(result);
394
+ });
395
+ // Create template subcommand
396
+ createCmd
397
+ .command('template')
398
+ .description('Create a new template')
399
+ .argument('<name>', `Name for template. ${nameGuideline}`)
400
+ .argument('[content]', 'Template content. If empty, template is created with default values. Must conform to templateSchema.json')
401
+ .action(async (name, content, options) => {
402
+ const result = await commandHandler.command(Cmd.create, ['template', name, content].filter(Boolean), Object.assign({}, options, program.opts()));
403
+ handleResponse(result);
404
+ });
405
+ // Create workflow subcommand
406
+ createCmd
407
+ .command('workflow')
408
+ .description('Create a new workflow')
409
+ .argument('<name>', `Name for workflow. ${nameGuideline}`)
410
+ .argument('[content]', 'Workflow content. If empty, workflow is created with default values. Must conform to workflowSchema.json')
411
+ .action(async (name, content, options) => {
412
+ const result = await commandHandler.command(Cmd.create, ['workflow', name, content].filter(Boolean), Object.assign({}, options, program.opts()));
413
+ handleResponse(result);
414
+ });
415
+ // Create new resource subcommand
416
+ createCmd
417
+ .command('resource')
418
+ .description('Create a new resource')
419
+ .argument('<resourceName>', 'Resource name (e.g. <prefix>/<type>/<identifier>)')
420
+ .argument('[content]', 'Resource content. If empty, resource is created with default values. Must conform to its resource schema')
421
+ .action(async (resourceName, content, options) => {
422
+ const result = await commandHandler.command(Cmd.create, [resourceName, content].filter(Boolean), Object.assign({}, options, program.opts()));
423
+ handleResponse(result);
424
+ });
375
425
  // Edit command
376
- program
377
- .command('edit')
426
+ const editCmd = new CommandWithPath('edit')
378
427
  .description('Edit a card')
379
- .argument('<cardKey>', 'Card key of card')
380
- .option('-p, --project-path [path]', `${pathGuideline}`)
381
- .action(async (cardKey, options) => {
428
+ .argument('<cardKey>', 'Card key of card');
429
+ program.addCommand(editCmd);
430
+ editCmd.action(async (cardKey, options) => {
382
431
  const result = await commandHandler.command(Cmd.edit, [cardKey], Object.assign({}, options, program.opts()));
383
432
  handleResponse(result);
384
433
  });
385
434
  // Export command
386
- program
387
- .command('export')
388
- .description('Export a project or a card')
435
+ const exportCmd = new CommandWithPath('export').description('Export a project or a card');
436
+ program.addCommand(exportCmd);
437
+ exportCmd
389
438
  .addArgument(new Argument('<format>', 'Export format').choices(Object.values(ExportFormats)))
390
439
  .argument('<output>', 'Output path')
391
440
  .argument('[cardKey]', 'Export a specific card by card key. If omitted, exports the whole site.')
392
- .option('-p, --project-path [path]', `${pathGuideline}`)
393
441
  .option('-r, --recursive', 'Export cards under the specified card recursively')
394
442
  .option('-t, --title [title]', 'Title of the exported document(pdf export only)')
395
443
  .option('-n, --name [name]', 'Name of the exported document(pdf export only)')
@@ -405,13 +453,15 @@ program
405
453
  recursive: options.recursive,
406
454
  cardKey: cardKey,
407
455
  }, options.logLevel, (current, total) => {
408
- if (!progress.isActive) {
456
+ if (!progress.isActive && total !== undefined) {
409
457
  progress.start(total, 0);
410
458
  }
411
- if (progress.getTotal() !== total) {
459
+ if (total !== undefined && progress.getTotal() !== total) {
412
460
  progress.setTotal(total);
413
461
  }
414
- progress.update(current);
462
+ if (current !== undefined) {
463
+ progress.update(current);
464
+ }
415
465
  });
416
466
  progress.stop();
417
467
  if (errors.length > 0) {
@@ -436,22 +486,17 @@ program
436
486
  const result = await commandHandler.command(Cmd.export, [format, output, cardKey], Object.assign({}, options, program.opts()));
437
487
  handleResponse(result);
438
488
  });
439
- const fetchCmd = program
440
- .command('fetch')
441
- .description('Retrieve external data to local file system.')
442
- .option('-p, --project-path [path]', `${pathGuideline}`);
489
+ const fetchCmd = new CommandWithPath('fetch').description('Retrieve external data to local file system.');
490
+ program.addCommand(fetchCmd);
443
491
  fetchCmd
444
492
  .command('hubs')
445
493
  .description('Retrieves module lists from hubs')
446
- .option('-p, --project-path [path]', `${pathGuideline}`)
447
494
  .action(async (options) => {
448
495
  const result = await commandHandler.command(Cmd.fetch, ['hubs'], Object.assign({}, options, program.opts()));
449
496
  handleResponse(result);
450
497
  });
451
- const importCmd = program
452
- .command('import')
453
- .description('Import modules and data into the project')
454
- .option('-p, --project-path [path]', `${pathGuideline}`);
498
+ const importCmd = new CommandWithPath('import').description('Import modules and data into the project');
499
+ program.addCommand(importCmd);
455
500
  // Import module
456
501
  importCmd
457
502
  .command('module')
@@ -459,7 +504,6 @@ importCmd
459
504
  .argument('[source]', 'Path to import from or module name. If omitted, shows interactive selection')
460
505
  .argument('[branch]', 'When using git URL defines the branch. Default: main')
461
506
  .argument('[useCredentials]', 'When using git URL uses credentials for cloning. Default: false')
462
- .option('-p, --project-path [path]', `${pathGuideline}`)
463
507
  .action(async (source, branch, useCredentials, options) => {
464
508
  let resolvedSource = source;
465
509
  let resolvedBranch = branch;
@@ -525,20 +569,68 @@ importCmd
525
569
  .command('csv')
526
570
  .description('Imports cards from a csv file')
527
571
  .argument('<csvFile>', 'File to import from')
528
- .argument('[cardKey]', 'Card key of the parent. If defined, cards are created as children of this card')
529
- .option('-p, --project-path [path]', `${pathGuideline}`)
572
+ .argument('[cardKey]', 'Parent card key. If defined, cards are created as children of this card')
530
573
  .action(async (csvFile, cardKey, options) => {
531
574
  const result = await commandHandler.command(Cmd.import, ['csv', csvFile, cardKey], Object.assign({}, options, program.opts()));
532
575
  handleResponse(result);
533
576
  });
534
- // Move command
577
+ // Migrate command
535
578
  program
536
- .command('move')
579
+ .command('migrate')
580
+ .description('Migrate project schema to a newer version.')
581
+ .argument('[version]', 'Target schema version. If not provided, migrates to the latest version. Can only migrate one version at a time when specified.')
582
+ .option('-p, --project-path [path]', `${pathGuideline}`)
583
+ .option('-b, --backup <directory>', 'Create a backup before migration in the specified directory. Directory must exist.')
584
+ .option('-t, --timeout <minutes>', 'Timeout for migration in minutes (default: 2 minutes)', '2')
585
+ .action(async (version, options) => {
586
+ if (version) {
587
+ const versionNumber = parseInt(version);
588
+ if (isNaN(versionNumber)) {
589
+ console.error(`Error: migration version is not a number: '${version}'`);
590
+ process.exit(1);
591
+ }
592
+ if (versionNumber <= 0) {
593
+ console.error(`Error: migration version must be above zero: '${version}'`);
594
+ process.exit(1);
595
+ }
596
+ if (options.backup) {
597
+ options.backup = resolve(options.backup.toString().trim());
598
+ if (!existsSync(options.backup)) {
599
+ console.error(`Error: Backup directory does not exist: ${options.backup}`);
600
+ process.exit(1);
601
+ }
602
+ try {
603
+ await access(options.backup, constants.W_OK);
604
+ }
605
+ catch {
606
+ console.error(`Error: Cannot write to backup directory: ${options.backup}`);
607
+ process.exit(1);
608
+ }
609
+ if (!(await lstat(options.backup)).isDirectory()) {
610
+ console.error(`Error: Backup directory is a file: ${options.backup}`);
611
+ process.exit(1);
612
+ }
613
+ }
614
+ }
615
+ if (options.timeout !== undefined) {
616
+ const timeoutMinutes = Number(options.timeout);
617
+ if (isNaN(timeoutMinutes) || timeoutMinutes <= 0) {
618
+ console.error(`Error: Timeout must be a positive number`);
619
+ process.exit(1);
620
+ }
621
+ // Convert minutes to milliseconds
622
+ options.timeout = timeoutMinutes * 60 * 1000;
623
+ }
624
+ const result = await commandHandler.command(Cmd.migrate, [version], Object.assign({}, options, program.opts()));
625
+ handleResponse(result);
626
+ });
627
+ // Move command
628
+ const moveCmd = new CommandWithPath('move')
537
629
  .description('Moves a card from root to under another card, from under another card to root, or from under a one card to another.')
538
630
  .argument('[source]', 'Source Card key that needs to be moved')
539
- .argument('[destination]', 'Destination Card key where "source" is moved to. If moving to root, use "root"')
540
- .option('-p, --project-path [path]', `${pathGuideline}`)
541
- .action(async (source, destination, options) => {
631
+ .argument('[destination]', 'Destination Card key where "source" is moved to. If moving to root, use "root"');
632
+ program.addCommand(moveCmd);
633
+ moveCmd.action(async (source, destination, options) => {
542
634
  const result = await commandHandler.command(Cmd.move, [source, destination], Object.assign({}, options, program.opts()));
543
635
  handleResponse(result);
544
636
  });
@@ -549,16 +641,13 @@ program
549
641
  .action(async (dir) => {
550
642
  await previewSite(dir || '.', true);
551
643
  });
552
- const rank = program
553
- .command('rank')
554
- .description('Manage card ranking and ordering')
555
- .option('-p, --project-path [path]', `${pathGuideline}`);
644
+ const rank = new CommandWithPath('rank').description('Manage card ranking and ordering');
645
+ program.addCommand(rank);
556
646
  rank
557
647
  .command('card')
558
648
  .description('Set the rank of a card. Ranks define the order in which cards are shown.')
559
649
  .argument('<cardKey>', 'Card key of the card to be moved')
560
650
  .argument('<afterCardKey>', 'Card key of the card that the card should be after. Use "first" to rank the card first.')
561
- .option('-p, --project-path [path]', `${pathGuideline}`)
562
651
  .action(async (cardKey, afterCardKey, options) => {
563
652
  const result = await commandHandler.command(Cmd.rank, ['card', cardKey, afterCardKey], Object.assign({}, options, program.opts()));
564
653
  handleResponse(result);
@@ -566,22 +655,20 @@ rank
566
655
  rank
567
656
  .command('rebalance')
568
657
  .description('Rebalance the rank of all cards in the project. Can be also used, if ranks do not exist')
569
- .argument('[parentCardKey]', 'if null, rebalance the whole project, otherwise rebalance only the direct children of the card key')
570
- .option('-p, --project-path [path]', `${pathGuideline}`)
658
+ .argument('[parentCardKey]', 'if null, rebalance the whole project, otherwise rebalance only the direct children')
571
659
  .action(async (cardKey, options) => {
572
660
  const result = await commandHandler.command(Cmd.rank, ['rebalance', cardKey], Object.assign({}, options, program.opts()));
573
661
  handleResponse(result);
574
662
  });
575
663
  // Remove command
576
- program
577
- .command('remove')
578
- .description('Remove cards, resources and other project items')
664
+ const removeCmd = new CommandWithPath('remove').description('Remove cards, resources and other project items');
665
+ program.addCommand(removeCmd);
666
+ removeCmd
579
667
  .argument('<type>', `removable types: '${Parser.listTargets('remove').join("', '")}', or resource name (e.g. <prefix>/<type>/<identifier>)`, Parser.parseRemoveTypes)
580
668
  .argument('[parameter1]', 'Depends on context; see below for specific remove operation')
581
669
  .argument('[parameter2]', 'Depends on context; see below for specific remove operation')
582
670
  .argument('[parameter3]', 'Depends on context; see below for specific remove operation')
583
671
  .addHelpText('after', additionalHelpForRemove)
584
- .option('-p, --project-path [path]', `${pathGuideline}`)
585
672
  .action(async (type, parameter1, parameter2, parameter3, options) => {
586
673
  if (type) {
587
674
  if (!parameter1) {
@@ -620,36 +707,33 @@ program
620
707
  }
621
708
  });
622
709
  // Rename command
623
- program
624
- .command('rename')
710
+ const renameCmd = new CommandWithPath('rename')
625
711
  .description('Change project prefix and rename all the content with the new prefix')
626
- .argument('<to>', 'New project prefix')
627
- .option('-p, --project-path [path]', `${pathGuideline}`)
628
- .action(async (to, options) => {
712
+ .argument('<to>', 'New project prefix');
713
+ program.addCommand(renameCmd);
714
+ renameCmd.action(async (to, options) => {
629
715
  const result = await commandHandler.command(Cmd.rename, [to], Object.assign({}, options, program.opts()));
630
716
  handleResponse(result);
631
717
  });
632
718
  // Report command
633
- program
634
- .command('report')
719
+ const reportCmd = new CommandWithPath('report')
635
720
  .description('Runs a report')
636
721
  .argument('<parameters>', 'Path to parameters file. This file defines which report to run and what parameters to use.')
637
722
  .argument('[output]', 'Optional output file; if omitted output will be directed to stdout')
638
- .addOption(contextOption)
639
- .option('-p, --project-path [path]', `${pathGuideline}`)
640
- .action(async (parameters, output, options) => {
723
+ .addOption(contextOption);
724
+ program.addCommand(reportCmd);
725
+ reportCmd.action(async (parameters, output, options) => {
641
726
  const result = await commandHandler.command(Cmd.report, [parameters, output], Object.assign({}, options, program.opts()));
642
727
  handleResponse(result);
643
728
  });
644
729
  // Show command
645
- program
646
- .command('show')
647
- .description('Shows details from a project')
730
+ const showCmd = new CommandWithPath('show').description('Shows details from a project');
731
+ program.addCommand(showCmd);
732
+ showCmd
648
733
  .argument('<type>', `details can be seen from: ${Parser.listTargets('show').join(', ')}`, Parser.parseShowTypes)
649
734
  .argument('[typeDetail]', 'additional information about the requested type; for example a card key')
650
735
  .option('-d --details', 'Certain types (such as cards) can have additional details')
651
736
  .option('-a --showAll', 'Show all modules, irregardless if it has been imported or not. Only with "show importableModules"')
652
- .option('-p, --project-path [path]', `${pathGuideline}`)
653
737
  .option('-u --show-use', 'Show where resource is used. Only used with resources, otherwise will be ignored.')
654
738
  .action(async (type, typeDetail, options) => {
655
739
  if (type !== '') {
@@ -666,48 +750,43 @@ program
666
750
  }
667
751
  });
668
752
  // Transition command
669
- program
670
- .command('transition')
753
+ const transitionCmd = new CommandWithPath('transition')
671
754
  .description('Transition a card to the specified state')
672
755
  .argument('<cardKey>', 'card key of a card')
673
- .argument('<transition>', '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.')
674
- .option('-p, --project-path [path]', `${pathGuideline}`)
675
- .action(async (cardKey, transition, options) => {
756
+ .argument('<transition>', '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.');
757
+ program.addCommand(transitionCmd);
758
+ transitionCmd.action(async (cardKey, transition, options) => {
676
759
  const result = await commandHandler.command(Cmd.transition, [cardKey, transition], Object.assign({}, options, program.opts()));
677
760
  handleResponse(result);
678
761
  });
679
762
  // Update command
680
- program
681
- .command('update')
682
- .description('Update resource details')
763
+ const updateCmd = new CommandWithPath('update').description('Update resource details');
764
+ program.addCommand(updateCmd);
765
+ updateCmd
683
766
  .argument('<resourceName>', 'Resource name')
684
767
  .argument('<operation>', 'Type of change, either "add", "change", "rank" or "remove" ')
685
768
  .argument('<key>', 'Detail to be changed')
686
769
  .argument('<value>', 'Value for a detail')
687
770
  .argument('[newValue]', 'When using "change" define new value for detail.\nWhen using "remove" provide optional replacement value for removed value')
688
- .option('-m, --mapping-file [path]', 'Path to JSON file containing workflow state mapping (only used when changing workflow)')
689
- .option('-p, --project-path [path]', `${pathGuideline}`)
771
+ .option('-m, --mapping-file <path>', 'Path to JSON file containing workflow state mapping (only used when changing workflow)')
690
772
  .addHelpText('after', additionalHelpForUpdate)
691
773
  .action(async (resourceName, operation, key, value, newValue, options) => {
692
774
  const result = await commandHandler.command(Cmd.update, [resourceName, operation, key, value, newValue], Object.assign({}, options, program.opts()));
693
775
  handleResponse(result);
694
776
  });
695
777
  // Updates all modules, or specific named module in the project.
696
- program
697
- .command('update-modules')
778
+ const updateModulesCmd = new CommandWithPath('update-modules')
698
779
  .description('Updates to latest versions either all modules or a specific module')
699
- .argument('[moduleName]', 'Module name')
700
- .option('-p, --project-path [path]', `${pathGuideline}`)
701
- .action(async (moduleName, options) => {
780
+ .argument('[moduleName]', 'Module name');
781
+ program.addCommand(updateModulesCmd);
782
+ updateModulesCmd.action(async (moduleName, options) => {
702
783
  const result = await commandHandler.command(Cmd.updateModules, [moduleName], Object.assign({}, options, program.opts()), credentials());
703
784
  handleResponse(result);
704
785
  });
705
786
  // Validate command
706
- program
707
- .command('validate')
708
- .description('Validate project structure')
709
- .option('-p, --project-path [path]', `${pathGuideline}`)
710
- .action(async (options) => {
787
+ const validateCmd = new CommandWithPath('validate').description('Validate project structure');
788
+ program.addCommand(validateCmd);
789
+ validateCmd.action(async (options) => {
711
790
  const result = await commandHandler.command(Cmd.validate, [], Object.assign({}, options, program.opts()));
712
791
  handleResponse(result);
713
792
  });
@@ -716,12 +795,11 @@ program
716
795
  // There is 10 sec timeout on the prompt. If user does not reply, then
717
796
  // it is assumed that validation errors do not matter and application
718
797
  // start is resumed.
719
- program
720
- .command('app')
798
+ const appCmd = new CommandWithPath('app')
721
799
  .description('Starts the cyberismo app, accessible with a web browser at http://localhost:3000')
722
- .option('-w, --watch-resource-changes', 'Project watches changes in .cards folder resources')
723
- .option('-p, --project-path [path]', `${pathGuideline}`)
724
- .action(async (options) => {
800
+ .option('-w, --watch-resource-changes', 'Project watches changes in .cards folder resources');
801
+ program.addCommand(appCmd);
802
+ appCmd.action(async (options) => {
725
803
  // validate project
726
804
  const result = await commandHandler.command(Cmd.validate, [], Object.assign({}, options, program.opts()));
727
805
  if (!result.message) {