@nocobase/cli 2.1.31 → 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.
@@ -14,6 +14,7 @@ export default class ConfigSet extends Command {
14
14
  static description = 'Set a CLI configuration value for a supported configuration key.';
15
15
  static examples = [
16
16
  '<%= config.bin %> <%= command.id %> locale zh-CN',
17
+ '<%= config.bin %> <%= command.id %> default-portal-template @nocobase/portal-template-default',
17
18
  '<%= config.bin %> <%= command.id %> update.policy prompt',
18
19
  '<%= config.bin %> <%= command.id %> docker.network nocobase',
19
20
  '<%= config.bin %> <%= command.id %> docker.container-prefix nb',
@@ -32,8 +32,12 @@ import EnvAdd from "./env/add.js";
32
32
  import Install, { defaultDbPortForDialect } from "./install.js";
33
33
  const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
34
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';
35
38
  const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
36
39
  const INIT_SETUP_MODES = ['install-new', 'manage-local', 'connect-remote'];
40
+ const INIT_PORTAL_TYPES = ['no-code', 'ai'];
37
41
  const INIT_ENV_ADD_FLAG_NAMES = [
38
42
  'locale',
39
43
  'default-api-base-url',
@@ -80,6 +84,9 @@ function isInstallNewSetupMode(values) {
80
84
  function isInstallLikeSetupMode(values) {
81
85
  return !isRemoteSetupMode(values);
82
86
  }
87
+ function isAiMode(values) {
88
+ return String(values.portalType ?? DEFAULT_INIT_PORTAL_TYPE).trim() === 'ai';
89
+ }
83
90
  function remoteConnectionOnly(def) {
84
91
  return withExtraHidden(def, (values) => !isRemoteSetupMode(values));
85
92
  }
@@ -387,6 +394,42 @@ Prompt modes:
387
394
  devDependencies: installLikeDownloadExecutionOnly(Download.prompts.devDependencies),
388
395
  build: installLikeDownloadExecutionOnly(Download.prompts.build),
389
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
+ }),
390
433
  dbDialect: installLikeOnly(Install.dbPrompts.dbDialect),
391
434
  builtinDb: installLikeOnly(Install.dbPrompts.builtinDb),
392
435
  builtinDbImage: installLikeOnly(Install.dbPrompts.builtinDbImage),
@@ -412,6 +455,13 @@ Prompt modes:
412
455
  ...Init.prompts,
413
456
  installApiBaseUrl: createInstallConnectionApiBaseUrlPrompt(options.defaultApiHost),
414
457
  };
458
+ const defaultPortalTemplate = String(options.defaultPortalTemplate ?? '').trim();
459
+ if (defaultPortalTemplate) {
460
+ prompts.portalTemplate = {
461
+ ...prompts.portalTemplate,
462
+ yesInitialValue: defaultPortalTemplate,
463
+ };
464
+ }
415
465
  if (flags['skip-auth']) {
416
466
  const accessTokenPrompt = {
417
467
  ...EnvAdd.prompts.accessToken,
@@ -461,6 +511,16 @@ Prompt modes:
461
511
  description: 'Setup mode: install a new app, manage a local app by reusing its database, or connect a remote app',
462
512
  options: [...INIT_SETUP_MODES],
463
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
+ }),
464
524
  ui: Flags.boolean({
465
525
  description: 'Open the guided setup flow in a local browser form (not valid with --yes)',
466
526
  default: false,
@@ -578,7 +638,8 @@ Prompt modes:
578
638
  const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
579
639
  const defaultUiHost = await resolveDefaultUiHost();
580
640
  const defaultApiHost = await resolveDefaultApiHost();
581
- const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost });
641
+ const defaultPortalTemplate = String(dynamicInitialValues.portalTemplate ?? '').trim();
642
+ const promptCatalog = this.buildPromptCatalog(normalizedFlags, { defaultApiHost, defaultPortalTemplate });
582
643
  if (useBrowserUi) {
583
644
  presetValues = await runPromptCatalogWebUI({
584
645
  stages: Init.buildWebUiStages(promptCatalog),
@@ -607,6 +668,7 @@ Prompt modes:
607
668
  ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
608
669
  : {}),
609
670
  },
671
+ yesInitialValues: pickKeys(dynamicInitialValues, ['portalTemplate']),
610
672
  values: presetValues,
611
673
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
612
674
  hooks: {
@@ -679,7 +741,9 @@ Prompt modes:
679
741
  }
680
742
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
681
743
  const out = {};
682
- 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) {
683
747
  const appInitialValues = await Install.buildAppPromptInitialValues({
684
748
  envName: String(presetValues.appName ?? '').trim(),
685
749
  flags: {
@@ -687,12 +751,20 @@ Prompt modes:
687
751
  'app-path': flags['app-path'] ?? '',
688
752
  'app-root-path': flags['app-root-path'] ?? '',
689
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),
690
758
  },
691
759
  warnOnPortFallback: false,
692
760
  });
693
- if (appInitialValues.appPort !== undefined) {
761
+ if (appInitialValues.appPort !== undefined && !Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
694
762
  out.appPort = appInitialValues.appPort;
695
763
  }
764
+ if (appInitialValues.portalTemplate !== undefined &&
765
+ !Object.prototype.hasOwnProperty.call(presetValues, 'portalTemplate')) {
766
+ out.portalTemplate = appInitialValues.portalTemplate;
767
+ }
696
768
  }
697
769
  const downloadSeed = { ...presetValues };
698
770
  if (flags.yes && !Object.prototype.hasOwnProperty.call(downloadSeed, 'source')) {
@@ -771,6 +843,14 @@ Prompt modes:
771
843
  buildDts: c.buildDts,
772
844
  },
773
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
+ },
774
854
  {
775
855
  sectionTitle: initText('webUi.configureDatabase.title'),
776
856
  sectionDescription: initText('webUi.configureDatabase.description'),
@@ -871,6 +951,15 @@ Prompt modes:
871
951
  if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
872
952
  preset.appPublicPath = String(flags['app-public-path']).trim();
873
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
+ }
874
963
  if (flags['root-username'] !== undefined) {
875
964
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
876
965
  }
