@nocobase/cli 2.2.0-alpha.1 → 2.2.0-alpha.10

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.
Files changed (78) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  3. package/bin/early-locale.js +89 -0
  4. package/bin/node-version.js +35 -0
  5. package/bin/run.js +9 -0
  6. package/bin/windows-admin.js +60 -0
  7. package/dist/commands/app/destroy.js +4 -3
  8. package/dist/commands/app/restart.js +38 -0
  9. package/dist/commands/app/shared.js +49 -3
  10. package/dist/commands/app/start.js +95 -0
  11. package/dist/commands/app/upgrade.js +11 -0
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/examples/prompts-stages.js +2 -2
  15. package/dist/commands/examples/prompts-test.js +2 -2
  16. package/dist/commands/init.js +152 -14
  17. package/dist/commands/install.js +256 -109
  18. package/dist/commands/license/activate.js +4 -1
  19. package/dist/commands/license/shared.js +24 -15
  20. package/dist/commands/portal/config.js +88 -0
  21. package/dist/commands/portal/create.js +104 -0
  22. package/dist/commands/portal/deploy.js +81 -0
  23. package/dist/commands/portal/destroy.js +104 -0
  24. package/dist/commands/portal/dev.js +71 -0
  25. package/dist/commands/portal/index.js +20 -0
  26. package/dist/commands/portal/info.js +82 -0
  27. package/dist/commands/portal/list.js +98 -0
  28. package/dist/commands/portal/pull.js +84 -0
  29. package/dist/commands/portal/push.js +79 -0
  30. package/dist/commands/proxy/caddy/generate.js +93 -7
  31. package/dist/commands/proxy/nginx/generate.js +98 -7
  32. package/dist/commands/revision/create.js +1 -1
  33. package/dist/commands/self/check.js +1 -1
  34. package/dist/commands/self/update.js +4 -4
  35. package/dist/commands/skills/check.js +4 -5
  36. package/dist/commands/skills/install.js +18 -1
  37. package/dist/commands/skills/update.js +19 -4
  38. package/dist/commands/source/dev.js +10 -6
  39. package/dist/commands/source/download.js +85 -16
  40. package/dist/lib/api-command-compat.js +51 -8
  41. package/dist/lib/app-managed-resources.js +104 -5
  42. package/dist/lib/auth-store.js +105 -13
  43. package/dist/lib/cli-config.js +93 -2
  44. package/dist/lib/docker-image.js +94 -6
  45. package/dist/lib/env-auth.js +291 -45
  46. package/dist/lib/env-config.js +14 -0
  47. package/dist/lib/env-proxy-config.js +48 -0
  48. package/dist/lib/env-proxy.js +266 -61
  49. package/dist/lib/hook-script.js +160 -0
  50. package/dist/lib/managed-init-env.js +6 -1
  51. package/dist/lib/portal-command-env.js +31 -0
  52. package/dist/lib/portal-config.js +133 -0
  53. package/dist/lib/portal-configure.js +117 -0
  54. package/dist/lib/portal-create.js +433 -0
  55. package/dist/lib/portal-deploy.js +283 -0
  56. package/dist/lib/portal-destroy.js +100 -0
  57. package/dist/lib/portal-dev.js +79 -0
  58. package/dist/lib/portal-env-files.js +53 -0
  59. package/dist/lib/portal-info.js +31 -0
  60. package/dist/lib/portal-list.js +211 -0
  61. package/dist/lib/portal-source.js +523 -0
  62. package/dist/lib/prompt-catalog-terminal.js +32 -19
  63. package/dist/lib/prompt-validators.js +1 -1
  64. package/dist/lib/prompt-web-ui.js +20 -13
  65. package/dist/lib/proxy-caddy.js +77 -9
  66. package/dist/lib/proxy-nginx.js +71 -11
  67. package/dist/lib/run-npm.js +21 -16
  68. package/dist/lib/self-manager.js +254 -46
  69. package/dist/lib/skills-manager.js +116 -23
  70. package/dist/lib/source-publish.js +2 -2
  71. package/dist/lib/startup-update.js +1 -1
  72. package/dist/lib/ui.js +28 -1
  73. package/dist/locale/en-US.json +227 -43
  74. package/dist/locale/zh-CN.json +227 -43
  75. package/package.json +11 -2
  76. package/scripts/build.mjs +0 -34
  77. package/scripts/clean.mjs +0 -9
  78. package/tsconfig.json +0 -19
@@ -15,22 +15,29 @@ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
15
15
  import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
16
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
17
  import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
