forceios 13.0.1 → 13.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forceios",
3
- "version": "13.0.1",
3
+ "version": "13.1.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
 
@@ -81,28 +85,32 @@ function readConfig(args, cli, handler) {
81
85
  var processorList = null;
82
86
 
83
87
  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);
88
+ case SDK.commands.version.name:
89
+ printVersion(cli);
90
+ process.exit(0);
91
+ break;
92
+ case SDK.commands.create.name:
93
+ case SDK.commands.createwithtemplate.name:
94
+ processorList = buildArgsProcessorList(cli, commandName);
95
+ commandLineUtils.processArgsInteractive(commandLineArgs, processorList, handler);
96
+ break;
97
+ case SDK.commands.checkconfig.name:
98
+ processorList = buildArgsProcessorList(cli, commandName);
99
+ commandLineUtils.processArgsInteractive(commandLineArgs, processorList, function (config) {
100
+ validateJson(config.configpath, config.configtype);
101
+ });
102
+ break;
103
+ case SDK.commands.listtemplates.name:
104
+ listTemplates(cli, commandLineArgs);
105
+ process.exit(0);
106
+ break;
107
+ case SDK.commands.describetemplate.name:
108
+ describeTemplate(cli, commandLineArgs);
109
+ process.exit(0);
110
+ break;
111
+ default:
112
+ usage(cli);
113
+ process.exit(1);
106
114
  };
107
115
 
108
116
 