@@ -1008,6 +1097,9 @@ Prompt modes:
1008
1097
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
1009
1098
  const appPort = String(results.appPort ?? '').trim();
1010
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();
1011
1103
  const source = String(results.source ?? '').trim();
1012
1104
  const version = resolveInitDownloadVersion(results);
1013
1105
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
@@ -1080,6 +1172,9 @@ Prompt modes:
1080
1172
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
1081
1173
  ...(appPort ? { appPort } : {}),
1082
1174
  ...(appPublicPath ? { appPublicPath } : {}),
1175
+ ...(portalType && portalType !== DEFAULT_INIT_PORTAL_TYPE ? { portalType } : {}),
1176
+ ...(portalName ? { portalName } : {}),
1177
+ ...(portalTemplate ? { portalTemplate } : {}),
1083
1178
  ...(appKey ? { appKey } : {}),
1084
1179
  ...(timeZone ? { timezone: timeZone } : {}),
1085
1180
  ...(!skipDownload && results.devDependencies !== undefined
@@ -1204,6 +1299,18 @@ Prompt modes:
1204
1299
  if (appPublicPath) {
1205
1300
  argv.push('--app-public-path', appPublicPath);
1206
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
+ }
1207
1314
  if (flags.force) {
1208
1315
  argv.push('--force');
1209
1316
  }
@@ -1401,6 +1508,15 @@ Prompt modes:
1401
1508
  delete normalized.rootPassword;
1402
1509
  delete normalized.rootNickname;
1403
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
+ }
1404
1520
  delete normalized.installApiBaseUrl;
1405
1521
  delete normalized.installAuthType;
