forceios 13.0.2 → 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.0.2",
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",
@@ -32,33 +32,37 @@ var path = require('path'),
32
32
  COLOR = require('./outputColors'),
33
33
  commandLineUtils = require('./commandLineUtils'),
34
34
  logInfo = require('./utils').logInfo,
35
+ separateRepoUrlPathBranch = require('./utils').separateRepoUrlPathBranch,
35
36
  getTemplates = require('./templateHelper').getTemplates,
37
+ getTemplate = require('./templateHelper').getTemplate,
38
+ displayTemplateList = require('./templateHelper').displayTemplateList,
39
+ displayTemplateDetail = require('./templateHelper').displayTemplateDetail,
36
40
  validateJson = require('./jsonChecker').validateJson;
37
41
 
38
42
  function applyCli(f, cli) {
39
- return typeof f === 'function' ? f(cli): f;
43
+ return typeof f === 'function' ? f(cli) : f;
40
44
  }
41
45
 
42
46
  function getArgsExpanded(cli, commandName) {
43
47
  var argNames = applyCli(SDK.commands[commandName].args, cli);
44
48
  return argNames
45
49
  .map(argName => SDK.args[argName])
46
- .map(arg =>
47
- ({
48
- name: arg.name,
49
- 'char': arg.char,
50
- description: applyCli(arg.description, cli),
51
- longDescription: applyCli(arg.longDescription, cli),
52
- prompt: applyCli(arg.prompt, cli),
53
- error: applyCli(arg.error, cli),
54
- validate: applyCli(arg.validate, cli),
55
- promptIf: arg.promptIf,
56
- required: arg.required === undefined ? true : arg.required,
57
- hasValue: arg.hasValue === undefined ? true : arg.hasValue,
58
- hidden: applyCli(arg.hidden, cli),
59
- type: arg.type
60
- })
61
- );
50
+ .map(arg =>
51
+ ({
52
+ name: arg.name,
53
+ 'char': arg.char,
54
+ description: applyCli(arg.description, cli),
55
+ longDescription: applyCli(arg.longDescription, cli),
56
+ prompt: applyCli(arg.prompt, cli),
57
+ error: applyCli(arg.error, cli),
58
+ validate: applyCli(arg.validate, cli),
59
+ promptIf: arg.promptIf,
60
+ required: arg.required === undefined ? true : arg.required,
61
+ hasValue: arg.hasValue === undefined ? true : arg.hasValue,
62
+ hidden: applyCli(arg.hidden, cli),
63
+ type: arg.type
64
+ })
65
+ );
62
66
 
63
67
  }
64
68
 
@@ -69,7 +73,8 @@ function getCommandExpanded(cli, commandName) {
69
73
  args: getArgsExpanded(cli, commandName),
70
74
  description: applyCli(command.description, cli),
71
75
  longDescription: applyCli(command.longDescription, cli),
72
- help: applyCli(command.help, cli)
76
+ help: applyCli(command.help, cli),
77
+ supportCustomFlags: command.supportCustomFlags
73
78
  };
74
79
  }
75
80
 