@@ -115,20 +123,76 @@ function printVersion(cli) {
115
123
  function printArgs(cli, commandName) {
116
124
  getArgsExpanded(cli, commandName)
117
125
  .filter(arg => !arg.hidden)
118
- .forEach(arg => logInfo(' ' + (!arg.required ? '[' : '') + '--' + arg.name + '=' + arg.description + (!arg.required ? ']' : ''), COLOR.magenta));
126
+ .forEach(arg => logInfo(' ' + (!arg.required ? '[' : '') + '--' + arg.name + '=' + arg.description + (!arg.required ? ']' : ''), COLOR.magenta));
127
+ }
128
+
129
+ function listTemplates(cli, commandLineArgs) {
130
+ var cliName = cli.name;
131
+
132
+ // Parse command line arguments to extract templatesource or templaterepouri
133
+ var templateSource = null;
134
+ var templateRepoUri = null;
135
+ var includeDescriptions = false;
136
+ var outputJson = false;
137
+ if (commandLineArgs && commandLineArgs.length > 0) {
138
+ try {
139
+ var argsMap = commandLineUtils.parseArgs(commandLineArgs);
140
+ templateSource = argsMap[SDK.args.templateSource.name];
141
+ templateRepoUri = argsMap[SDK.args.templateRepoUri.name];
142
+ includeDescriptions = argsMap.hasOwnProperty(SDK.args.doc.name);
143
+ outputJson = argsMap.hasOwnProperty(SDK.args.json.name);
144
+ } catch (error) {
145
+ // If argument parsing fails, continue without templateRepoUri
146
+ }
147
+ }
148
+
149
+ var source = templateSource || templateRepoUri;
150
+ var applicableTemplates = getTemplates(cli, source, includeDescriptions);
151
+
152
+ // Use shared display function
153
+ var commandPrefix = cliName + ' ' + SDK.commands.createwithtemplate.name;
154
+ displayTemplateList(applicableTemplates, source, cliName, commandPrefix, includeDescriptions, null, outputJson);
119
155
  }
120
156
 
121
- function listTemplates(cli) {
157
+ function describeTemplate(cli, commandLineArgs) {
122
158
  var cliName = cli.name;
123
- var applicableTemplates = getTemplates(cli);
124
159
 
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);
160
+ // Parse command line arguments to extract templatesource, template, and doc
161
+ var templateSource = null;
162
+ var templateRepoUri = null;
163
+ var templateName = null;
164
+ var includeDescriptions = false;
165
+ var outputJson = false;
166
+ if (commandLineArgs && commandLineArgs.length > 0) {
167
+ try {
168
+ var argsMap = commandLineUtils.parseArgs(commandLineArgs);
169
+ templateSource = argsMap[SDK.args.templateSource.name];
170
+ templateRepoUri = argsMap[SDK.args.templateRepoUri.name];
171
+ templateName = argsMap[SDK.args.template.name];
172
+ includeDescriptions = argsMap.hasOwnProperty(SDK.args.doc.name);
173
+ outputJson = argsMap.hasOwnProperty(SDK.args.json.name);
174
+ } catch (error) {
175
+ // If argument parsing fails, continue without templateRepoUri
176
+ }
130
177
  }
131
- logInfo('');
178
+
179
+ // Check if template name is provided
180
+ if (!templateName) {
181
+ logInfo('Error: Template name is required. Use --template to specify the template name.', COLOR.red);
182
+ process.exit(1);
183
+ }
184
+
185
+ var source = templateSource || templateRepoUri;
186
+ var template = getTemplate(templateName, source, includeDescriptions);
187
+
188
+ if (!template) {
189
+ logInfo('Error: Template "' + templateName + '" not found.', COLOR.red);
190
+ process.exit(1);
191
+ }
192
+
193
+ // Use shared display function
194
+ var commandPrefix = cliName + ' ' + SDK.commands.createwithtemplate.name;
195
+ displayTemplateDetail(template, source, cliName, commandPrefix, includeDescriptions, null, outputJson);
132
196
  }
133
197
 
134
198
  function usage(cli) {
@@ -184,7 +248,7 @@ function buildArgsProcessorList(cli, commandName) {
184
248
  // * preprocessor: function or null
185
249
  //
186
250
  function addProcessorFor(argProcessorList, argName, prompt, error, validation, preprocessor) {
187
- argProcessorList.addArgProcessor(argName, prompt, function(val) {
251
+ argProcessorList.addArgProcessor(argName, prompt, function (val) {
188
252
  val = val.trim();
189
253
 
190
254
  // 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.1';
31
+ var VERSION= '13.1.0';
32
32
 
33
33
  module.exports = {
34
34
  version: VERSION,
@@ -65,7 +65,7 @@ module.exports = {
65
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: {
@@ -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,7 +137,7 @@ 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
 
@@ -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: {
@@ -277,7 +305,31 @@ 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
+ }
281
333
  },
282
334
 
283
335
  commands: {
@@ -301,7 +353,9 @@ module.exports = {
301
353
  createwithtemplate: {
302
354
  name: 'createwithtemplate',
303
355
  args: cli => [cli.platforms.length > 1 ? 'platform' : null,
356
+ 'templateSource',
304
357
  'templateRepoUri',
358
+ 'template',
305
359
  'appName',
306
360
  'packageName',
307
361
  'organization',
@@ -309,7 +363,7 @@ module.exports = {
309
363
  'outputDir',
310
364
  'verbose',
311
365
  cli.name === 'forcehybrid' ? 'pluginRepoUri' : null,
312
- 'sdkDependencies'
366
+ 'sdkDependencies'
313
367
  ].filter(x=>x!=null),
314
368
  description: cli => 'create ' + cli.purpose + ' from a template',
315
369
  longDescription: cli => 'Create ' + cli.purpose + ' from a template.',
@@ -324,10 +378,17 @@ module.exports = {
324
378
  },
325
379
  listtemplates: {
326
380
  name: 'listtemplates',
327
- args: [],
381
+ args: ['templateSource', 'doc', 'json'],
328
382
  description: cli => 'list available Mobile SDK templates to create ' + cli.purpose,
329
383
  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.'
384
+ 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.'
385
+ },
386
+ describetemplate: {
387
+ name: 'describetemplate',
388
+ args: ['templateSource', 'template', 'doc', 'json'],
389
+ description: cli => 'list details for a specific Mobile SDK template to create ' + cli.purpose,
390
+ longDescription: cli => 'List details for a specific Mobile SDK template to create ' + cli.purpose + '.',
391
+ 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
392
  },
332
393
  checkconfig: {
333
394
  name: 'checkconfig',
@@ -109,10 +109,38 @@ function createHybridApp(config) {
109
109
  // Run cordova prepare
110
110
  utils.runProcessThrowError('cordova prepare', config.projectDir);
111
111
 
112
+ // Add theme for Android API 35
113
+ if (config.platform.split(',').includes('android')) {
114
+ createAndroidAPI35Theme(config.projectDir);
115
+ }
116
+
112
117
  // Done
113
118
  return prepareResult;
114
119
  }
115
120
 
121
+ //
122
+ // Add Android API 35 theme file
123
+ //
124
+ function createAndroidAPI35Theme(projectDir) {
125
+ const dirPath = path.join(projectDir, 'platforms', 'android', 'app', 'src', 'main', 'res', 'values-v35');
126
+ const filePath = path.join(dirPath, 'themes.xml');
127
+ const fileContents = `<?xml version='1.0' encoding='utf-8'?>
128
+ <resources>
129
+ <!-- Override for API 35+ to fix white status bar with white icons issue -->
130
+ <style name="SalesforceSDK_SplashScreen" parent="Theme.SplashScreen.IconBackground">
131
+ <item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
132
+ <!-- Use dark icons on light status bar background -->
133
+ <item name="android:windowLightStatusBar">true</item>
134
+ </style>
135
+ </resources>`
136
+
137
+ // Ensure the directory exists
138
+ utils.mkDirIfNeeded(dirPath);
139
+
140
+ // Write the file
141
+ fs.writeFileSync(filePath, fileContents, 'utf8');
142
+ }
143
+
116
144
  //
117
145
  // Print details
118
146
  //
@@ -324,13 +352,41 @@ function actuallyCreateApp(forcecli, config) {
324
352
  config.version = SDK.version;
325
353
 
326
354
  // Figuring out template repo uri and path
327
- if (config.templaterepouri) {
328
- if (!config.templaterepouri.startsWith("https://")) {
355
+ let localTemplatesRoot;
356
+ if (config.templatesource) {
357
+ const source = config.templatesource;
358
+ if (fs.existsSync(source)) {
359
+ // Local path to templates suite
360
+ localTemplatesRoot = path.resolve(source);
361
+ if (!config.template) {
362
+ throw new Error('Missing --template when using --templatesource pointing to a local path');
363
+ }
364
+ config.templatepath = config.template;
365
+ // For display purposes
366
+ config.templaterepouri = source;
367
+ } else {
368
+ // Git URL with optional #branch
369
+ const parsed = utils.separateRepoUrlPathBranch(source);
370
+ config.templaterepouri = parsed.repo + '#' + parsed.branch;
371
+ config.templatepath = config.template || parsed.path;
372
+ if (!config.templatepath) {
373
+ throw new Error('Missing template name. Use --template to specify a template within your --templatesource repository.');
374
+ }
375
+ }
376
+ }
377
+ else if (config.templaterepouri) {
378
+ if (fs.existsSync(config.templaterepouri)) {
379
+ // Local path directly to a specific template directory
380
+ localTemplatesRoot = path.resolve(config.templaterepouri);
381
+ config.templaterepouri = localTemplatesRoot;
382
+ // Use the directory itself as the template root
383
+ config.templatepath = '';
384
+ } else if (!config.templaterepouri.startsWith("https://")) {
329
385
  // Given a Mobile SDK template name
330
386
  config.templatepath = config.templaterepouri;
331
387
  config.templaterepouri = SDK.templatesRepoUri;
332
388
  } else {
333
- // Given a full URI
389
+ // Given a full URI to a specific template path
334
390
  var templateUriParsed = utils.separateRepoUrlPathBranch(config.templaterepouri);
335
391
  config.templaterepouri = templateUriParsed.repo + '#' + templateUriParsed.branch;
336
392
  config.templatepath = templateUriParsed.path;
@@ -344,8 +400,13 @@ function actuallyCreateApp(forcecli, config) {
344
400
  // Creating tmp dir for template clone
345
401
  var tmpDir = utils.mkTmpDir();
346
402
 
347
- // Cloning template repo
348
- var repoDir = utils.cloneRepo(tmpDir, config.templaterepouri);
403
+ // Resolve template source directory (clone if needed)
404
+ var repoDir;
405
+ if (localTemplatesRoot) {
406
+ repoDir = localTemplatesRoot;
407
+ } else {
408
+ repoDir = utils.cloneRepo(tmpDir, config.templaterepouri);
409
+ }
349
410
  config.templateLocalPath = path.join(repoDir, config.templatepath);
350
411
 
351
412
  // Override sdk dependencies in package.json if any were provided
@@ -30,9 +30,11 @@ 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
40
  const { SfdxError } = require('@salesforce/core');
@@ -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:
@@ -189,7 +210,7 @@ class OclifAdapter extends Command {
189
210
  }
190
211
  }
191
212
 
192
- OclifAdapter.getCommand = function(cli, commandName) {
213
+ OclifAdapter.getCommand = function (cli, commandName) {
193
214
  if (!this._command) {
194
215
  this._command = configHelper.getCommandExpanded(cli, commandName);
195
216
  }
@@ -28,7 +28,8 @@
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');
32
33
 
33
34
  //
34
35
  // Helper to prepare template
@@ -36,7 +37,7 @@ var path = require('path'),
36
37
  function prepareTemplate(config, templateDir) {
37
38
  var template = require(path.join(templateDir, 'template.js'));
38
39
  return utils.runFunctionThrowError(
39
- function() {
40
+ function () {
40
41
  return template.prepare(config, utils.replaceInFiles, utils.moveFile, utils.removeFile);
41
42
  },
42
43
  templateDir);
@@ -45,14 +46,22 @@ function prepareTemplate(config, templateDir) {
45
46
  //
46
47
  // Get templates for the given cli
47
48
  //
48
- function getTemplates(cli) {
49
+ function getTemplates(cli, templateSourceOrRepoUri, includeDescriptions) {
49
50
  try {
50
51
 
51
52
  // Creating tmp dir for template clone
52
53
  var tmpDir = utils.mkTmpDir();
53
54
 
54
- // Cloning template repo
55
- var repoDir = utils.cloneRepo(tmpDir, SDK.templatesRepoUri);
55
+ // Use provided source (git URL or local path) or fall back to default
56
+ var source = templateSourceOrRepoUri || SDK.templatesRepoUri;
57
+ var repoDir;
58
+ if (fs.existsSync(source)) {
59
+ // Local path
60
+ repoDir = path.resolve(source);
61
+ } else {
62
+ // Git URL
63
+ repoDir = utils.cloneRepo(tmpDir, source);
64
+ }
56
65
 
57
66
  // Getting list of templates
58
67
  var templates = require(path.join(repoDir, 'templates.json'));
@@ -61,6 +70,13 @@ function getTemplates(cli) {
61
70
  var applicableTemplates = templates
62
71
  .filter(template => cli.appTypes.includes(template.appType) && cli.platforms.filter(platform => template.platforms.includes(platform)).length > 0);
63
72
 
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
+ }
79
+
64
80
  // Cleanup
65
81
  utils.removeFile(tmpDir);
66
82
 
@@ -96,9 +112,236 @@ function getAppTypeFromTemplate(templateRepoUriWithPossiblePath) {
96
112
  return appType;
97
113
  }
98
114
 
115
+ //
116
+ // Extract template metadata from template.json file
117
+ //
118
+ function getTemplateMetadata(templatePath, repoDir) {
119
+ try {
120
+ var templateJsonPath = path.join(repoDir, templatePath, 'template.json');
121
+ if (fs.existsSync(templateJsonPath)) {
122
+ var templateJsonContent = fs.readFileSync(templateJsonPath, 'utf8');
123
+ var templateData = JSON.parse(templateJsonContent);
124
+
125
+ // Return all metadata properties from the template.json file
126
+ // This makes the function more flexible for future use cases
127
+ return templateData;
128
+ }
129
+ } catch (error) {
130
+ // If there's any error reading or parsing the template.json, just return null
131
+ // This ensures the command continues to work even if template.json parsing fails
132
+ }
133
+ return null;
134
+ }
135
+
136
+ //
137
+ // Get a single template by name
138
+ //
139
+ function getTemplate(templateName, templateSourceOrRepoUri, includeDescriptions) {
140
+ try {
141
+ // Creating tmp dir for template clone
142
+ var tmpDir = utils.mkTmpDir();
143
+
144
+ // Use provided source (git URL or local path) or fall back to default
145
+ var source = templateSourceOrRepoUri || SDK.templatesRepoUri;
146
+ var repoDir;
147
+ if (fs.existsSync(source)) {
148
+ // Local path
149
+ repoDir = path.resolve(source);
150
+ } else {
151
+ // Git URL
152
+ repoDir = utils.cloneRepo(tmpDir, source);
153
+ }
154
+
155
+ // Getting list of templates
156
+ var templates = require(path.join(repoDir, 'templates.json'));
157
+
158
+ // Finding the specific template
159
+ var template = templates.find(t => t.path === templateName);
160
+
161
+ if (!template) {
162
+ utils.removeFile(tmpDir);
163
+ return null;
164
+ }
165
+
166
+ // If descriptions are requested, add metadata
167
+ if (includeDescriptions) {
168
+ template.metadata = getTemplateMetadata(template.path, repoDir);
169
+ }
170
+
171
+ // Cleanup
172
+ utils.removeFile(tmpDir);
173
+
174
+ return template;
175
+ } catch (error) {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ //
181
+ // Display template list with optional metadata
182
+ //
183
+ function displayTemplateList(templates, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
184
+ var utils = require('./utils');
185
+ var COLOR = require('./outputColors');
186
+ var logInfo = utils.logInfo;
187
+ var SDK = require('./constants');
188
+
189
+ if (outputJson) {
190
+ // Output in JSON format
191
+ var jsonOutput = {
192
+ 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
+ }
201
+
202
+ var jsonTemplate = {
203
+ index: index + 1,
204
+ path: template.path,
205
+ description: template.description,
206
+ appType: template.appType,
207
+ platforms: template.platforms,
208
+ command: command
209
+ };
210
+
211
+ if (includeDescriptions && template.metadata) {
212
+ jsonTemplate.metadata = template.metadata;
213
+ }
214
+
215
+ return jsonTemplate;
216
+ })
217
+ };
218
+
219
+ logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
220
+ return;
221
+ }
222
+
223
+ // Show which template repository is being used
224
+ if (source) {
225
+ logInfo('\nAvailable templates from custom repository:\n', COLOR.cyan);
226
+ logInfo('Repository: ' + source, COLOR.cyan);
227
+ } else {
228
+ logInfo('\nAvailable templates:\n', COLOR.cyan);
229
+ }
230
+
231
+ for (var i = 0; i < templates.length; i++) {
232
+ var template = templates[i];
233
+ logInfo((i + 1) + ') ' + template.description, COLOR.cyan);
234
+
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
+
244
+ logInfo(command, COLOR.magenta);
245
+
246
+ // If descriptions are requested and available, show them
247
+ if (includeDescriptions && template.metadata) {
248
+ if (template.metadata.description) {
249
+ logInfo(' Description: ' + template.metadata.description, COLOR.white);
250
+ }
251
+ if (template.metadata.useCase) {
252
+ logInfo(' Use Case: ' + template.metadata.useCase, COLOR.white);
253
+ }
254
+ if (template.metadata.features && Array.isArray(template.metadata.features)) {
255
+ logInfo(' Features: ' + template.metadata.features.join(', '), COLOR.white);
256
+ }
257
+ if (template.metadata.complexity) {
258
+ logInfo(' Complexity: ' + template.metadata.complexity, COLOR.white);
259
+ }
260
+ }
261
+ }
262
+ logInfo('');
263
+ }
264
+
265
+ //
266
+ // Display detailed information about a single template
267
+ //
268
+ function displayTemplateDetail(template, source, cliName, commandPrefix, includeDescriptions, extraRequiredArgs, outputJson) {
269
+ var utils = require('./utils');
270
+ var COLOR = require('./outputColors');
271
+ var logInfo = utils.logInfo;
272
+ var SDK = require('./constants');
273
+
274
+ if (outputJson) {
275
+ // 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
+ var jsonOutput = {
285
+ repository: source || 'default',
286
+ template: {
287
+ path: template.path,
288
+ description: template.description,
289
+ appType: template.appType,
290
+ platforms: template.platforms,
291
+ command: command
292
+ }
293
+ };
294
+
295
+ if (includeDescriptions && template.metadata) {
296
+ jsonOutput.template.metadata = template.metadata;
297
+ }
298
+
299
+ logInfo(JSON.stringify(jsonOutput, null, 2), COLOR.white);
300
+ return;
301
+ }
302
+
303
+ // Show which template repository is being used
304
+ if (source) {
305
+ logInfo('\nTemplate from custom repository:\n', COLOR.cyan);
306
+ logInfo('Repository: ' + source, COLOR.cyan);
307
+ } else {
308
+ logInfo('\nTemplate from default repository:\n', COLOR.cyan);
309
+ }
310
+
311
+ // Display template basic info
312
+ logInfo('Template: ' + template.path, COLOR.cyan);
313
+ logInfo('Description: ' + template.description, COLOR.cyan);
314
+ logInfo('App Type: ' + template.appType, COLOR.cyan);
315
+ logInfo('Platforms: ' + template.platforms.join(', '), COLOR.cyan);
316
+
317
+ // 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
+ logInfo(command, COLOR.magenta);
329
+
330
+ // If descriptions are requested and available, show raw JSON metadata
331
+ if (includeDescriptions && template.metadata) {
332
+ logInfo('\nTemplate Metadata (template.json):', COLOR.cyan);
333
+ logInfo(JSON.stringify(template.metadata, null, 2), COLOR.white);
334
+ }
335
+
336
+ logInfo('');
337
+ }
99
338
 
100
339
  module.exports = {
101
340
  prepareTemplate,
102
341
  getTemplates,
103
- getAppTypeFromTemplate
342
+ getTemplate,
343
+ getAppTypeFromTemplate,
344
+ getTemplateMetadata,
345
+ displayTemplateList,
346
+ displayTemplateDetail
104
347
  };