1406
1522
  delete normalized.installUsername;
@@ -53,6 +53,10 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
53
53
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
54
54
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
55
55
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
56
+ const DEFAULT_INSTALL_PORTAL_TYPE = 'no-code';
57
+ const DEFAULT_INSTALL_PORTAL_NAME = 'admin';
58
+ const DEFAULT_INSTALL_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
59
+ const INSTALL_PORTAL_TYPES = ['no-code', 'ai'];
56
60
  function toOptionalPromptString(value) {
57
61
  const text = String(value ?? '').trim();
58
62
  return text || undefined;
@@ -180,6 +184,9 @@ function defaultBuiltinDbImageForDialect(value, options) {
180
184
  function defaultDbDatabaseForDialect(value) {
181
185
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
182
186
  }
187
+ function isAiMode(values) {
188
+ return String(values.portalType ?? DEFAULT_INSTALL_PORTAL_TYPE).trim() === 'ai';
189
+ }
183
190
  function supportsDbSchemaPrompt(value) {
184
191
  const dialect = String(value ?? '').trim();
185
192
  return dialect === 'postgres' || dialect === 'kingbase';
@@ -404,6 +411,16 @@ export default class Install extends Command {
404
411
  'app-public-path': Flags.string({
405
412
  description: 'Public path for the local app, for example / or /console/',
406
413
  }),
414
+ 'portal-type': Flags.string({
415
+ description: 'Initial portal type for the installed app',
416
+ options: [...INSTALL_PORTAL_TYPES],
417
+ }),
418
+ 'portal-name': Flags.string({
419
+ description: 'Initial portal name',
420
+ }),
421
+ 'portal-template': Flags.string({
422
+ description: 'Initial portal template npm package or local path when --portal-type ai is used',
423
+ }),
407
424
  'root-username': Flags.string({
408
425
  description: 'Initial admin username for the installed app',
409
426
  required: false,
@@ -513,6 +530,41 @@ export default class Install extends Command {
513
530
  yesInitialValue: '/',
514
531
  validate: validateAppPublicPath,
515
532
  },
533
+ portalType: {
534
+ type: 'select',
535
+ message: installText('prompts.portalType.message'),
536
+ options: [
537
+ {
538
+ value: 'no-code',
539
+ label: installText('prompts.portalType.noCodeLabel'),
540
+ hint: installText('prompts.portalType.noCodeHint'),
541
+ },
542
+ {
543
+ value: 'ai',
544
+ label: installText('prompts.portalType.aiLabel'),
545
+ hint: installText('prompts.portalType.aiHint'),
546
+ },
547
+ ],
548
+ initialValue: DEFAULT_INSTALL_PORTAL_TYPE,
549
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TYPE,
550
+ required: true,
551
+ },
552
+ portalName: {
553
+ type: 'text',
554
+ message: installText('prompts.portalName.message'),
555
+ placeholder: DEFAULT_INSTALL_PORTAL_NAME,
556
+ initialValue: DEFAULT_INSTALL_PORTAL_NAME,
557
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_NAME,
558
+ required: true,
559
+ },
560
+ portalTemplate: {
561
+ type: 'text',
562
+ message: installText('prompts.portalTemplate.message'),
563
+ placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
564
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
565
+ hidden: (values) => !isAiMode(values),
566
+ required: true,
567
+ },
516
568
  };
517
569
  }
518
570
  static dbPrompts = {
@@ -772,6 +824,24 @@ export default class Install extends Command {
772
824
  preset.appPublicPath = v;
773
825
  }
774
826
  }
827
+ if (flags['portal-type'] !== undefined) {
828
+ const v = String(flags['portal-type'] ?? '').trim();
829
+ if (v) {
830
+ preset.portalType = v;
831
+ }
832
+ }
833
+ if (flags['portal-name'] !== undefined) {
834
+ const v = String(flags['portal-name'] ?? '').trim();
835
+ if (v) {
836
+ preset.portalName = v;
837
+ }
838
+ }
839
+ if (flags['portal-template'] !== undefined) {
840
+ const v = String(flags['portal-template'] ?? '').trim();
841
+ if (v) {
842
+ preset.portalTemplate = v;
843
+ }
844
+ }
775
845
  if (flags['root-username'] !== undefined) {
776
846
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
777
847
  }
@@ -863,6 +933,9 @@ export default class Install extends Command {
863
933
  'appPort',
864
934
  'storagePath',
865
935
  'appPublicPath',
936
+ 'portalType',
937
+ 'portalName',
938
+ 'portalTemplate',
866
939
  ]);
867
940
  }
868
941
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -1112,6 +1185,9 @@ export default class Install extends Command {
1112
1185
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1113
1186
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1114
1187
  const lang = Install.toOptionalPromptString(config.lang);
1188
+ const portalType = Install.toOptionalPromptString(config.portalType);
1189
+ const portalName = Install.toOptionalPromptString(config.portalName);
1190
+ const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1115
1191
  const auth = config.auth;
1116
1192
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1117
1193
  const appPreset = {
@@ -1121,6 +1197,9 @@ export default class Install extends Command {
1121
1197
  ...(appPort ? { appPort } : {}),
1122
1198
  ...(storagePath ? { storagePath } : {}),
1123
1199
  ...(appPublicPath ? { appPublicPath } : {}),
1200
+ ...(portalType ? { portalType } : {}),
1201
+ ...(portalName ? { portalName } : {}),
1202
+ ...(portalTemplate ? { portalTemplate } : {}),
1124
1203
  ...(hookScript ? { hookScript } : {}),
1125
1204
  };
1126
1205
  const downloadPreset = {
@@ -1263,6 +1342,12 @@ export default class Install extends Command {
1263
1342
  warn: params.warnOnPortFallback ?? true,
1264
1343
  });
1265
1344
  }
1345
+ if (params.flags['portal-template'] === undefined) {
1346
+ const defaultPortalTemplate = Install.toOptionalPromptString(await getCliConfigValue('default-portal-template'));
1347
+ if (defaultPortalTemplate) {
1348
+ initialValues.portalTemplate = defaultPortalTemplate;
1349
+ }
1350
+ }
1266
1351
  return initialValues;
1267
1352
  }
1268
1353
  static shouldPublishBuiltinDbPortForValues(values) {
@@ -1430,14 +1515,17 @@ export default class Install extends Command {
1430
1515
  static buildDockerAppContainerName(envName, containerPrefix) {
1431
1516
  return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1432
1517
  }
1433
- static buildInitAppEnvVars(params) {
1518
+ static buildInitAppEnvVars(params, options = {}) {
1434
1519
  return buildInitAppEnvVarsFromConfig({
1435
1520
  lang: String(params.appResults.lang ?? ''),
1436
1521
  rootUsername: String(params.rootResults.rootUsername ?? ''),
1437
1522
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1438
1523
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1439
1524
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1440
- });
1525
+ portalType: String(params.appResults.portalType ?? ''),
1526
+ portalName: String(params.appResults.portalName ?? ''),
1527
+ portalTemplate: String(params.appResults.portalTemplate ?? ''),
1528
+ }, options);
1441
1529
  }
1442
1530
  static shouldPublishBuiltinDbPort(source) {
1443
1531
  return String(source ?? '').trim() !== 'docker';
@@ -2309,6 +2397,9 @@ export default class Install extends Command {
2309
2397
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2310
2398
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2311
2399
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2400
+ const portalType = Install.toOptionalPromptString(params.appResults.portalType);
2401
+ const portalName = Install.toOptionalPromptString(params.appResults.portalName);
2402
+ const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2312
2403
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2313
2404
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2314
2405
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
@@ -2343,6 +2434,9 @@ export default class Install extends Command {
2343
2434
  ...(appPublicPath ? { appPublicPath } : {}),
2344
2435
  ...(envFile ? { envFile } : {}),
2345
2436
  lang: params.appResults.lang,
2437
+ portalType,
2438
+ portalName,
2439
+ portalTemplate,
2346
2440
  appKey: params.appResults.appKey,
2347
2441
  timezone: params.appResults.timeZone,
2348
2442
  builtinDb: params.dbResults.builtinDb,
@@ -2394,6 +2488,7 @@ export default class Install extends Command {
2394
2488
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2395
2489
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2396
2490
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2491
+ 'portal-template': parsed['portal-template'] ?? Install.toOptionalPromptString(appPreset.portalTemplate),
2397
2492
  },
2398
2493
  }),
2399
2494
  values: appPreset,
@@ -0,0 +1,88 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { configurePortalWorkspace } from '../../lib/portal-configure.js';
15
+ import { printInfo, printSuccess } from '../../lib/ui.js';
16
+ const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
17
+ export default class PortalConfig extends Command {
18
+ static summary = 'Update portal source configuration';
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %> customer --source-storage nocobase',
21
+ '<%= config.bin %> <%= command.id %> customer --source-storage git --git-repo git@github.com:nocobase/customer-portal.git',
22
+ '<%= config.bin %> <%= command.id %> customer --git-branch main --git-path portals/customer',
23
+ ];
24
+ static args = {
25
+ portal: Args.string({
26
+ required: true,
27
+ description: 'Portal name',
28
+ }),
29
+ };
30
+ static flags = {
31
+ env: Flags.string({
32
+ char: 'e',
33
+ description: 'CLI env name; omitted uses the current env',
34
+ }),
35
+ yes: Flags.boolean({
36
+ char: 'y',
37
+ description: 'Confirm using --env when it targets a different env than the current env',
38
+ default: false,
39
+ }),
40
+ 'source-storage': Flags.string({
41
+ description: 'Where portal source code is managed',
42
+ options: ['nocobase', 'git'],
43
+ }),
44
+ 'git-repo': Flags.string({
45
+ description: 'Git repository URL used when --source-storage=git',
46
+ }),
47
+ 'git-branch': Flags.string({
48
+ description: 'Git branch used when --source-storage=git',
49
+ }),
50
+ 'git-path': Flags.string({
51
+ description: 'Directory inside the Git repository for this portal; defaults to the repository root',
52
+ }),
53
+ };
54
+ async run() {
55
+ const { args, flags } = await this.parse(PortalConfig);
56
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
57
+ const confirmed = await ensureCrossEnvConfirmed({
58
+ command: this,
59
+ requestedEnv,
60
+ yes: flags.yes,
61
+ });
62
+ if (!confirmed) {
63
+ return;
64
+ }
65
+ const scope = resolveDefaultConfigScope();
66
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
67
+ const env = await getEnv(envName, { scope });
68
+ if (!env) {
69
+ this.error(portalConfigureText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
70
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
71
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
72
+ }
73
+ const result = await configurePortalWorkspace({
74
+ portal: args.portal,
75
+ env,
76
+ envName,
77
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
78
+ sourceStorage: flags['source-storage'],
79
+ gitRepo: flags['git-repo'],
80
+ gitBranch: flags['git-branch'],
81
+ gitPath: flags['git-path'],
82
+ });
83
+ printSuccess(portalConfigureText('messages.updated', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" configuration updated at ${result.portalDir}/portal.config.json.`));
84
+ printInfo(result.remoteSynced
85
+ ? portalConfigureText('messages.remoteSynced', undefined, 'Remote portal record: synced')
86
+ : portalConfigureText('messages.remoteSkipped', undefined, 'Remote portal record: not found; local config only'));
87
+ }
88
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Args, Command, Flags } from '@oclif/core';
10
+ import { getEnv } from '../../lib/auth-store.js';
11
+ import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
13
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
14
+ import { createPortalWorkspace } from '../../lib/portal-create.js';
15
+ import { printInfo, printSuccess } from '../../lib/ui.js';
16
+ const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
17
+ const portalCreateText = (key, values, fallback) => translateCli(`commands.portalCreate.${key}`, values, { fallback });
18
+ export default class PortalCreate extends Command {
19
+ static summary = 'Create a local portal from a template';
20
+ static examples = [
21
+ '<%= config.bin %> <%= command.id %> customer',
22
+ '<%= config.bin %> <%= command.id %> customer --template @nocobase/portal-template-default',
23
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
24
+ '<%= config.bin %> <%= command.id %> customer --source-storage git --git-repo git@github.com:nocobase/customer-portal.git',
25
+ ];
26
+ static args = {
27
+ portal: Args.string({
28
+ required: true,
29
+ description: 'Portal name',
30
+ }),
31
+ };
32
+ static flags = {
33
+ template: Flags.string({
34
+ description: 'Template package, local path, or file:// URL',
35
+ default: DEFAULT_PORTAL_TEMPLATE,
36
+ }),
37
+ env: Flags.string({
38
+ char: 'e',
39
+ description: 'CLI env name; omitted uses the current env',
40
+ }),
41
+ yes: Flags.boolean({
42
+ char: 'y',
43
+ description: 'Confirm using --env when it targets a different env than the current env',
44
+ default: false,
45
+ }),
46
+ title: Flags.string({
47
+ description: 'Portal display title; defaults to a title generated from the portal slug',
48
+ }),
49
+ force: Flags.boolean({
50
+ description: 'Delete the existing portal and recreate it',
51
+ default: false,
52
+ }),
53
+ 'source-storage': Flags.string({
54
+ description: 'Where portal source code is managed',
55
+ options: ['nocobase', 'git'],
56
+ default: 'nocobase',
57
+ }),
58
+ 'git-repo': Flags.string({
59
+ description: 'Git repository URL used when --source-storage=git',
60
+ }),
61
+ 'git-branch': Flags.string({
62
+ description: 'Git branch used when --source-storage=git',
63
+ }),
64
+ 'git-path': Flags.string({
65
+ description: 'Directory inside the Git repository for this portal; defaults to the repository root',
66
+ }),
67
+ };
68
+ async run() {
69
+ const { args, flags } = await this.parse(PortalCreate);
70
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
71
+ const confirmed = await ensureCrossEnvConfirmed({
72
+ command: this,
73
+ requestedEnv,
74
+ yes: flags.yes,
75
+ });
76
+ if (!confirmed) {
77
+ return;
78
+ }
79
+ const env = await getEnv(flags.env, { scope: resolveDefaultConfigScope() });
80
+ if (!env) {
81
+ this.error(flags.env
82
+ ? portalCreateText('errors.envNotConfigured', { envName: flags.env }, `Env "${flags.env}" is not configured. Run \`nb env add ${flags.env} --api-base-url <url>\` first.`)
83
+ : portalCreateText('errors.noEnvConfigured', undefined, 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
84
+ }
85
+ const result = await createPortalWorkspace({
86
+ portal: args.portal,
87
+ title: flags.title,
88
+ template: flags.template,
89
+ env,
90
+ envName: flags.env,
91
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
92
+ force: flags.force,
93
+ sourceStorage: flags['source-storage'],
94
+ gitRepo: flags['git-repo'],
95
+ gitBranch: flags['git-branch'],
96
+ gitPath: flags['git-path'],
97
+ onSkipInstall: (message) => printInfo(message),
98
+ });
99
+ printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}.`));
100
+ printInfo(portalCreateText('messages.app', { app: result.app }, `App: ${result.app}`));
101
+ printInfo(portalCreateText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
102
+ printInfo(portalCreateText('messages.sourceStorage', { sourceStorage: result.sourceStorage }, `Source storage: ${result.sourceStorage}`));
103
+ }
104
+ }