@@ -81,28 +86,32 @@ function readConfig(args, cli, handler) {
81
86
  var processorList = null;
82
87
 
83
88
  switch (commandName || '') {
84
- case SDK.commands.version.name:
85
- printVersion(cli);
86
- process.exit(0);
87
- break;
88
- case SDK.commands.create.name:
89
- case SDK.commands.createwithtemplate.name:
90
- processorList = buildArgsProcessorList(cli, commandName);
91
- commandLineUtils.processArgsInteractive(commandLineArgs, processorList, handler);
92
- break;
93
- case SDK.commands.checkconfig.name:
94
- processorList = buildArgsProcessorList(cli, commandName);
95
- commandLineUtils.processArgsInteractive(commandLineArgs, processorList, function (config) {
96
- validateJson(config.configpath, config.configtype);
97
- });
98
- break;
99
- case SDK.commands.listtemplates.name:
100
- listTemplates(cli);
101
- process.exit(0);
102
- break;
103
- default:
104
- usage(cli);
105
- process.exit(1);
89
+ case SDK.commands.version.name:
90
+ printVersion(cli);
91
+ process.exit(0);
92
+ break;
93
+ case SDK.commands.create.name:
94
+ case SDK.commands.createwithtemplate.name:
95
+ processorList = buildArgsProcessorList(cli, commandName);
96
+ commandLineUtils.processArgsInteractive(commandLineArgs, processorList, handler);
97
+ break;
98
+ case SDK.commands.checkconfig.name:
99
+ processorList = buildArgsProcessorList(cli, commandName);
100
+ commandLineUtils.processArgsInteractive(commandLineArgs, processorList, function (config) {
101
+ validateJson(config.configpath, config.configtype);
102
+ });
103
+ break;
104
+ case SDK.commands.listtemplates.name:
105
+ listTemplates(cli, commandLineArgs);
106
+ process.exit(0);
107
+ break;
108
+ case SDK.commands.describetemplate.name:
109
+ describeTemplate(cli, commandLineArgs);
110
+ process.exit(0);
111
+ break;
112
+ default:
113
+ usage(cli);
114
+ process.exit(1);
106
115
  };
107
116
 
108
117
 
@@ -115,20 +124,76 @@ function printVersion(cli) {
115
124
  function printArgs(cli, commandName) {
116
125
  getArgsExpanded(cli, commandName)
117
126
  .filter(arg => !arg.hidden)
118
- .forEach(arg => logInfo(' ' + (!arg.required ? '[' : '') + '--' + arg.name + '=' + arg.description + (!arg.required ? ']' : ''), COLOR.magenta));
127
+ .forEach(arg => logInfo(' ' + (!arg.required ? '[' : '') + '--' + arg.name + '=' + arg.description + (!arg.required ? ']' : ''), COLOR.magenta));
128
+ }
129
+
130
+ function listTemplates(cli, commandLineArgs) {
131
+ var cliName = cli.name;
132
+
133
+ // Parse command line arguments to extract templatesource or templaterepouri
134
+ var templateSource = null;
135
+ var templateRepoUri = null;
136
+ var includeDescriptions = false;
137
+ var outputJson = false;
138
+ if (commandLineArgs && commandLineArgs.length > 0) {
139
+ try {
140
+ var argsMap = commandLineUtils.parseArgs(commandLineArgs);
141
+ templateSource = argsMap[SDK.args.templateSource.name];
142
+ templateRepoUri = argsMap[SDK.args.templateRepoUri.name];
143
+ includeDescriptions = argsMap.hasOwnProperty(SDK.args.doc.name);
144
+ outputJson = argsMap.hasOwnProperty(SDK.args.json.name);
145
+ } catch (error) {
146
+ // If argument parsing fails, continue without templateRepoUri
147
+ }
148
+ }
149
+
150
+ var source = templateSource || templateRepoUri;
151
+ var applicableTemplates = getTemplates(cli, source, includeDescriptions);
152
+
153
+ // Use shared display function
154
+ var commandPrefix = cliName + ' ' + SDK.commands.createwithtemplate.name;
155
+ displayTemplateList(applicableTemplates, source, cliName, commandPrefix, includeDescriptions, null, outputJson);
119
156
  }
120
157
 
121
- function listTemplates(cli) {
158
+ function describeTemplate(cli, commandLineArgs) {
122
159
  var cliName = cli.name;
123
- var applicableTemplates = getTemplates(cli);
124
160
 
125
- logInfo('\nAvailable templates:\n', COLOR.cyan);
126
- for (var i=0; i<applicableTemplates.length; i++) {
127
- var template = applicableTemplates[i];
128
- logInfo((i+1) + ') ' + template.description, COLOR.cyan);
129
- logInfo(cliName + ' ' + SDK.commands.createwithtemplate.name + ' --' + SDK.args.templateRepoUri.name + '=' + template.path, COLOR.magenta);
161
+ // Parse command line arguments to extract templatesource, template, and doc
162
+ var templateSource = null;
163
+ var templateRepoUri = null;
164
+ var templateName = null;
165
+ var includeDescriptions = false;
166
+ var outputJson = false;
167
+ if (commandLineArgs && commandLineArgs.length > 0) {
168
+ try {
169
+ var argsMap = commandLineUtils.parseArgs(commandLineArgs);
170
+ templateSource = argsMap[SDK.args.templateSource.name];
171
+ templateRepoUri = argsMap[SDK.args.templateRepoUri.name];
172
+ templateName = argsMap[SDK.args.template.name];
173
+ includeDescriptions = argsMap.hasOwnProperty(SDK.args.doc.name);
174
+ outputJson = argsMap.hasOwnProperty(SDK.args.json.name);
175
+ } catch (error) {
176
+ // If argument parsing fails, continue without templateRepoUri
177
+ }
130
178
  }
131
- logInfo('');
179
+
180
+ // Check if template name is provided
181
+ if (!templateName) {
182
+ logInfo('Error: Template name is required. Use --template to specify the template name.', COLOR.red);
183
+ process.exit(1);
184
+ }
185
+
186
+ var source = templateSource || templateRepoUri;
187
+ var template = getTemplate(templateName, source, includeDescriptions);
188
+
189
+ if (!template) {
190
+ logInfo('Error: Template "' + templateName + '" not found.', COLOR.red);
191
+ process.exit(1);
192
+ }
193
+
194
+ // Use shared display function
195
+ var commandPrefix = cliName + ' ' + SDK.commands.createwithtemplate.name;
196
+ displayTemplateDetail(template, source, cliName, commandPrefix, includeDescriptions, null, outputJson);
132
197
  }
133
198
 
134
199
  function usage(cli) {
@@ -184,7 +249,7 @@ function buildArgsProcessorList(cli, commandName) {
184
249
  // * preprocessor: function or null
185
250
  //
186
251
  function addProcessorFor(argProcessorList, argName, prompt, error, validation, preprocessor) {
187
- argProcessorList.addArgProcessor(argName, prompt, function(val) {
252
+ argProcessorList.addArgProcessor(argName, prompt, function (val) {
188
253
  val = val.trim();
189
254
 
190
255
  // validation is either a function or null
@@ -28,7 +28,7 @@
28
28
  var path = require('path'),
29
29
  shelljs = require('shelljs');
30
30
 
31
- var VERSION= '13.0.2';
31
+ var VERSION= '13.2.0-alpha.0';
32
32
 
33
33
  module.exports = {
34
34
  version: VERSION,
@@ -60,12 +60,12 @@ 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
- android: '13.0.0'
68
+ android: '14.0.1'
69
69
  }
70
70
  },
71
71
  sf: {
@@ -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: {
@@ -95,7 +95,7 @@ module.exports = {
95
95
  'native': 'iOSNativeTemplate',
96
96
  'native_swift': 'iOSNativeSwiftTemplate'
97
97
  },
98
- commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'checkconfig']
98
+ commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'describetemplate', 'checkconfig']
99
99
  },
100
100
  forcedroid: {
101
101
  name: 'forcedroid',
@@ -109,7 +109,7 @@ module.exports = {
109
109
  'native': 'AndroidNativeTemplate',
110
110
  'native_kotlin': 'AndroidNativeKotlinTemplate'
111
111
  },
112
- commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'checkconfig']
112
+ commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'describetemplate', 'checkconfig']
113
113
  },
114
114
  forcehybrid: {
115
115
  name: 'forcehybrid',
@@ -123,7 +123,7 @@ module.exports = {
123
123
  'hybrid_local': 'HybridLocalTemplate',
124
124
  'hybrid_remote': 'HybridRemoteTemplate'
125
125
  },
126
- commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'checkconfig']
126
+ commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'describetemplate', 'checkconfig']
127
127
  },
128
128
  forcereact: {
129
129
  name: 'forcereact',
@@ -137,10 +137,10 @@ module.exports = {
137
137
  'react_native': 'ReactNativeTemplate',
138
138
  'react_native_typescript': 'ReactNativeTypeScriptTemplate'
139
139
  },
140
- commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'checkconfig']
140
+ commands: ['create', 'createwithtemplate', 'version', 'listtemplates', 'describetemplate', 'checkconfig']
141
141
  }
142
142
  },
143
-
143
+
144
144
  args: {
145
145
  platform: {
146
146
  name: 'platform',
@@ -171,6 +171,34 @@ module.exports = {
171
171
  prompt: 'Enter URI of repo containing template application or a Mobile SDK template name:',
172
172
  error: cli => val => 'Invalid value for template repo uri: \'' + val + '\'.',
173
173
  validate: cli => val => /^\S+$/.test(val),
174
+ promptIf: otherArgs => !otherArgs.templatesource,
175
+ required: false,
176
+ type: 'string'
177
+ },
178
+ templateSource: {
179
+ name: 'templatesource',
180
+ 'char': 'S',
181
+ description: 'git repo URL (optionally with #branch) or local path to a templates suite (root must contain templates.json)',
182
+ longDescription: 'Location of a suite of templates. Can be a git URL with optional #branch suffix or a local filesystem path whose root contains templates.json.',
183
+ prompt: 'Enter git URL or local path to your templates suite:',
184
+ error: cli => val => 'Invalid value for template source: \'' + val + '\'.',
185
+ validate: cli => val => /\S+/.test(val),
186
+ // Process only when explicitly provided to avoid prompting during interactive flows
187
+ promptIf: otherArgs => typeof otherArgs.templatesource !== 'undefined',
188
+ required: false,
189
+ type: 'string'
190
+ },
191
+ template: {
192
+ name: 'template',
193
+ 'char': 'm',
194
+ description: 'template name within the templates suite (e.g. ReactNativeTemplate)',
195
+ longDescription: 'Name of the template to use from the suite specified by --templatesource. Should match the directory name or the path field in templates.json.',
196
+ prompt: 'Enter the template name from your template source:',
197
+ error: cli => val => 'Invalid value for template: \'' + val + '\'.',
198
+ validate: cli => val => /\S+/.test(val),
199
+ // Only prompt for template when a templatesource is provided
200
+ promptIf: otherArgs => !!otherArgs.templatesource,
201
+ required: false,
174
202
  type: 'string'
175
203
  },
176
204
  appName: {
@@ -268,7 +296,7 @@ module.exports = {
268
296
  type: 'string',
269
297
  hidden: true
270
298
  },
271
- sdkDependencies: {
299
+ sdkDependencies: {
272
300
  name: 'sdkdependencies',
273
301
  description: 'override sdk dependencies',
274
302
  'char': 'd',
@@ -277,7 +305,64 @@ module.exports = {
277
305
  required: false,
278
306
  type: 'string',
279
307
  hidden: true
280
- },
308
+ },
309
+ doc: {
310
+ name: 'doc',
311
+ 'char': 'D',
312
+ description: 'include verbose documentation from template.json files',
313
+ longDescription: 'When specified, includes detailed metadata from each template\'s template.json file if available (displayName, description, useCase, features, complexity).',
314
+ prompt: null,
315
+ error: cli => val => 'Invalid value for doc flag: \'' + val + '\'.',
316
+ validate: cli => val => true, // Boolean flag, no validation needed
317
+ required: false,
318
+ type: 'boolean',
319
+ hidden: false
320
+ },
321
+ json: {
322
+ name: 'json',
323
+ 'char': 'j',
324
+ description: 'output response in JSON format',
325
+ longDescription: 'When specified, outputs the response in JSON format instead of human-readable text. Useful for programmatic consumption.',
326
+ prompt: null,
327
+ error: cli => val => 'Invalid value for json flag: \'' + val + '\'.',
328
+ validate: cli => val => true, // Boolean flag, no validation needed
329
+ required: false,
330
+ type: 'boolean',
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'
365
+ }
281
366
  },
282
367
 
283
368
  commands: {
@@ -290,6 +375,9 @@ module.exports = {
290
375
  'organization',
291
376
  cli.appTypes.indexOf('hybrid_remote') >=0 ? 'startPage' : null,
292
377
  'outputDir',
378
+ 'consumerKey',
379
+ 'callbackURL',
380
+ 'loginServer',
293
381
  'verbose',
294
382
  cli.name === 'forcehybrid' ? 'pluginRepoUri' : null,
295
383
  'sdkDependencies'
@@ -301,19 +389,25 @@ module.exports = {
301
389
  createwithtemplate: {
302
390
  name: 'createwithtemplate',
303
391
  args: cli => [cli.platforms.length > 1 ? 'platform' : null,
392
+ 'templateSource',
304
393
  'templateRepoUri',
394
+ 'template',
305
395
  'appName',
306
396
  'packageName',
307
397
  'organization',
308
398
  cli.appTypes.indexOf('hybrid_remote') >=0 ? 'startPage' : null,
309
399
  'outputDir',
400
+ 'consumerKey',
401
+ 'callbackURL',
402
+ 'loginServer',
310
403
  'verbose',
311
404
  cli.name === 'forcehybrid' ? 'pluginRepoUri' : null,
312
- 'sdkDependencies'
405
+ 'sdkDependencies'
313
406
  ].filter(x=>x!=null),
314
407
  description: cli => 'create ' + cli.purpose + ' from a template',
315
408
  longDescription: cli => 'Create ' + cli.purpose + ' from a template.',
316
- 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
317
411
  },
318
412
  version: {
319
413
  name: 'version',
@@ -324,10 +418,17 @@ module.exports = {
324
418
  },
325
419
  listtemplates: {
326
420
  name: 'listtemplates',
327
- args: [],
421
+ args: ['templateSource', 'doc', 'json'],
328
422
  description: cli => 'list available Mobile SDK templates to create ' + cli.purpose,
329
423
  longDescription: cli => 'List available Mobile SDK templates to create ' + cli.purpose + '.',
330
- help: 'This command displays the list of available Mobile SDK templates. You can copy repo paths from the output for use with the createwithtemplate command.'
424
+ help: 'This command displays the list of available Mobile SDK templates. You can copy repo paths from the output for use with the createwithtemplate command. Use --templatesource to specify a custom template repository or leave blank to use the default template repository. Use --doc to include detailed metadata from template.json files (displayName, description, useCase, features, complexity). Use --json to output the response in JSON format.'
425
+ },
426
+ describetemplate: {
427
+ name: 'describetemplate',
428
+ args: ['templateSource', 'template', 'doc', 'json'],
429
+ description: cli => 'list details for a specific Mobile SDK template to create ' + cli.purpose,
430
+ longDescription: cli => 'List details for a specific Mobile SDK template to create ' + cli.purpose + '.',
431
+ help: 'This command displays detailed information about a specific Mobile SDK template. Use --templatesource to specify a custom template repository or leave blank to use the default template repository. Use --template to specify the template name. Use --doc to include verbose metadata from template.json files. Use --json to output the response in JSON format.'
331
432
  },
332
433
  checkconfig: {
333
434
  name: 'checkconfig',
@@ -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';
@@ -109,85 +113,150 @@ function createHybridApp(config) {
109
113
  // Run cordova prepare
110
114
  utils.runProcessThrowError('cordova prepare', config.projectDir);
111
115
 
116
+ // Add theme for Android API 35
117
+ if (config.platform.split(',').includes('android')) {
118
+ createAndroidAPI35Theme(config.projectDir);
119
+ }
120
+
112
121
  // Done
113
122
  return prepareResult;
114
123
  }
115
124
 
125
+ //
126
+ // Add Android API 35 theme file
127
+ //
128
+ function createAndroidAPI35Theme(projectDir) {
129
+ const dirPath = path.join(projectDir, 'platforms', 'android', 'app', 'src', 'main', 'res', 'values-v35');
130
+ const filePath = path.join(dirPath, 'themes.xml');
131
+ const fileContents = `<?xml version='1.0' encoding='utf-8'?>
132
+ <resources>
133
+ <!-- Override for API 35+ to fix white status bar with white icons issue -->
134
+ <style name="SalesforceSDK_SplashScreen" parent="Theme.SplashScreen.IconBackground">
135
+ <item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
136
+ <!-- Use dark icons on light status bar background -->
137
+ <item name="android:windowLightStatusBar">true</item>
138
+ </style>
139
+ </resources>`
140
+
141
+ // Ensure the directory exists
142
+ utils.mkDirIfNeeded(dirPath);
143
+
144
+ // Write the file
145
+ fs.writeFileSync(filePath, fileContents, 'utf8');
146
+ }
147
+
116
148
  //
117
149
  // Print details
118
150
  //
119
151
  function printDetails(config) {
120
152
  // Printing out details
121
153
  var details = ['Creating ' + config.platform.replace(',', ' and ') + ' ' + config.apptype + ' application using Salesforce Mobile SDK',
122
- ' with app name: ' + config.appname,
123
- ' package name: ' + config.packagename,
124
- ' organization: ' + config.organization,
154
+ ' with app name: ' + config.appname,
155
+ ' package name: ' + config.packagename,
156
+ ' organization: ' + config.organization,
125
157
  '',
126
- ' in: ' + config.projectPath,
158
+ ' in: ' + config.projectPath,
127
159
  '',
128
- ' from template repo: ' + config.templaterepouri
160
+ ' from template repo: ' + config.templaterepouri
129
161
  ];
130
162
 
131
163
  if (config.templatepath) {
132
- details = details.concat([' template path: ' + config.templatepath]);
164
+ details = details.concat([' template path: ' + config.templatepath]);
133
165
  }
134
166
 
135
167
 
136
168
  if (config.sdkdependencies) {
137
- 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]);
138
183
  }
139
184
 
140
185
  // Hybrid extra details
141
186
  if (config.apptype.indexOf('hybrid') >= 0) {
142
187
  if (config.apptype === 'hybrid_remote') {
143
- details = details.concat([' start page: ' + config.startpage]);
188
+ details = details.concat([' start page: ' + config.startpage]);
144
189
  }
145
190
 
146
- details = details.concat([' plugin repo: ' + config.cordovaPluginRepoUri]);
191
+ details = details.concat([' plugin repo: ' + config.cordovaPluginRepoUri]);
147
192
  }
148
193
 
149
194
  utils.logParagraph(details);
150
195
  }
151
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
+
152
209
  //
153
210
  // Print next steps
154
211
  //
155
- function printNextSteps(ide, projectPath, result) {
212
+ function printNextSteps(ide, projectPath, result, hasValidOAuth) {
156
213
  var workspacePath = path.join(projectPath, result.workspacePath);
157
214
  var bootconfigFile = path.join(projectPath, result.bootconfigFile);
158
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
+
159
230
  // Printing out next steps
160
- utils.logParagraph(['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
161
- '',
162
- 'Your application project is ready in ' + projectPath + '.',
163
- 'To use your new application in ' + ide + ', do the following:',
164
- ' - open ' + workspacePath + ' in ' + ide,
165
- ' - build and run',
166
- 'Before you ship, make sure to plug your OAuth Client ID and Callback URI,',
167
- 'and OAuth Scopes into ' + bootconfigFile,
168
- ]);
231
+ utils.logParagraph(nextSteps);
169
232
  };
170
233
 
171
234
  //
172
235
  // Print next steps for Native Login
173
236
  //
174
- function printNextStepsForNativeLogin(ide, projectPath, result) {
237
+ function printNextStepsForNativeLogin(ide, projectPath, result, hasValidOAuth) {
175
238
  var workspacePath = path.join(projectPath, result.workspacePath);
176
239
  var bootconfigFile = path.join(projectPath, result.bootconfigFile);
177
240
  var entryFile = (ide === 'XCode') ? 'SceneDelegate' : 'MainApplication';
178
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
+
179
258
  // Printing out next steps
180
- utils.logParagraph(['Next steps' + (result.platform ? ' for ' + result.platform : '') + ':',
181
- '',
182
- 'Your application project is ready in ' + projectPath + '.',
183
- 'To use your new application in ' + ide + ', do the following:',
184
- ' - open ' + workspacePath + ' in ' + ide,
185
- ' - Update the OAuth Client ID, Callback URI, and Community URL in ' + entryFile + ' class.',
186
- ' - build and run',
187
- 'Before you ship, make sure to plug your OAuth Client ID and Callback URI,',
188
- 'and OAuth Scopes into ' + bootconfigFile + ', since it is still used for',
189
- 'authentication if we fallback on the webview.'
190
- ]);
259
+ utils.logParagraph(nextSteps);
191
260
  }
192
261
 
193
262
  //
@@ -324,13 +393,41 @@ function actuallyCreateApp(forcecli, config) {
324
393
  config.version = SDK.version;
325
394
 
326
395
  // Figuring out template repo uri and path
327
- if (config.templaterepouri) {
328
- if (!config.templaterepouri.startsWith("https://")) {
396
+ let localTemplatesRoot;
397
+ if (config.templatesource) {
398
+ const source = config.templatesource;
399
+ if (fs.existsSync(source)) {
400
+ // Local path to templates suite
401
+ localTemplatesRoot = path.resolve(source);
402
+ if (!config.template) {
403
+ throw new Error('Missing --template when using --templatesource pointing to a local path');
404
+ }
405
+ config.templatepath = config.template;
406
+ // For display purposes
407
+ config.templaterepouri = source;
408
+ } else {
409
+ // Git URL with optional #branch
410
+ const parsed = utils.separateRepoUrlPathBranch(source);
411
+ config.templaterepouri = parsed.repo + '#' + parsed.branch;
412
+ config.templatepath = config.template || parsed.path;
413
+ if (!config.templatepath) {
414
+ throw new Error('Missing template name. Use --template to specify a template within your --templatesource repository.');
415
+ }
416
+ }
417
+ }
418
+ else if (config.templaterepouri) {
419
+ if (fs.existsSync(config.templaterepouri)) {
420
+ // Local path directly to a specific template directory
421
+ localTemplatesRoot = path.resolve(config.templaterepouri);
422
+ config.templaterepouri = localTemplatesRoot;
423
+ // Use the directory itself as the template root
424
+ config.templatepath = '';
425
+ } else if (!config.templaterepouri.startsWith("https://")) {
329
426
  // Given a Mobile SDK template name
330
427
  config.templatepath = config.templaterepouri;
331
428
  config.templaterepouri = SDK.templatesRepoUri;
332
429
  } else {
333
- // Given a full URI
430
+ // Given a full URI to a specific template path
334
431
  var templateUriParsed = utils.separateRepoUrlPathBranch(config.templaterepouri);
335
432
  config.templaterepouri = templateUriParsed.repo + '#' + templateUriParsed.branch;
336
433
  config.templatepath = templateUriParsed.path;
@@ -344,10 +441,17 @@ function actuallyCreateApp(forcecli, config) {
344
441
  // Creating tmp dir for template clone
345
442
  var tmpDir = utils.mkTmpDir();
346
443
 
347
- // Cloning template repo
348
- var repoDir = utils.cloneRepo(tmpDir, config.templaterepouri);
444
+ // Resolve template source directory (clone if needed)
445
+ var repoDir;
446
+ if (localTemplatesRoot) {
447
+ repoDir = localTemplatesRoot;
448
+ } else {
449
+ repoDir = utils.cloneRepo(tmpDir, config.templaterepouri);
450
+ }
349
451
  config.templateLocalPath = path.join(repoDir, config.templatepath);
350
452
 
453
+ validateCustomProperties(`${repoDir}/template.json`, config.templateProperties);
454
+
351
455
  // Override sdk dependencies in package.json if any were provided
352
456
  if (config.sdkdependencies) {
353
457
  overrideSdkDependencies(path.join(config.templateLocalPath, 'package.json'), config.sdkdependencies);
@@ -374,13 +478,14 @@ function actuallyCreateApp(forcecli, config) {
374
478
 
375
479
  // Printing next steps
376
480
  if (!(results instanceof Array)) { results = [results] };
481
+ var hasValidOAuth = hasValidOAuthConfig(config);
377
482
  for (var result of results) {
378
483
  var ide = SDK.ides[result.platform || config.platform.split(',')[0]];
379
484
 
380
485
  if (config.templatepath != undefined && config.templatepath.includes('NativeLogin')) {
381
- printNextStepsForNativeLogin(ide, config.projectPath, result);
486
+ printNextStepsForNativeLogin(ide, config.projectPath, result, hasValidOAuth);
382
487
  } else {
383
- printNextSteps(ide, config.projectPath, result);
488
+ printNextSteps(ide, config.projectPath, result, hasValidOAuth);
384
489
  }
385
490
  }
386
491
  printNextStepsForServerProjectIfNeeded(config.projectPath);
@@ -392,6 +497,33 @@ function actuallyCreateApp(forcecli, config) {
392
497
  }
393
498
  }
394
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
+
395
526
  module.exports = {
396
- createApp
527
+ createApp,
528
+ validateCustomProperties
397
529
  };
@@ -71,5 +71,6 @@ function readJsonFile(filePath) {
71
71
  }
72
72
 
73
73
  module.exports = {
74
- validateJson
74
+ validateJson,
75
+ readJsonFile
75
76
  };
@@ -30,12 +30,14 @@ const SDK = require('./constants');
30
30
  const configHelper = require('./configHelper');
31
31
  const createHelper = require('./createHelper');
32
32
  const templateHelper = require('./templateHelper');
33
+ const { getTemplates, getTemplate, displayTemplateList, displayTemplateDetail } = require('./templateHelper');
33
34
  const jsonChecker = require('./jsonChecker');
34
35
  const logInfo = require('./utils').logInfo;
35
36
  const logError = require('./utils').logError;
37
+ const separateRepoUrlPathBranch = require('./utils').separateRepoUrlPathBranch;
36
38
  const os = require('os');
37
39
 
38
- const { SfdxError } = require('@salesforce/core');
40
+ const { SfError } = require('@salesforce/core');
39
41
  const { Command, flags } = require('@oclif/command');
40
42
 
41
43
  const namespace = 'mobilesdk';
@@ -46,21 +48,36 @@ class OclifAdapter extends Command {
46
48
  return `${description}${os.EOL}${os.EOL}${help}`;
47
49
  }
48
50
 
49
- static listTemplates(cli) {
50
- const applicableTemplates = templateHelper.getTemplates(cli);
51
+ static listTemplates(cli, templateSourceOrRepoUri, includeDescriptions, outputJson) {
52
+ const applicableTemplates = getTemplates(cli, templateSourceOrRepoUri, includeDescriptions);
51
53
 
52
- logInfo('\nAvailable templates:\n', COLOR.cyan);
53
- for (let i=0; i<applicableTemplates.length; i++) {
54
- const template = applicableTemplates[i];
55
- logInfo((i+1) + ') ' + template.description, COLOR.cyan);
56
- logInfo('sfdx ' + [namespace, cli.topic, SDK.commands.createwithtemplate.name].join(':') + ' --' +
57
- SDK.args.templateRepoUri.name + '=' + template.path, COLOR.magenta);
54
+ // Use shared display function
55
+ const commandPrefix = 'sf ' + [namespace, cli.topic, SDK.commands.createwithtemplate.name].join(':');
56
+ const usageExample = '--' + SDK.args.appName.name + '=<YOUR_APP_NAME> --' + SDK.args.packageName.name + '=<YOUR_PACKAGE_NAME> --' + SDK.args.organization.name + '=<YOUR_ORGANIZATION_NAME>';
57
+ displayTemplateList(applicableTemplates, templateSourceOrRepoUri, cli.name, commandPrefix, includeDescriptions, usageExample, outputJson);
58
+ }
59
+
60
+ static describeTemplate(cli, templateSourceOrRepoUri, templateName, includeDescriptions, outputJson) {
61
+ if (!templateName) {
62
+ logError('Error: Template name is required. Use --template to specify the template name.');
63
+ process.exit(1);
64
+ }
65
+
66
+ const template = getTemplate(templateName, templateSourceOrRepoUri, includeDescriptions);
67
+
68
+ if (!template) {
69
+ logError('Error: Template "' + templateName + '" not found.');
70
+ process.exit(1);
58
71
  }
59
- logInfo('');
72
+
73
+ // Use shared display function
74
+ const commandPrefix = 'sf ' + [namespace, cli.topic, SDK.commands.createwithtemplate.name].join(':');
75
+ const usageExample = '--' + SDK.args.appName.name + '=<YOUR_APP_NAME> --' + SDK.args.packageName.name + '=<YOUR_PACKAGE_NAME> --' + SDK.args.organization.name + '=<YOUR_ORGANIZATION_NAME>';
76
+ displayTemplateDetail(template, templateSourceOrRepoUri, cli.name, commandPrefix, includeDescriptions, usageExample, outputJson);
60
77
  }
61
78
 
62
79
  static runCommand(cli, commandName, vals) {
63
- switch(commandName) {
80
+ switch (commandName) {
64
81
  case SDK.commands.create.name:
65
82
  case SDK.commands.createwithtemplate.name:
66
83
  createHelper.createApp(cli, vals);
@@ -69,7 +86,11 @@ class OclifAdapter extends Command {
69
86
  configHelper.printVersion(cli);
70
87
  break;
71
88
  case SDK.commands.listtemplates.name:
72
- OclifAdapter.listTemplates(cli);
89
+ OclifAdapter.listTemplates(cli, vals.templatesource, vals.doc, vals.json);
90
+ process.exit(0);
91
+ break;
92
+ case SDK.commands.describetemplate.name:
93
+ OclifAdapter.describeTemplate(cli, vals.templatesource, vals.template, vals.doc, vals.json);
73
94
  process.exit(0);
74
95
  break;
75
96
  case SDK.commands.checkconfig.name:
@@ -139,9 +160,15 @@ class OclifAdapter extends Command {
139
160
  }
140
161
 
141
162
  execute(cli, klass) {
142
- const { flags } = this.parse(klass);
143
- if (OclifAdapter.validateCommand(cli, klass.command.name, flags)) {
144
- 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);
145
172
  }
146
173
  }
147
174
 
@@ -180,16 +207,58 @@ class OclifAdapter extends Command {
180
207
  this.flags[name] = flag.quantity + '';
181
208
  break;
182
209
  } else {
183
- throw new SfdxError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
210
+ throw new SfError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
184
211
  }
185
212
  default:
186
- throw new SfdxError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
213
+ throw new SfError(`Unexpected value type for flag ${name}`, 'UnexpectedFlagValueType');
187
214
  }
188
215
  });
189
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
+
190
259
  }
191
260
 
192
- OclifAdapter.getCommand = function(cli, commandName) {
261
+ OclifAdapter.getCommand = function (cli, commandName) {
193
262
  if (!this._command) {
194
263
  this._command = configHelper.getCommandExpanded(cli, commandName);
195
264
  }
@@ -28,7 +28,9 @@
28
28
  // Dependencies
29
29
  var path = require('path'),
30
30
  SDK = require('./constants'),
31
- utils = require('./utils');
31
+ utils = require('./utils'),
32
+ fs = require('fs'),
33
+ { constantCase } = require('change-case');
32
34
 
33
35
  //
34
36
  // Helper to prepare template
@@ -36,7 +38,7 @@ var path = require('path'),
36
38
  function prepareTemplate(config, templateDir) {
37
39
  var template = require(path.join(templateDir, 'template.js'));
38
40
  return utils.runFunctionThrowError(
39
- function() {
41
+ function () {
40
42
  return template.prepare(config, utils.replaceInFiles, utils.moveFile, utils.removeFile);
41
43
  },
42
44
  templateDir);
@@ -45,14 +47,22 @@ function prepareTemplate(config, templateDir) {
45
47
  //
46
48
  // Get templates for the given cli
47
49
  //
48
- function getTemplates(cli) {
50
+ function getTemplates(cli, templateSourceOrRepoUri, includeDescriptions) {
49
51
  try {
50
52
 
51
53
  // Creating tmp dir for template clone
52
54
  var tmpDir = utils.mkTmpDir();
53
55
 
54
- // Cloning template repo
55
- var repoDir = utils.cloneRepo(tmpDir, SDK.templatesRepoUri);
56
+ // Use provided source (git URL or local path) or fall back to default
57
+ var source = templateSourceOrRepoUri || SDK.templatesRepoUri;
58
+ var repoDir;
59
+ if (fs.existsSync(source)) {
60
+ // Local path
61
+ repoDir = path.resolve(source);
62
+ } else {
63
+ // Git URL
64
+ repoDir = utils.cloneRepo(tmpDir, source);
65
+ }
56
66
 
57
67
  // Getting list of templates
58
68
  var templates = require(path.join(repoDir, 'templates.json'));
@@ -61,6 +71,16 @@ function getTemplates(cli) {
61
71
  var applicableTemplates = templates
62
72
  .filter(template => cli.appTypes.includes(template.appType) && cli.platforms.filter(platform => template.platforms.includes(platform)).length > 0);
63
73
 
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
+ });
83
+
64
84
  // Cleanup
65
85
  utils.removeFile(tmpDir);
66
86
 
@@ -96,9 +116,255 @@ function getAppTypeFromTemplate(templateRepoUriWithPossiblePath) {
96
116
  return appType;
97
117
  }
98
118
 
119
+ //
120
+ // Extract template metadata from template.json file
121
+ //
122
+ function getTemplateMetadata(templatePath, repoDir) {
123
+ try {
124
+ var templateJsonPath = path.join(repoDir, templatePath, 'template.json');
125
+ if (fs.existsSync(templateJsonPath)) {
126
+ var templateJsonContent = fs.readFileSync(templateJsonPath, 'utf8');
127
+ var templateData = JSON.parse(templateJsonContent);
128
+
129
+ // Return all metadata properties from the template.json file
130
+ // This makes the function more flexible for future use cases
131
+ return templateData;
132
+ }
133
+ } catch (error) {
134
+ // If there's any error reading or parsing the template.json, just return null
135
+ // This ensures the command continues to work even if template.json parsing fails
136
+ }
137
+ return null;
138
+ }
139
+
140
+ //
141
+ // Get a single template by name
142
+ //
143
+ function getTemplate(templateName, templateSourceOrRepoUri, includeDescriptions) {
144
+ try {
145
+ // Creating tmp dir for template clone
146
+ var tmpDir = utils.mkTmpDir();
147
+
148
+ // Use provided source (git URL or local path) or fall back to default
149
+ var source = templateSourceOrRepoUri || SDK.templatesRepoUri;
150
+ var repoDir;
151
+ if (fs.existsSync(source)) {
152
+ // Local path
153
+ repoDir = path.resolve(source);
154
+ } else {
155
+ // Git URL
156
+ repoDir = utils.cloneRepo(tmpDir, source);
157
+ }
158
+
159
+ // Getting list of templates
160
+ var templates = require(path.join(repoDir, 'templates.json'));
161
+
162
+ // Finding the specific template
163
+ var template = templates.find(t => t.path === templateName);
164
+
165
+ if (!template) {
166
+ utils.removeFile(tmpDir);
167
+ return null;
168
+ }
169
+
170
+ const metadata = getTemplateMetadata(template.path, repoDir);
171
+ template.customProperties = getCustomProperties(metadata);
172
+
173
+ // If descriptions are requested, add metadata
174
+ if (includeDescriptions) {
175
+ template.metadata = metadata;
176
+ }
177
+
178
+ // Cleanup
179
+ utils.removeFile(tmpDir);
180
+
181
+ return template;
182
+ } catch (error) {
183
+ return null;
184
+ }
185
+ }
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
+
210
+ //
211
+ // Display template list with optional metadata
212
+ //
213
+ function displayTemplateList(templates, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
214
+ var COLOR = require('./outputColors');
215
+ var logInfo = utils.logInfo;
216
+
217
+ if (outputJson) {
218
+ // Output in JSON format
219
+ var jsonOutput = {
220
+ repository: source || 'default',
221
+ templates: templates.map(function (template, index) {
222
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
223
+
224
+ var jsonTemplate = {
225
+ index: index + 1,
226
+ path: template.path,
227
+ description: template.description,
228
+ appType: template.appType,
229
+ platforms: template.platforms,
230
+ command: command
231
+ };
232
+
233
+ if (includeDescriptions && template.metadata) {
234
+ jsonTemplate.metadata = template.metadata;
235
+ }
236
+
237
+ return jsonTemplate;
238
+ })
239
+ };
240
+
241
+ logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
242
+ return;
243
+ }
244
+
245
+ // Show which template repository is being used
246
+ if (source) {
247
+ logInfo('\nAvailable templates from custom repository:\n', COLOR.cyan);
248
+ logInfo('Repository: ' + source, COLOR.cyan);
249
+ } else {
250
+ logInfo('\nAvailable templates:\n', COLOR.cyan);
251
+ }
252
+
253
+ for (var i = 0; i < templates.length; i++) {
254
+ var template = templates[i];
255
+ logInfo((i + 1) + ') ' + template.description, COLOR.cyan);
256
+
257
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
258
+
259
+ logInfo(command, COLOR.magenta);
260
+
261
+ // If descriptions are requested and available, show them
262
+ if (includeDescriptions && template.metadata) {
263
+ if (template.metadata.description) {
264
+ logInfo(' Description: ' + template.metadata.description, COLOR.white);
265
+ }
266
+ if (template.metadata.useCase) {
267
+ logInfo(' Use Case: ' + template.metadata.useCase, COLOR.white);
268
+ }
269
+ if (template.metadata.features && Array.isArray(template.metadata.features)) {
270
+ logInfo(' Features: ' + template.metadata.features.join(', '), COLOR.white);
271
+ }
272
+ if (template.metadata.complexity) {
273
+ logInfo(' Complexity: ' + template.metadata.complexity, COLOR.white);
274
+ }
275
+ }
276
+ }
277
+ logInfo('');
278
+ }
279
+
280
+ //
281
+ // Display detailed information about a single template
282
+ //
283
+ function displayTemplateDetail(template, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
284
+ var COLOR = require('./outputColors');
285
+ var logInfo = utils.logInfo;
286
+
287
+ // create command usage
288
+ var command = buildTemplateCommand(source, commandPrefix, template.path, extraRequiredArgs, template.customProperties);
289
+
290
+ if (outputJson) {
291
+ // Output in JSON format
292
+ var jsonOutput = {
293
+ repository: source || 'default',
294
+ template: {
295
+ path: template.path,
296
+ description: template.description,
297
+ appType: template.appType,
298
+ platforms: template.platforms,
299
+ command: command
300
+ }
301
+ };
302
+
303
+ if (includeDescriptions && template.metadata) {
304
+ jsonOutput.template.metadata = template.metadata;
305
+ }
306
+
307
+ logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
308
+ return;
309
+ }
310
+
311
+ // Show which template repository is being used
312
+ if (source) {
313
+ logInfo('\nTemplate from custom repository:\n', COLOR.cyan);
314
+ logInfo('Repository: ' + source, COLOR.cyan);
315
+ } else {
316
+ logInfo('\nTemplate from default repository:\n', COLOR.cyan);
317
+ }
318
+
319
+ // Display template basic info
320
+ logInfo('Template: ' + template.path, COLOR.cyan);
321
+ logInfo('Description: ' + template.description, COLOR.cyan);
322
+ logInfo('App Type: ' + template.appType, COLOR.cyan);
323
+ logInfo('Platforms: ' + template.platforms.join(', '), COLOR.cyan);
324
+ logInfo('\nUsage:', COLOR.magenta);
325
+
326
+ // Display command usage
327
+ logInfo(command, COLOR.magenta);
328
+
329
+ // If descriptions are requested and available, show raw JSON metadata
330
+ if (includeDescriptions && template.metadata) {
331
+ logInfo('\nTemplate Metadata (template.json):', COLOR.cyan);
332
+ logInfo(JSON.stringify(template.metadata, null, 2), COLOR.white);
333
+ }
334
+
335
+ logInfo('');
336
+ }
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
+ }
99
359
 
100
360
  module.exports = {
101
361
  prepareTemplate,
102
362
  getTemplates,
103
- getAppTypeFromTemplate
363
+ getTemplate,
364
+ getAppTypeFromTemplate,
365
+ getTemplateMetadata,
366
+ displayTemplateList,
367
+ displayTemplateDetail,
368
+ getCustomProperties,
369
+ getCustomCommandArgs
104
370
  };