forceios 13.1.0 → 13.2.0-alpha.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/README.md CHANGED
@@ -39,21 +39,46 @@ forceios create
39
39
  --packagename=app package identifier (e.g. com.mycompany.myapp)
40
40
  --organization=organization name (your company's/organization's name)
41
41
  [--outputdir=output directory (leave empty for current directory)]
42
+ [--consumerkey=OAuth consumer key for the Salesforce External Client App or Connected App]
43
+ [--callbackurl=OAuth callback URL for the Salesforce External Client App or Connected App]
44
+ [--loginserver=Login server URL for the Salesforce org]
42
45
 
43
46
  OR
44
47
 
45
48
  # create an iOS native mobile application from a template
46
49
  forceios createwithtemplate
47
- --templaterepouri=template repo URI or Mobile SDK template name
50
+ [--templatesource=git repo URL (optionally with #branch) or local path to a templates suite (root must contain templates.json)]
51
+ [--templaterepouri=template repo URI or Mobile SDK template name]
52
+ [--template=template name within the templates suite (e.g. ReactNativeTemplate)]
48
53
  --appname=application name
49
54
  --packagename=app package identifier (e.g. com.mycompany.myapp)
50
55
  --organization=organization name (your company's/organization's name)
51
56
  [--outputdir=output directory (leave empty for current directory)]
57
+ [--consumerkey=OAuth consumer key for the Salesforce External Client App or Connected App]
58
+ [--callbackurl=OAuth callback URL for the Salesforce External Client App or Connected App]
59
+ [--loginserver=Login server URL for the Salesforce org]
52
60
 
53
61
  OR
54
62
 
55
- # list available Mobile SDK templates
63
+ # show version of Mobile SDK
64
+ forceios version
65
+
66
+ OR
67
+
68
+ # list available Mobile SDK templates to create an iOS native mobile application
56
69
  forceios listtemplates
70
+ [--templatesource=git repo URL (optionally with #branch) or local path to a templates suite (root must contain templates.json)]
71
+ [--doc=include verbose documentation from template.json files]
72
+ [--json=output response in JSON format]
73
+
74
+ OR
75
+
76
+ # list details for a specific Mobile SDK template to create an iOS native mobile application
77
+ forceios describetemplate
78
+ [--templatesource=git repo URL (optionally with #branch) or local path to a templates suite (root must contain templates.json)]
79
+ [--template=template name within the templates suite (e.g. ReactNativeTemplate)]
80
+ [--doc=include verbose documentation from template.json files]
81
+ [--json=output response in JSON format]
57
82
 
58
83
  OR
59
84
 
@@ -64,11 +89,6 @@ forceios checkconfig
64
89
 
65
90
  OR
66
91
 
67
- # show version of Mobile SDK
68
- forceios version
69
-
70
- OR
71
-
72
92
  forceios
73
93
  ```
74
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forceios",
3
- "version": "13.1.0",
3
+ "version": "13.2.0-alpha.0",
4
4
  "description": "Utilities for creating mobile apps based on the Salesforce Mobile SDK for iOS",
5
5
  "keywords": [ "mobilesdk", "ios", "salesforce", "mobile", "sdk" ],
6
6
  "homepage": "https://github.com/forcedotcom/SalesforceMobileSDK-iOS",
@@ -73,7 +73,8 @@ function getCommandExpanded(cli, commandName) {
73
73
  args: getArgsExpanded(cli, commandName),
74
74
  description: applyCli(command.description, cli),
75
75
  longDescription: applyCli(command.longDescription, cli),
76
- help: applyCli(command.help, cli)
76
+ help: applyCli(command.help, cli),
77
+ supportCustomFlags: command.supportCustomFlags
77
78
  };
78
79
  }
79
80
 
@@ -28,7 +28,7 @@
28
28
  var path = require('path'),
29
29
  shelljs = require('shelljs');
30
30
 
31
- var VERSION= '13.1.0';
31
+ var VERSION= '13.2.0-alpha.0';
32
32
 
33
33
  module.exports = {
34
34
  version: VERSION,
@@ -60,9 +60,9 @@ module.exports = {
60
60
  },
61
61
  cordova: {
62
62
  checkCmd: 'cordova -v',
63
- // pluginRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin#dev', // dev
63
+ pluginRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin#dev', // dev
64
64
  minVersion: '12.0.0',
65
- pluginRepoUri: 'salesforce-mobilesdk-cordova-plugin@v' + VERSION, // GA
65
+ // pluginRepoUri: 'salesforce-mobilesdk-cordova-plugin@v' + VERSION, // GA
66
66
  platformVersion: {
67
67
  ios: '7.1.1',
68
68
  android: '14.0.1'
@@ -79,8 +79,8 @@ module.exports = {
79
79
  android: 'Android Studio'
80
80
  },
81
81
 
82
- // templatesRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-Templates#dev', // dev
83
- templatesRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-Templates#v' + VERSION, // GA
82
+ templatesRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-Templates#dev', // dev
83
+ // templatesRepoUri: 'https://github.com/forcedotcom/SalesforceMobileSDK-Templates#v' + VERSION, // GA
84
84
 
85
85
  forceclis: {
86
86
  forceios: {
@@ -140,7 +140,7 @@ module.exports = {
140
140
  commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'describetemplate', 'checkconfig']
141
141
  }
142
142
  },
143
-
143
+
144
144
  args: {
145
145
  platform: {
146
146
  name: 'platform',
@@ -296,7 +296,7 @@ module.exports = {
296
296
  type: 'string',
297
297
  hidden: true
298
298
  },
299
- sdkDependencies: {
299
+ sdkDependencies: {
300
300
  name: 'sdkdependencies',
301
301
  description: 'override sdk dependencies',
302
302
  'char': 'd',
@@ -329,6 +329,39 @@ module.exports = {
329
329
  required: false,
330
330
  type: 'boolean',
331
331
  hidden: false
332
+ },
333
+ consumerKey: {
334
+ name: 'consumerkey',
335
+ 'char': 'c',
336
+ description: 'OAuth consumer key for the Salesforce External Client App or Connected App',
337
+ longDescription: 'The OAuth consumer key (client ID) for your Salesforce External Client App or Connected App. When provided, this will be automatically configured in the generated app.',
338
+ prompt: 'Enter your OAuth consumer key (leave empty to manually configure this value in the project\'s bootconfig file):',
339
+ error: cli => val => 'Invalid value for consumer key: \'' + val + '\'.',
340
+ validate: cli => val => val === undefined || val === '' || /\S+/.test(val),
341
+ required: false,
342
+ type: 'string'
343
+ },
344
+ callbackURL: {
345
+ name: 'callbackurl',
346
+ 'char': 'u',
347
+ description: 'OAuth callback URL for the Salesforce External Client App or Connected App',
348
+ longDescription: 'The OAuth callback URL (redirect URI) for your Salesforce External Client App or Connected App. When provided, this will be automatically configured in the generated app.',
349
+ prompt: 'Enter your OAuth callback URL (leave empty to manually configure this value in the project\'s bootconfig file):',
350
+ error: cli => val => 'Invalid value for callback URL: \'' + val + '\'.',
351
+ validate: cli => val => val === undefined || val === '' || /\S+/.test(val),
352
+ required: false,
353
+ type: 'string'
354
+ },
355
+ loginServer: {
356
+ name: 'loginserver',
357
+ 'char': 'l',
358
+ description: 'Login server URL for the Salesforce org',
359
+ longDescription: 'The login server URL for your Salesforce org (e.g. https://login.salesforce.com, https://test.salesforce.com, or custom domain). When provided, this will be automatically configured in the generated app.',
360
+ prompt: 'Enter your login server URL (leave empty for https://login.salesforce.com):',
361
+ error: cli => val => 'Invalid value for login server: \'' + val + '\'.',
362
+ validate: cli => val => val === undefined || val === '' || /\S+/.test(val),
363
+ required: false,
364
+ type: 'string'
332
365
  }
333
366
  },
334
367
 
@@ -342,6 +375,9 @@ module.exports = {
342
375
  'organization',
343
376
  cli.appTypes.indexOf('hybrid_remote') >=0 ? 'startPage' : null,
344
377
  'outputDir',
378
+ 'consumerKey',
379
+ 'callbackURL',
380
+ 'loginServer',
345
381
  'verbose',
346
382
  cli.name === 'forcehybrid' ? 'pluginRepoUri' : null,
347
383
  'sdkDependencies'
@@ -361,13 +397,17 @@ module.exports = {
361
397
  'organization',
362
398
  cli.appTypes.indexOf('hybrid_remote') >=0 ? 'startPage' : null,
363
399
  'outputDir',
400
+ 'consumerKey',
401
+ 'callbackURL',
402
+ 'loginServer',
364
403
  'verbose',
365
404
  cli.name === 'forcehybrid' ? 'pluginRepoUri' : null,
366
405
  'sdkDependencies'
367
406
  ].filter(x=>x!=null),
368
407
  description: cli => 'create ' + cli.purpose + ' from a template',
369
408
  longDescription: cli => 'Create ' + cli.purpose + ' from a template.',
370
- help: 'This command initiates creation of a new app based on the Mobile SDK template that you specify. The template can be a specialized app for your app type that Mobile SDK provides, or your own custom app that you\'ve configured to use as a template. See https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/ios_new_project_template.htm for information on custom templates.'
409
+ help: 'This command initiates creation of a new app based on the Mobile SDK template that you specify. The template can be a specialized app for your app type that Mobile SDK provides, or your own custom app that you\'ve configured to use as a template. See https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/ios_new_project_template.htm for information on custom templates.',
410
+ supportCustomFlags: true
371
411
  },
372
412
  version: {
373
413
  name: 'version',
@@ -32,7 +32,11 @@ var path = require('path'),
32
32
  configHelper = require('./configHelper'),
33
33
  prepareTemplate = require('./templateHelper').prepareTemplate,
34
34
  getSDKTemplateURI = require('./templateHelper').getSDKTemplateURI,
35
- fs = require('fs');
35
+ fs = require('fs'),
36
+ Ajv = require('ajv'),
37
+ COLOR = require('./outputColors'),
38
+ readJsonFile = require('./jsonChecker').readJsonFile,
39
+ JSON5 = require('json5');
36
40
 
37
41
  // Constant
38
42
  var SERVER_PROJECT_DIR = 'server';
@@ -147,75 +151,112 @@ function createAndroidAPI35Theme(projectDir) {
147
151
  function printDetails(config) {
148
152
  // Printing out details
149
153
  var details = ['Creating ' + config.platform.replace(',', ' and ') + ' ' + config.apptype + ' application using Salesforce Mobile SDK',
150
- ' with app name: ' + config.appname,
151
- ' package name: ' + config.packagename,
152
- ' organization: ' + config.organization,
154
+ ' with app name: ' + config.appname,
155
+ ' package name: ' + config.packagename,
156
+ ' organization: ' + config.organization,
153
157
  '',
154
- ' in: ' + config.projectPath,
158
+ ' in: ' + config.projectPath,
155
159
  '',
156
- ' from template repo: ' + config.templaterepouri
160
+ ' from template repo: ' + config.templaterepouri
157
161
  ];
158
162
 
159
163
  if (config.templatepath) {
160
- details = details.concat([' template path: ' + config.templatepath]);
164
+ details = details.concat([' template path: ' + config.templatepath]);
161
165
  }
162
166
 
163
167
 
164
168
  if (config.sdkdependencies) {
165
- details = details.concat([' sdk dependencies: ' + config.sdkdependencies]);
169
+ details = details.concat([' sdk dependencies: ' + config.sdkdependencies]);
170
+ }
171
+
172
+ // OAuth configuration details
173
+ if (config.consumerkey && config.consumerkey !== '__INSERT_CONSUMER_KEY_HERE__' && config.consumerkey.trim() !== '') {
174
+ details = details.concat([' consumer key: ' + config.consumerkey]);
175
+ }
176
+
177
+ if (config.callbackurl && config.callbackurl !== '__INSERT_CALLBACK_URL_HERE__' && config.callbackurl.trim() !== '') {
178
+ details = details.concat([' callback URL: ' + config.callbackurl]);
179
+ }
180
+
181
+ if (config.loginserver && config.loginserver.trim() !== '') {
182
+ details = details.concat([' login server: ' + config.loginserver]);
166
183
  }
167
184
 
168
185
  // Hybrid extra details
169
186
  if (config.apptype.indexOf('hybrid') >= 0) {
170
187
  if (config.apptype === 'hybrid_remote') {
171
- details = details.concat([' start page: ' + config.startpage]);
188
+ details = details.concat([' start page: ' + config.startpage]);
172
189
  }
173
190
 
174
- details = details.concat([' plugin repo: ' + config.cordovaPluginRepoUri]);
191
+ details = details.concat([' plugin repo: ' + config.cordovaPluginRepoUri]);
175
192
  }
176
193
 
177
194
  utils.logParagraph(details);
178
195
  }
179
196
 
197
+ //
198
+ // Check if valid OAuth configuration is provided
199
+ //
200
+ function hasValidOAuthConfig(config) {
201
+ return config.consumerkey && config.callbackurl &&
202
+ config.consumerkey !== '__INSERT_CONSUMER_KEY_HERE__' &&
203
+ config.callbackurl !== '__INSERT_CALLBACK_URL_HERE__' &&
204
+ config.consumerkey.trim() !== '' &&
205
+ config.callbackurl.trim() !== '' &&
206
+ (!config.loginserver || config.loginserver.trim() !== '');
207
+ }
208
+
180
209
  //
181
210
  // Print next steps
182
211
  //
183
- function printNextSteps(ide, projectPath, result) {
212
+ function printNextSteps(ide, projectPath, result, hasValidOAuth) {
184
213
  var workspacePath = path.join(projectPath, result.workspacePath);
185
214
  var bootconfigFile = path.join(projectPath, result.bootconfigFile);
186
215
 
216
+ var nextSteps = ['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
217
+ '',
218
+ 'Your application project is ready in ' + projectPath + '.',
219
+ 'To use your new application in ' + ide + ', do the following:',
220
+ ' - open ' + workspacePath + ' in ' + ide];
221
+
222
+ // Only show OAuth configuration instructions if valid OAuth config was not provided
223
+ if (!hasValidOAuth) {
224
+ nextSteps.push(' - make sure to plug your OAuth Client ID and Callback URI');
225
+ nextSteps.push(' into ' + bootconfigFile);
226
+ }
227
+
228
+ nextSteps.push(' - build and run');
229
+
187
230
  // Printing out next steps
188
- utils.logParagraph(['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
189
- '',
190
- 'Your application project is ready in ' + projectPath + '.',
191
- 'To use your new application in ' + ide + ', do the following:',
192
- ' - open ' + workspacePath + ' in ' + ide,
193
- ' - build and run',
194
- 'Before you ship, make sure to plug your OAuth Client ID and Callback URI,',
195
- 'and OAuth Scopes into ' + bootconfigFile,
196
- ]);
231
+ utils.logParagraph(nextSteps);
197
232
  };
198
233
 
199
234
  //
200
235
  // Print next steps for Native Login
201
236
  //
202
- function printNextStepsForNativeLogin(ide, projectPath, result) {
237
+ function printNextStepsForNativeLogin(ide, projectPath, result, hasValidOAuth) {
203
238
  var workspacePath = path.join(projectPath, result.workspacePath);
204
239
  var bootconfigFile = path.join(projectPath, result.bootconfigFile);
205
240
  var entryFile = (ide === 'XCode') ? 'SceneDelegate' : 'MainApplication';
206
241
 
242
+ var nextSteps = ['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
243
+ '',
244
+ 'Your application project is ready in ' + projectPath + '.',
245
+ 'To use your new application in ' + ide + ', do the following:',
246
+ ' - open ' + workspacePath + ' in ' + ide];
247
+
248
+ // Only show OAuth configuration instructions if valid OAuth config was not provided
249
+ if (!hasValidOAuth) {
250
+ nextSteps.push(' - Update the OAuth Client ID, Callback URI, and Community URL in ' + entryFile + ' class.');
251
+ nextSteps.push(' - Make sure to plug your OAuth Client ID and Callback URI into');
252
+ nextSteps.push(' into ' + bootconfigFile);
253
+ nextSteps.push(' since it is still be used for authentication if we fallback on the webview.');
254
+ }
255
+
256
+ nextSteps.push(' - build and run');
257
+
207
258
  // Printing out next steps
208
- utils.logParagraph(['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
209
- '',
210
- 'Your application project is ready in ' + projectPath + '.',
211
- 'To use your new application in ' + ide + ', do the following:',
212
- ' - open ' + workspacePath + ' in ' + ide,
213
- ' - Update the OAuth Client ID, Callback URI, and Community URL in ' + entryFile + ' class.',
214
- ' - build and run',
215
- 'Before you ship, make sure to plug your OAuth Client ID and Callback URI,',
216
- 'and OAuth Scopes into ' + bootconfigFile + ', since it is still used for',
217
- 'authentication if we fallback on the webview.'
218
- ]);
259
+ utils.logParagraph(nextSteps);
219
260
  }
220
261
 
221
262
  //
@@ -409,6 +450,8 @@ function actuallyCreateApp(forcecli, config) {
409
450
  }
410
451
  config.templateLocalPath = path.join(repoDir, config.templatepath);
411
452
 
453
+ validateCustomProperties(`${repoDir}/template.json`, config.templateProperties);
454
+
412
455
  // Override sdk dependencies in package.json if any were provided
413
456
  if (config.sdkdependencies) {
414
457
  overrideSdkDependencies(path.join(config.templateLocalPath, 'package.json'), config.sdkdependencies);
@@ -435,13 +478,14 @@ function actuallyCreateApp(forcecli, config) {
435
478
 
436
479
  // Printing next steps
437
480
  if (!(results instanceof Array)) { results = [results] };
481
+ var hasValidOAuth = hasValidOAuthConfig(config);
438
482
  for (var result of results) {
439
483
  var ide = SDK.ides[result.platform || config.platform.split(',')[0]];
440
484
 
441
485
  if (config.templatepath != undefined && config.templatepath.includes('NativeLogin')) {
442
- printNextStepsForNativeLogin(ide, config.projectPath, result);
486
+ printNextStepsForNativeLogin(ide, config.projectPath, result, hasValidOAuth);
443
487
  } else {
444
- printNextSteps(ide, config.projectPath, result);
488
+ printNextSteps(ide, config.projectPath, result, hasValidOAuth);
445
489
  }
446
490
  }
447
491
  printNextStepsForServerProjectIfNeeded(config.projectPath);
@@ -453,6 +497,33 @@ function actuallyCreateApp(forcecli, config) {
453
497
  }
454
498
  }
455
499
 
500
+ function validateCustomProperties(templateJsonPath, customProperties) {
501
+ // skip if template json file does not exist
502
+ if (!fs.existsSync(templateJsonPath)) {
503
+ return;
504
+ }
505
+
506
+ utils.log('Validating custom properties against schema...');
507
+ // Validate data against schema with AJV
508
+ const ajv = new Ajv({allErrors: true});
509
+ const schema = readJsonFile(templateJsonPath);
510
+ const validate = ajv.compile(schema);
511
+
512
+ const jsonToValidate = {
513
+ templatePrerequisites: { templateProperties: customProperties }
514
+ }
515
+ const valid = validate(jsonToValidate);
516
+
517
+ if (!valid) {
518
+ utils.logError('Custom properties validation failed:\n',
519
+ JSON.stringify(validate.errors, null, " "));
520
+ process.exit(1);
521
+ }
522
+
523
+ utils.logInfo('Custom properties are valid\n', COLOR.green);
524
+ }
525
+
456
526
  module.exports = {
457
- createApp
527
+ createApp,
528
+ validateCustomProperties
458
529
  };
@@ -71,5 +71,6 @@ function readJsonFile(filePath) {
71
71
  }
72
72
 
73
73
  module.exports = {
74
- validateJson
74
+ validateJson,
75
+ readJsonFile
75
76
  };
@@ -37,7 +37,7 @@ const logError = require('./utils').logError;
37
37
  const separateRepoUrlPathBranch = require('./utils').separateRepoUrlPathBranch;
38
38
  const os = require('os');
39
39
 
40
- const { SfdxError } = require('@salesforce/core');
40
+ const { SfError } = require('@salesforce/core');
41
41
  const { Command, flags } = require('@oclif/command');
42
42
 
43
43
  const namespace = 'mobilesdk';
@@ -160,9 +160,15 @@ class OclifAdapter extends Command {
160
160
  }
161
161
 
162
162
  execute(cli, klass) {
163
- const { flags } = this.parse(klass);
164
- if (OclifAdapter.validateCommand(cli, klass.command.name, flags)) {
165
- return OclifAdapter.runCommand(cli, klass.command.name, flags);
163
+ const { flags, argv : remainingArgs } = this.parse(klass);
164
+ const commandConfig = klass.command;
165
+ if (OclifAdapter.validateCommand(cli, commandConfig.name, flags)) {
166
+ // If the command supports custom flags and there are remaining arguments, parse the custom properties
167
+ if (klass.command.supportCustomFlags === true && remainingArgs.length > 0) {
168
+ const customProperties = OclifAdapter.getCustomProperties(remainingArgs);
169
+ flags.templateProperties = customProperties;
170
+ }
171
+ return OclifAdapter.runCommand(cli, commandConfig.name, flags);
166
172
  }
167
173
  }
168
174
 
@@ -201,13 +207,55 @@ class OclifAdapter extends Command {
201
207
  this.flags[name] = flag.quantity + '';
202
208
  break;
203
209
  } else {
204
- throw new SfdxError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
210
+ throw new SfError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
205
211
  }
206
212
  default:
207
- throw new SfdxError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
213
+ throw new SfError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
208
214
  }
209
215
  });
210
216
  }
217
+
218
+ static getCustomProperties(args) {
219
+ const properties = {};
220
+ const prefix = '--template-property-';
221
+
222
+ for (let i = 0; i < args.length; i++) {
223
+ const arg = args[i];
224
+
225
+ // Check if this argument starts with --template- but not --template-property-
226
+ if (arg.startsWith('--template-') && !arg.startsWith(prefix)) {
227
+ throw new SfError(
228
+ `Invalid template property flag: "${arg}". Template properties must be prefixed with "${prefix}".`,
229
+ 'InvalidTemplatePropertyPrefix'
230
+ );
231
+ }
232
+
233
+ // Check if this argument is a template property flag
234
+ if (arg.startsWith(prefix)) {
235
+ // Check if the value is provided with equals sign (--template-property-prop1=val1)
236
+ if (arg.includes('=')) {
237
+ const equalIndex = arg.indexOf('=');
238
+ const propertyName = arg.substring(prefix.length, equalIndex);
239
+ const propertyValue = arg.substring(equalIndex + 1);
240
+ properties[propertyName] = propertyValue;
241
+ } else {
242
+ // Extract the property name (everything after the prefix)
243
+ const propertyName = arg.substring(prefix.length);
244
+
245
+ // Check if there's a next argument and it's not another template property flag
246
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
247
+ // Use the next argument as the value
248
+ properties[propertyName] = args[i + 1];
249
+ i++; // Skip the value in the next iteration
250
+ }
251
+ // If the next argument is another flag or doesn't exist, skip this property
252
+ }
253
+ }
254
+ }
255
+
256
+ return properties;
257
+ }
258
+
211
259
  }
212
260
 
213
261
  OclifAdapter.getCommand = function (cli, commandName) {
@@ -29,7 +29,8 @@
29
29
  var path = require('path'),
30
30
  SDK = require('./constants'),
31
31
  utils = require('./utils'),
32
- fs = require('fs');
32
+ fs = require('fs'),
33
+ { constantCase } = require('change-case');
33
34
 
34
35
  //
35
36
  // Helper to prepare template
@@ -70,12 +71,15 @@ function getTemplates(cli, templateSourceOrRepoUri, includeDescriptions) {
70
71
  var applicableTemplates = templates
71
72
  .filter(template => cli.appTypes.includes(template.appType) && cli.platforms.filter(platform => template.platforms.includes(platform)).length > 0);
72
73
 
73
- // If descriptions are requested, add them to each template
74
- if (includeDescriptions) {
75
- applicableTemplates.forEach(function(template) {
76
- template.metadata = getTemplateMetadata(template.path, repoDir);
77
- });
78
- }
74
+ // Add metadata and custom properties to each template
75
+ applicableTemplates.forEach(function (template) {
76
+ const metadata = getTemplateMetadata(template.path, repoDir);
77
+ template.customProperties = getCustomProperties(metadata);
78
+ // If descriptions are requested, add metadata
79
+ if (includeDescriptions) {
80
+ template.metadata = metadata;
81
+ }
82
+ });
79
83
 
80
84
  // Cleanup
81
85
  utils.removeFile(tmpDir);
@@ -121,7 +125,7 @@ function getTemplateMetadata(templatePath, repoDir) {
121
125
  if (fs.existsSync(templateJsonPath)) {
122
126
  var templateJsonContent = fs.readFileSync(templateJsonPath, 'utf8');
123
127
  var templateData = JSON.parse(templateJsonContent);
124
-
128
+
125
129
  // Return all metadata properties from the template.json file
126
130
  // This makes the function more flexible for future use cases
127
131
  return templateData;
@@ -157,47 +161,65 @@ function getTemplate(templateName, templateSourceOrRepoUri, includeDescriptions)
157
161
 
158
162
  // Finding the specific template
159
163
  var template = templates.find(t => t.path === templateName);
160
-
164
+
161
165
  if (!template) {
162
166
  utils.removeFile(tmpDir);
163
167
  return null;
164
168
  }
165
-
169
+
170
+ const metadata = getTemplateMetadata(template.path, repoDir);
171
+ template.customProperties = getCustomProperties(metadata);
172
+
166
173
  // If descriptions are requested, add metadata
167
174
  if (includeDescriptions) {
168
- template.metadata = getTemplateMetadata(template.path, repoDir);
175
+ template.metadata = metadata;
169
176
  }
170
-
177
+
171
178
  // Cleanup
172
179
  utils.removeFile(tmpDir);
173
-
180
+
174
181
  return template;
175
182
  } catch (error) {
176
183
  return null;
177
184
  }
178
185
  }
179
186
 
187
+ //
188
+ // Build template command string
189
+ //
190
+ function buildTemplateCommand(source, commandPrefix, templatePath, extraRequiredArgs, customProperties) {
191
+ var sourceForCommand = source || SDK.templatesRepoUri;
192
+ var command = commandPrefix + ' --' + SDK.args.templateSource.name + '=' + sourceForCommand
193
+ + ' --' + SDK.args.template.name + '=' + templatePath;
194
+
195
+ if (extraRequiredArgs) {
196
+ command += ` ${extraRequiredArgs}`;
197
+ }
198
+
199
+ // Add custom command args if provided
200
+ if (customProperties) {
201
+ const customCommandArgs = getCustomCommandArgs(customProperties);
202
+ if (customCommandArgs.length > 0) {
203
+ command += ` ${customCommandArgs.join(' ')}`;
204
+ }
205
+ }
206
+
207
+ return command;
208
+ }
209
+
180
210
  //
181
211
  // Display template list with optional metadata
182
212
  //
183
213
  function displayTemplateList(templates, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
184
- var utils = require('./utils');
185
214
  var COLOR = require('./outputColors');
186
215
  var logInfo = utils.logInfo;
187
- var SDK = require('./constants');
188
216
 
189
217
  if (outputJson) {
190
218
  // Output in JSON format
191
219
  var jsonOutput = {
192
220
  repository: source || 'default',
193
- templates: templates.map(function(template, index) {
194
- var sourceForCommand = source || SDK.templatesRepoUri;
195
- var command = commandPrefix + ' --' + SDK.args.templateSource.name + '=' + sourceForCommand
196
- + ' --' + SDK.args.template.name + '=' + template.path;
197
-
198
- if (extraRequiredArgs) {
199
- command += ` ${extraRequiredArgs}`;
200
- }
221
+ templates: templates.map(function (template, index) {
222
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
201
223
 
202
224
  var jsonTemplate = {
203
225
  index: index + 1,
@@ -215,7 +237,7 @@ function displayTemplateList(templates, source, cliName, commandPrefix, includeD
215
237
  return jsonTemplate;
216
238
  })
217
239
  };
218
-
240
+
219
241
  logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
220
242
  return;
221
243
  }
@@ -232,17 +254,10 @@ function displayTemplateList(templates, source, cliName, commandPrefix, includeD
232
254
  var template = templates[i];
233
255
  logInfo((i + 1) + ') ' + template.description, COLOR.cyan);
234
256
 
235
- var sourceForCommand = source || SDK.templatesRepoUri;
236
- var command = commandPrefix + ' --' + SDK.args.templateSource.name + '=' + sourceForCommand
237
- + ' --' + SDK.args.template.name + '=' + template.path;
238
-
239
- // Add additional required args if provided
240
- if (extraRequiredArgs) {
241
- command += ` ${extraRequiredArgs}`;
242
- }
243
-
257
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
258
+
244
259
  logInfo(command, COLOR.magenta);
245
-
260
+
246
261
  // If descriptions are requested and available, show them
247
262
  if (includeDescriptions && template.metadata) {
248
263
  if (template.metadata.description) {
@@ -266,21 +281,14 @@ function displayTemplateList(templates, source, cliName, commandPrefix, includeD
266
281
  // Display detailed information about a single template
267
282
  //
268
283
  function displayTemplateDetail(template, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
269
- var utils = require('./utils');
270
284
  var COLOR = require('./outputColors');
271
285
  var logInfo = utils.logInfo;
272
- var SDK = require('./constants');
286
+
287
+ // create command usage
288
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
273
289
 
274
290
  if (outputJson) {
275
291
  // Output in JSON format
276
- var sourceForCommand = source || SDK.templatesRepoUri;
277
- var command = commandPrefix + ' --' + SDK.args.templateSource.name + '=' + sourceForCommand
278
- + ' --' + SDK.args.template.name + '=' + template.path;
279
-
280
- if (extraRequiredArgs) {
281
- command += ` ${extraRequiredArgs}`;
282
- }
283
-
284
292
  var jsonOutput = {
285
293
  repository: source || 'default',
286
294
  template: {
@@ -295,7 +303,7 @@ function displayTemplateDetail(template, source, cliName, commandPrefix, include
295
303
  if (includeDescriptions && template.metadata) {
296
304
  jsonOutput.template.metadata = template.metadata;
297
305
  }
298
-
306
+
299
307
  logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
300
308
  return;
301
309
  }
@@ -313,29 +321,42 @@ function displayTemplateDetail(template, source, cliName, commandPrefix, include
313
321
  logInfo('Description: ' + template.description, COLOR.cyan);
314
322
  logInfo('App Type: ' + template.appType, COLOR.cyan);
315
323
  logInfo('Platforms: ' + template.platforms.join(', '), COLOR.cyan);
324
+ logInfo('\nUsage:', COLOR.magenta);
316
325
 
317
326
  // Display command usage
318
- var sourceForCommand = source || SDK.templatesRepoUri;
319
- var command = commandPrefix + ' --' + SDK.args.templateSource.name + '=' + sourceForCommand
320
- + ' --' + SDK.args.template.name + '=' + template.path;
321
-
322
- // Add additional required args if provided
323
- if (extraRequiredArgs) {
324
- command += ` ${extraRequiredArgs}`;
325
- }
326
-
327
- logInfo('\nUsage:', COLOR.magenta);
328
327
  logInfo(command, COLOR.magenta);
329
-
328
+
330
329
  // If descriptions are requested and available, show raw JSON metadata
331
330
  if (includeDescriptions && template.metadata) {
332
331
  logInfo('\nTemplate Metadata (template.json):', COLOR.cyan);
333
332
  logInfo(JSON.stringify(template.metadata, null, 2), COLOR.white);
334
333
  }
335
-
334
+
336
335
  logInfo('');
337
336
  }
338
337
 
338
+ /**
339
+ * Get custom properties from metadata
340
+ * @param {Object} metadata - Template schema json object
341
+ * @returns {Object} Custom properties object
342
+ */
343
+ function getCustomProperties(metadata) {
344
+ return metadata?.properties?.templatePrerequisites?.properties?.templateProperties || null;
345
+ }
346
+
347
+ /**
348
+ * Get custom command args from custom properties metadata
349
+ * @param {JSON} customPropertiesMetadata - Custom properties metadata
350
+ * @returns {Array} Custom command args
351
+ */
352
+ function getCustomCommandArgs(customPropertiesMetadata) {
353
+ const properties = customPropertiesMetadata?.properties;
354
+ if (!properties) {
355
+ return [];
356
+ }
357
+ return Object.keys(properties).map(key => `--template-property-${key}=<${constantCase(key)}>`);
358
+ }
359
+
339
360
  module.exports = {
340
361
  prepareTemplate,
341
362
  getTemplates,
@@ -343,5 +364,7 @@ module.exports = {
343
364
  getAppTypeFromTemplate,
344
365
  getTemplateMetadata,
345
366
  displayTemplateList,
346
- displayTemplateDetail
367
+ displayTemplateDetail,
368
+ getCustomProperties,
369
+ getCustomCommandArgs
347
370
  };