- import { resolveDefaultConfigScope } from '../lib/cli-home.js';
19
- import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
18
+ import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
19
+ import { getCliConfigValue, resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
20
+ import { resolveOfficialDockerRegistry } from '../lib/docker-image.js';
20
21
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
21
22
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
22
23
  import { runPromptCatalogWebUI } from "../lib/prompt-web-ui.js";
23
24
  import { validateApiBaseUrl, validateEnvKey } from "../lib/prompt-validators.js";
24
25
  import { installNocoBaseSkills, isNpmRegistryUnavailable } from '../lib/skills-manager.js';
25
26
  import { omitKeys, pickKeys } from "../lib/object-utils.js";
27
+ import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
26
28
  import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
29
+ import { persistHookScript } from '../lib/hook-script.js';
27
30
  import Download from "./download.js";
28
31
  import EnvAdd from "./env/add.js";
29
32
  import Install, { defaultDbPortForDialect } from "./install.js";
30
33
  const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
31
34
  const DEFAULT_INIT_APP_NAME = 'local';
35
+ const DEFAULT_INIT_PORTAL_TYPE = 'no-code';
36
+ const DEFAULT_INIT_PORTAL_NAME = 'admin';
37
+ const DEFAULT_INIT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
32
38
  const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
33
39
  const INIT_SETUP_MODES = ['install-new', 'manage-local', 'connect-remote'];
40
+ const INIT_PORTAL_TYPES = ['no-code', 'ai'];
34
41
  const INIT_ENV_ADD_FLAG_NAMES = [
35
42
  'locale',
36
43
  'default-api-base-url',
@@ -77,6 +84,9 @@ function isInstallNewSetupMode(values) {
77
84
  function isInstallLikeSetupMode(values) {
78
85
  return !isRemoteSetupMode(values);
79
86
  }
87
+ function isAiMode(values) {
88
+ return String(values.portalType ?? DEFAULT_INIT_PORTAL_TYPE).trim() === 'ai';
89
+ }
80
90
  function remoteConnectionOnly(def) {
81
91
  return withExtraHidden(def, (values) => !isRemoteSetupMode(values));
82
92
  }
@@ -246,16 +256,12 @@ function formatBrowserOpenError(error) {
246
256
  return String(error);
247
257
  }
248
258
  export default class Init extends Command {
249
- static summary = 'Set up NocoBase so coding agents can connect and work with it';
250
- static description = `Set up NocoBase for coding agents in the current workspace.
251
-
252
- \`nb init\` prepares a NocoBase environment that coding agents can use. It supports three setup paths:
259
+ static summary = 'Set up or connect a NocoBase environment in the current workspace';
260
+ static description = `Set up NocoBase in the current workspace.
253
261
 
254
- - Install a new NocoBase app, then save it as a CLI env.
255
- - Take over managing an app that already exists on this machine by reusing its database.
256
- - Connect a remote NocoBase app and save it as a CLI env.
262
+ \`nb init\` helps you install a new NocoBase app, take over managing one that already exists on this machine, or connect a remote NocoBase app and save it as a CLI env.
257
263
 
258
- It can also install NocoBase AI coding skills (\`nocobase/skills\`) so agents get the project-specific workflow guidance.
264
+ You can use the saved environment directly, or let a coding agent access it later. It can also install NocoBase AI coding skills (\`nocobase/skills\`) when you want agent-specific workflow guidance.
259
265
 
260
266
  If setup was interrupted earlier, use \`--resume\` with an existing env name to continue from the saved workspace config.
261
267
 
@@ -388,6 +394,42 @@ Prompt modes:
388
394
  devDependencies: installLikeDownloadExecutionOnly(Download.prompts.devDependencies),
389
395
  build: installLikeDownloadExecutionOnly(Download.prompts.build),
390
396
  buildDts: installLikeDownloadExecutionOnly(Download.prompts.buildDts),
397
+ portalType: installNewOnly({
398
+ type: 'select',
399
+ variant: 'radio',
400
+ message: initText('prompts.portalType.message'),
401
+ options: [
402
+ {
403
+ value: 'no-code',
404
+ label: initText('prompts.portalType.noCodeLabel'),
405
+ hint: initText('prompts.portalType.noCodeHint'),
406
+ },
407
+ {
408
+ value: 'ai',
409
+ label: initText('prompts.portalType.aiLabel'),
410
+ hint: initText('prompts.portalType.aiHint'),
411
+ },
412
+ ],
413
+ initialValue: DEFAULT_INIT_PORTAL_TYPE,
414
+ yesInitialValue: DEFAULT_INIT_PORTAL_TYPE,
415
+ required: true,
416
+ }),
417
+ portalName: installNewOnly({
418
+ type: 'text',
419
+ message: initText('prompts.portalName.message'),
420
+ placeholder: DEFAULT_INIT_PORTAL_NAME,
421
+ initialValue: DEFAULT_INIT_PORTAL_NAME,
422
+ yesInitialValue: DEFAULT_INIT_PORTAL_NAME,
423
+ required: true,
424
+ }),
425
+ portalTemplate: installNewOnly({
426
+ type: 'text',
427
+ message: initText('prompts.portalTemplate.message'),
428
+ placeholder: DEFAULT_INIT_PORTAL_TEMPLATE,
429
+ yesInitialValue: DEFAULT_INIT_PORTAL_TEMPLATE,
430
+ hidden: (values) => !isAiMode(values),
431
+ required: true,
432
+ }),
391
433
  dbDialect: installLikeOnly(Install.dbPrompts.dbDialect),
392
434
  builtinDb: installLikeOnly(Install.dbPrompts.builtinDb),
393
435
  builtinDbImage: installLikeOnly(Install.dbPrompts.builtinDbImage),
@@ -413,6 +455,13 @@ Prompt modes:
413
455
  ...Init.prompts,
414
456
  installApiBaseUrl: createInstallConnectionApiBaseUrlPrompt(options.defaultApiHost),
415
457
  };
458
+ const defaultPortalTemplate = String(options.defaultPortalTemplate ?? '').trim();
459
+ if (defaultPortalTemplate) {
460
+ prompts.portalTemplate = {
461
+ ...prompts.portalTemplate,
462
+ yesInitialValue: defaultPortalTemplate,
463
+ };
464
+ }
416
465
  if (flags['skip-auth']) {
417
466
  const accessTokenPrompt = {
418
467
  ...EnvAdd.prompts.accessToken,
@@ -462,6 +511,16 @@ Prompt modes:
462
511
  description: 'Setup mode: install a new app, manage a local app by reusing its database, or connect a remote app',
463
512
  options: [...INIT_SETUP_MODES],
464
513
  }),
514
+ 'portal-type': Flags.string({
515
+ description: 'Initial portal type: no-code or ai',
516
+ options: [...INIT_PORTAL_TYPES],
517
+ }),
518
+ 'portal-name': Flags.string({
519
+ description: 'Initial portal name',
520
+ }),
521
+ 'portal-template': Flags.string({
522
+ description: 'Initial portal template npm package or local path when --portal-type ai is used',
523
+ }),
465
524
  ui: Flags.boolean({
466
525
  description: 'Open the guided setup flow in a local browser form (not valid with --yes)',
467
526
  default: false,
@@ -475,7 +534,7 @@ Prompt modes:
475
534
  default: false,
476
535
  }),
477
536
  'ui-host': Flags.string({
478
- description: 'Host for the local --ui setup server (default: 127.0.0.1)',
537
+ description: 'Browser-accessible host for the --ui setup page URL (default: 127.0.0.1)',
479
538
  }),
480
539
  'ui-port': Flags.integer({
481
540
  description: 'Port for the local --ui setup server; 0 lets the OS choose an available port',
@@ -579,7 +638,8 @@ Prompt modes:
579
638
  const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
580
639
  const defaultUiHost = await resolveDefaultUiHost();
581
640
  const defaultApiHost = await resolveDefaultApiHost();
582
- const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost });
641
+ const defaultPortalTemplate = String(dynamicInitialValues.portalTemplate ?? '').trim();
642
+ const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost, defaultPortalTemplate });
583
643
  if (useBrowserUi) {
584
644
  presetValues = await runPromptCatalogWebUI({
585
645
  stages: Init.buildWebUiStages(promptCatalog),
@@ -608,6 +668,7 @@ Prompt modes:
608
668
  ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
609
669
  : {}),
610
670
  },
671
+ yesInitialValues: pickKeys(dynamicInitialValues, ['portalTemplate']),
611
672
  values: presetValues,
612
673
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
613
674
  hooks: {
@@ -680,7 +741,9 @@ Prompt modes:
680
741
  }
681
742
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
682
743
  const out = {};
683
- if (!Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
744
+ const shouldResolveAppInitialValues = !Object.prototype.hasOwnProperty.call(presetValues, 'appPort') ||
745
+ !Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate');
746
+ if (shouldResolveAppInitialValues) {
684
747
  const appInitialValues = await Install.buildAppPromptInitialValues({
685
748
  envName: String(presetValues.appName ?? '').trim(),
686
749
  flags: {
@@ -688,17 +751,31 @@ Prompt modes:
688
751
  'app-path': flags['app-path'] ?? '',
689
752
  'app-root-path': flags['app-root-path'] ?? '',
690
753
  'storage-path': flags['storage-path'] ?? '',
754
+ 'portal-template': flags['portal-template'] ??
755
+ (Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate')
756
+ ? String(presetValues.portalTemplate ?? '')
757
+ : undefined),
691
758
  },
692
759
  warnOnPortFallback: false,
693
760
  });
694
- if (appInitialValues.appPort !== undefined) {
761
+ if (appInitialValues.appPort !== undefined && !Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
695
762
  out.appPort = appInitialValues.appPort;
696
763
  }
764
+ if (appInitialValues.portalTemplate !== undefined &&
765
+ !Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate')) {
766
+ out.portalTemplate = appInitialValues.portalTemplate;
767
+ }
697
768
  }
698
769
  const downloadSeed = { ...presetValues };
699
770
  if (flags.yes && !Object.prototype.hasOwnProperty.call(downloadSeed, 'source')) {
700
771
  downloadSeed.source = 'docker';
701
772
  }
773
+ const builtinDbImageRegistry = String(presetValues.dockerRegistry ?? '').trim() ||
774
+ resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
775
+ if (!Object.prototype.hasOwnProperty.call(presetValues, 'dockerRegistry')) {
776
+ out.dockerRegistry = builtinDbImageRegistry;
777
+ }
778
+ out.builtinDbImageRegistry = builtinDbImageRegistry;
702
779
  const dbInitial = await Install.buildDbPromptInitialValues({
703
780
  flags,
704
781
  downloadResults: downloadSeed,
@@ -706,6 +783,9 @@ Prompt modes:
706
783
  warnOnPortFallback: false,
707
784
  });
708
785
  for (const [key, value] of Object.entries(dbInitial)) {
786
+ if (key === 'builtinDbImage') {
787
+ continue;
788
+ }
709
789
  if (!Object.prototype.hasOwnProperty.call(presetValues, key)) {
710
790
  out[key] = value;
711
791
  }
@@ -763,6 +843,14 @@ Prompt modes:
763
843
  buildDts: c.buildDts,
764
844
  },
765
845
  },
846
+ {
847
+ sectionTitle: initText('webUi.portalType.title'),
848
+ sectionDescription: initText('webUi.portalType.description'),
849
+ catalog: {
850
+ portalName: c.portalName,
851
+ portalType: c.portalType,
852
+ },
853
+ },
766
854
  {
767
855
  sectionTitle: initText('webUi.configureDatabase.title'),
768
856
  sectionDescription: initText('webUi.configureDatabase.description'),
@@ -863,6 +951,15 @@ Prompt modes:
863
951
  if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
864
952
  preset.appPublicPath = String(flags['app-public-path']).trim();
865
953
  }
954
+ if (flags['portal-type'] !== undefined && String(flags['portal-type']).trim() !== '') {
955
+ preset.portalType = String(flags['portal-type']).trim();
956
+ }
957
+ if (flags['portal-name'] !== undefined && String(flags['portal-name']).trim() !== '') {
958
+ preset.portalName = String(flags['portal-name']).trim();
959
+ }
960
+ if (flags['portal-template'] !== undefined && String(flags['portal-template']).trim() !== '') {
961
+ preset.portalTemplate = String(flags['portal-template']).trim();
962
+ }
866
963
  if (flags['root-username'] !== undefined) {
867
964
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
868
965
  }
@@ -1000,6 +1097,9 @@ Prompt modes:
1000
1097
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
1001
1098
  const appPort = String(results.appPort ?? '').trim();
1002
1099
  const appPublicPath = String(results.appPublicPath ?? '').trim();
1100
+ const portalType = String(results.portalType ?? '').trim();
1101
+ const portalName = String(results.portalName ?? '').trim();
1102
+ const portalTemplate = String(results.portalTemplate ?? '').trim();
1003
1103
  const source = String(results.source ?? '').trim();
1004
1104
  const version = resolveInitDownloadVersion(results);
1005
1105
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
@@ -1032,6 +1132,14 @@ Prompt modes:
1032
1132
  const skipDownload = results.skipDownload === true;
1033
1133
  const appKey = resolveManagedAppKey(results.appKey ?? existingEnv?.config.appKey);
1034
1134
  const timeZone = resolveManagedTimeZone(results.timeZone ?? existingEnv?.config.timezone);
1135
+ const hookScriptInput = String(flags['hook-script'] ?? '').trim();
1136
+ const hookAppPath = appPath || `./${envName}/`;
1137
+ const hookScript = hookScriptInput
1138
+ ? await persistHookScript({
1139
+ sourcePath: hookScriptInput,
1140
+ appPath: resolveConfiguredEnvPath(hookAppPath) ?? resolveEnvRelativePath(hookAppPath),
1141
+ })
1142
+ : String(results.hookScript ?? '').trim();
1035
1143
  const builtinDb = explicitDbHostFlag(flags)
1036
1144
  ? false
1037
1145
  : results.builtinDb === undefined
@@ -1040,6 +1148,7 @@ Prompt modes:
1040
1148
  results.appKey = appKey;
1041
1149
  results.timeZone = timeZone;
1042
1150
  await upsertEnv(envName, {
1151
+ schemaVersion: ENV_CONFIG_SCHEMA_VERSION,
1043
1152
  ...(source === 'docker'
1044
1153
  ? { kind: 'docker' }
1045
1154
  : source || appPath || appRootPath
@@ -1057,11 +1166,15 @@ Prompt modes:
1057
1166
  ...(dockerPlatform ? { dockerPlatform } : {}),
1058
1167
  ...(gitUrl ? { gitUrl } : {}),
1059
1168
  ...(npmRegistry ? { npmRegistry } : {}),
1169
+ ...(hookScript ? { hookScript } : {}),
1060
1170
  ...(appPath ? { appPath } : {}),
1061
1171
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
1062
1172
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
1063
1173
  ...(appPort ? { appPort } : {}),
1064
1174
  ...(appPublicPath ? { appPublicPath } : {}),
1175
+ ...(portalType && portalType !== DEFAULT_INIT_PORTAL_TYPE ? { portalType } : {}),
1176
+ ...(portalName ? { portalName } : {}),
1177
+ ...(portalTemplate ? { portalTemplate } : {}),
1065
1178
  ...(appKey ? { appKey } : {}),
1066
1179
  ...(timeZone ? { timezone: timeZone } : {}),
1067
1180
  ...(!skipDownload && results.devDependencies !== undefined
@@ -1132,6 +1245,10 @@ Prompt modes:
1132
1245
  if (flags['prepare-only']) {
1133
1246
  argv.push('--prepare-only');
1134
1247
  }
1248
+ const hookScript = String(results.hookScript ?? flags['hook-script'] ?? '').trim();
1249
+ if (hookScript) {
1250
+ argv.push('--hook-script', hookScript);
1251
+ }
1135
1252
  if (flags.verbose) {
1136
1253
  argv.push('--verbose');
1137
1254
  }
@@ -1182,6 +1299,18 @@ Prompt modes:
1182
1299
  if (appPublicPath) {
1183
1300
  argv.push('--app-public-path', appPublicPath);
1184
1301
  }
1302
+ const portalType = String(results.portalType ?? '').trim();
1303
+ if (portalType && portalType !== DEFAULT_INIT_PORTAL_TYPE) {
1304
+ argv.push('--portal-type', portalType);
1305
+ }
1306
+ const portalName = String(results.portalName ?? '').trim();
1307
+ if (portalName) {
1308
+ argv.push('--portal-name', portalName);
1309
+ }
1310
+ const portalTemplate = String(results.portalTemplate ?? '').trim();
1311
+ if (portalTemplate) {
1312
+ argv.push('--portal-template', portalTemplate);
1313
+ }
1185
1314
  if (flags.force) {
1186
1315
  argv.push('--force');
1187
1316
  }
@@ -1379,6 +1508,15 @@ Prompt modes:
1379
1508
  delete normalized.rootPassword;
1380
1509
  delete normalized.rootNickname;
1381
1510
  }
1511
+ const portalType = normalizeConnectionString(normalized.portalType) || DEFAULT_INIT_PORTAL_TYPE;
1512
+ normalized.portalType = portalType;
1513
+ normalized.portalName = normalizeConnectionString(normalized.portalName) || DEFAULT_INIT_PORTAL_NAME;
1514
+ if (portalType === 'ai') {
1515
+ normalized.portalTemplate = normalizeConnectionString(normalized.portalTemplate);
1516
+ }
1517
+ else {
1518
+ delete normalized.portalTemplate;
1519
+ }
1382
1520
  delete normalized.installApiBaseUrl;
1383
1521
  delete normalized.installAuthType;
1384
1522
  delete normalized.installUsername;