@nocobase/cli 2.3.0-alpha.1 → 3.0.0-alpha.1

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 (43) hide show
  1. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  2. package/dist/commands/config/set.js +1 -0
  3. package/dist/commands/init.js +13 -5
  4. package/dist/commands/install.js +104 -3
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +105 -0
  7. package/dist/commands/portal/deploy.js +81 -0
  8. package/dist/commands/portal/destroy.js +104 -0
  9. package/dist/commands/portal/dev.js +71 -0
  10. package/dist/commands/portal/index.js +20 -0
  11. package/dist/commands/portal/info.js +82 -0
  12. package/dist/commands/portal/list.js +98 -0
  13. package/dist/commands/portal/pull.js +84 -0
  14. package/dist/commands/portal/push.js +79 -0
  15. package/dist/commands/source/dev.js +1 -1
  16. package/dist/lib/api-client.js +7 -0
  17. package/dist/lib/auth-store.js +3 -1
  18. package/dist/lib/cli-config.js +20 -1
  19. package/dist/lib/env-auth.js +2 -2
  20. package/dist/lib/env-config.js +3 -0
  21. package/dist/lib/env-proxy.js +141 -8
  22. package/dist/lib/managed-env-file.js +58 -2
  23. package/dist/lib/managed-init-env.js +6 -1
  24. package/dist/lib/naming.js +9 -0
  25. package/dist/lib/portal-command-env.js +31 -0
  26. package/dist/lib/portal-config.js +133 -0
  27. package/dist/lib/portal-configure.js +117 -0
  28. package/dist/lib/portal-create.js +433 -0
  29. package/dist/lib/portal-deploy.js +283 -0
  30. package/dist/lib/portal-destroy.js +100 -0
  31. package/dist/lib/portal-dev.js +79 -0
  32. package/dist/lib/portal-env-files.js +53 -0
  33. package/dist/lib/portal-info.js +31 -0
  34. package/dist/lib/portal-list.js +211 -0
  35. package/dist/lib/portal-source.js +523 -0
  36. package/dist/lib/proxy-caddy.js +2 -0
  37. package/dist/lib/proxy-nginx.js +1 -0
  38. package/dist/lib/run-npm.js +17 -16
  39. package/dist/lib/ui.js +28 -1
  40. package/dist/locale/en-US.json +178 -0
  41. package/dist/locale/zh-CN.json +178 -0
  42. package/nocobase-ctl.config.json +111 -0
  43. package/package.json +5 -2
@@ -1,12 +1,14 @@
1
1
  add_header Cache-Control "public" always;
2
+ add_header Content-Security-Policy "sandbox" always;
2
3
  add_header X-Content-Type-Options "nosniff" always;
3
4
 
4
5
  access_log off;
5
6
  autoindex off;
6
7
 
7
8
  # Force potentially renderable uploaded files to download.
8
- location ~* \.(?:htm|html|svg|svgz|xhtml)$ {
9
+ location ~* \.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$ {
9
10
  add_header Cache-Control "public" always;
11
+ add_header Content-Security-Policy "sandbox" always;
10
12
  add_header X-Content-Type-Options "nosniff" always;
11
13
  add_header Content-Disposition "attachment" always;
12
14
  }
@@ -16,6 +18,7 @@ location ~* \.md$ {
16
18
  default_type text/markdown;
17
19
 
18
20
  add_header Cache-Control "public" always;
21
+ add_header Content-Security-Policy "sandbox" always;
19
22
  add_header X-Content-Type-Options "nosniff" always;
20
23
  add_header Content-Disposition "inline" always;
21
24
  }
@@ -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',
@@ -27,6 +27,7 @@ import { omitKeys, pickKeys } from "../lib/object-utils.js";
27
27
  import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
28
28
  import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
29
29
  import { persistHookScript } from '../lib/hook-script.js';
30
+ import { ensureManagedEnvFileDefaults } from '../lib/managed-env-file.js';
30
31
  import Download from "./download.js";
31
32
  import EnvAdd from "./env/add.js";
32
33
  import Install, { defaultDbPortForDialect } from "./install.js";
@@ -607,6 +608,7 @@ Prompt modes:
607
608
  ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
608
609
  : {}),
609
610
  },
611
+ yesInitialValues: {},
610
612
  values: presetValues,
611
613
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
612
614
  hooks: {
@@ -679,7 +681,8 @@ Prompt modes:
679
681
  }
680
682
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
681
683
  const out = {};
682
- if (!Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
684
+ const shouldResolveAppInitialValues = !Object.prototype.hasOwnProperty.call(presetValues, 'appPort');
685
+ if (shouldResolveAppInitialValues) {
683
686
  const appInitialValues = await Install.buildAppPromptInitialValues({
684
687
  envName: String(presetValues.appName ?? '').trim(),
685
688
  flags: {
@@ -690,7 +693,7 @@ Prompt modes:
690
693
  },
691
694
  warnOnPortFallback: false,
692
695
  });
693
- if (appInitialValues.appPort !== undefined) {
696
+ if (appInitialValues.appPort !== undefined && !Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
694
697
  out.appPort = appInitialValues.appPort;
695
698
  }
696
699
  }
@@ -1034,7 +1037,8 @@ Prompt modes:
1034
1037
  const dbSchema = String(results.dbSchema ?? '').trim();
1035
1038
  const dbTablePrefix = String(results.dbTablePrefix ?? '').trim();
1036
1039
  const apiBaseUrl = String(results.apiBaseUrl ?? '').trim();
1037
- const authType = String(results.authType ?? '').trim() || 'oauth';
1040
+ const authTypeInput = String(results.authType ?? '').trim();
1041
+ const authType = authTypeInput === 'basic' || authTypeInput === 'token' || authTypeInput === 'oauth' ? authTypeInput : 'oauth';
1038
1042
  const authUsername = authType === 'basic' ? String(results.username ?? results.rootUsername ?? '').trim() : '';
1039
1043
  const accessToken = String(results.accessToken ?? '');
1040
1044
  const skipDownload = results.skipDownload === true;
@@ -1055,7 +1059,7 @@ Prompt modes:
1055
1059
  : Boolean(results.builtinDb);
1056
1060
  results.appKey = appKey;
1057
1061
  results.timeZone = timeZone;
1058
- await upsertEnv(envName, {
1062
+ const savedEnvConfig = {
1059
1063
  schemaVersion: ENV_CONFIG_SCHEMA_VERSION,
1060
1064
  ...(source === 'docker'
1061
1065
  ? { kind: 'docker' }
@@ -1100,7 +1104,11 @@ Prompt modes:
1100
1104
  ...(results.dbUnderscored !== undefined ? { dbUnderscored: Boolean(results.dbUnderscored) } : {}),
1101
1105
  setupState: 'prepared',
1102
1106
  ...(String(results.lang ?? '').trim() ? { lang: String(results.lang ?? '').trim() } : {}),
1103
- }, { scope: resolveDefaultConfigScope() });
1107
+ };
1108
+ await upsertEnv(envName, savedEnvConfig, { scope: resolveDefaultConfigScope() });
1109
+ if (source === 'docker' || appPath) {
1110
+ await ensureManagedEnvFileDefaults(envName, savedEnvConfig);
1111
+ }
1104
1112
  }
1105
1113
  buildEnvAddArgv(results) {
1106
1114
  const argv = [String(results.appName ?? DEFAULT_INIT_APP_NAME)];
@@ -29,6 +29,7 @@ import { buildStoredEnvConfig } from '../lib/env-config.js';
29
29
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
30
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
31
  import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
32
+ import { ensureManagedEnvFileDefaults } from '../lib/managed-env-file.js';
32
33
  import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
33
34
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
34
35
  import Download from './download.js';
@@ -53,6 +54,10 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
53
54
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
54
55
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
55
56
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
57
+ const DEFAULT_INSTALL_PORTAL_TYPE = 'ai';
58
+ const DEFAULT_INSTALL_PORTAL_NAME = 'main';
59
+ const DEFAULT_INSTALL_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
60
+ const INSTALL_PORTAL_TYPES = ['no-code', 'ai'];
56
61
  function toOptionalPromptString(value) {
57
62
  const text = String(value ?? '').trim();
58
63
  return text || undefined;
@@ -180,6 +185,9 @@ function defaultBuiltinDbImageForDialect(value, options) {
180
185
  function defaultDbDatabaseForDialect(value) {
181
186
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
182
187
  }
188
+ function isAiMode(values) {
189
+ return String(values.portalType ?? DEFAULT_INSTALL_PORTAL_TYPE).trim() === 'ai';
190
+ }
183
191
  function supportsDbSchemaPrompt(value) {
184
192
  const dialect = String(value ?? '').trim();
185
193
  return dialect === 'postgres' || dialect === 'kingbase';
@@ -404,6 +412,16 @@ export default class Install extends Command {
404
412
  'app-public-path': Flags.string({
405
413
  description: 'Public path for the local app, for example / or /console/',
406
414
  }),
415
+ 'portal-type': Flags.string({
416
+ description: 'Initial portal type for the installed app',
417
+ options: [...INSTALL_PORTAL_TYPES],
418
+ }),
419
+ 'portal-name': Flags.string({
420
+ description: 'Initial portal name',
421
+ }),
422
+ 'portal-template': Flags.string({
423
+ description: 'Initial portal template npm package or local path when --portal-type ai is used',
424
+ }),
407
425
  'root-username': Flags.string({
408
426
  description: 'Initial admin username for the installed app',
409
427
  required: false,
@@ -513,6 +531,41 @@ export default class Install extends Command {
513
531
  yesInitialValue: '/',
514
532
  validate: validateAppPublicPath,
515
533
  },
534
+ portalType: {
535
+ type: 'select',
536
+ message: installText('prompts.portalType.message'),
537
+ options: [
538
+ {
539
+ value: 'no-code',
540
+ label: installText('prompts.portalType.noCodeLabel'),
541
+ hint: installText('prompts.portalType.noCodeHint'),
542
+ },
543
+ {
544
+ value: 'ai',
545
+ label: installText('prompts.portalType.aiLabel'),
546
+ hint: installText('prompts.portalType.aiHint'),
547
+ },
548
+ ],
549
+ initialValue: DEFAULT_INSTALL_PORTAL_TYPE,
550
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TYPE,
551
+ required: true,
552
+ },
553
+ portalName: {
554
+ type: 'text',
555
+ message: installText('prompts.portalName.message'),
556
+ placeholder: DEFAULT_INSTALL_PORTAL_NAME,
557
+ initialValue: DEFAULT_INSTALL_PORTAL_NAME,
558
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_NAME,
559
+ required: true,
560
+ },
561
+ portalTemplate: {
562
+ type: 'text',
563
+ message: installText('prompts.portalTemplate.message'),
564
+ placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
565
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
566
+ hidden: (values) => !isAiMode(values),
567
+ required: true,
568
+ },
516
569
  };
517
570
  }
518
571
  static dbPrompts = {
@@ -772,6 +825,24 @@ export default class Install extends Command {
772
825
  preset.appPublicPath = v;
773
826
  }
774
827
  }
828
+ if (flags['portal-type'] !== undefined) {
829
+ const v = String(flags['portal-type'] ?? '').trim();
830
+ if (v) {
831
+ preset.portalType = v;
832
+ }
833
+ }
834
+ if (flags['portal-name'] !== undefined) {
835
+ const v = String(flags['portal-name'] ?? '').trim();
836
+ if (v) {
837
+ preset.portalName = v;
838
+ }
839
+ }
840
+ if (flags['portal-template'] !== undefined) {
841
+ const v = String(flags['portal-template'] ?? '').trim();
842
+ if (v) {
843
+ preset.portalTemplate = v;
844
+ }
845
+ }
775
846
  if (flags['root-username'] !== undefined) {
776
847
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
777
848
  }
@@ -863,6 +934,9 @@ export default class Install extends Command {
863
934
  'appPort',
864
935
  'storagePath',
865
936
  'appPublicPath',
937
+ 'portalType',
938
+ 'portalName',
939
+ 'portalTemplate',
866
940
  ]);
867
941
  }
868
942
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -1112,6 +1186,9 @@ export default class Install extends Command {
1112
1186
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1113
1187
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1114
1188
  const lang = Install.toOptionalPromptString(config.lang);
1189
+ const portalType = Install.toOptionalPromptString(config.portalType);
1190
+ const portalName = Install.toOptionalPromptString(config.portalName);
1191
+ const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1115
1192
  const auth = config.auth;
1116
1193
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1117
1194
  const appPreset = {
@@ -1121,6 +1198,9 @@ export default class Install extends Command {
1121
1198
  ...(appPort ? { appPort } : {}),
1122
1199
  ...(storagePath ? { storagePath } : {}),
1123
1200
  ...(appPublicPath ? { appPublicPath } : {}),
1201
+ ...(portalType ? { portalType } : {}),
1202
+ ...(portalName ? { portalName } : {}),
1203
+ ...(portalTemplate ? { portalTemplate } : {}),
1124
1204
  ...(hookScript ? { hookScript } : {}),
1125
1205
  };
1126
1206
  const downloadPreset = {
@@ -1263,6 +1343,12 @@ export default class Install extends Command {
1263
1343
  warn: params.warnOnPortFallback ?? true,
1264
1344
  });
1265
1345
  }
1346
+ if (params.flags['portal-template'] === undefined) {
1347
+ const defaultPortalTemplate = Install.toOptionalPromptString(await getCliConfigValue('default-portal-template'));
1348
+ if (defaultPortalTemplate) {
1349
+ initialValues.portalTemplate = defaultPortalTemplate;
1350
+ }
1351
+ }
1266
1352
  return initialValues;
1267
1353
  }
1268
1354
  static shouldPublishBuiltinDbPortForValues(values) {
@@ -1430,14 +1516,17 @@ export default class Install extends Command {
1430
1516
  static buildDockerAppContainerName(envName, containerPrefix) {
1431
1517
  return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1432
1518
  }
1433
- static buildInitAppEnvVars(params) {
1519
+ static buildInitAppEnvVars(params, options = {}) {
1434
1520
  return buildInitAppEnvVarsFromConfig({
1435
1521
  lang: String(params.appResults.lang ?? ''),
1436
1522
  rootUsername: String(params.rootResults.rootUsername ?? ''),
1437
1523
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1438
1524
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1439
1525
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1440
- });
1526
+ portalType: String(params.appResults.portalType ?? ''),
1527
+ portalName: String(params.appResults.portalName ?? ''),
1528
+ portalTemplate: String(params.appResults.portalTemplate ?? ''),
1529
+ }, options);
1441
1530
  }
1442
1531
  static shouldPublishBuiltinDbPort(source) {
1443
1532
  return String(source ?? '').trim() !== 'docker';
@@ -2228,9 +2317,13 @@ export default class Install extends Command {
2228
2317
  }
2229
2318
  async saveInstalledEnv(params) {
2230
2319
  const defaultApiHost = await resolveDefaultApiHost();
2231
- await upsertEnv(params.envName, Install.buildSavedEnvConfig(params, { defaultApiHost }), {
2320
+ const savedEnvConfig = Install.buildSavedEnvConfig(params, { defaultApiHost });
2321
+ await upsertEnv(params.envName, savedEnvConfig, {
2232
2322
  scope: resolveDefaultConfigScope(),
2233
2323
  });
2324
+ if (params.ensureEnvFileDefaults !== false) {
2325
+ await ensureManagedEnvFileDefaults(params.envName, savedEnvConfig);
2326
+ }
2234
2327
  await setCurrentEnv(params.envName, { scope: resolveDefaultConfigScope() });
2235
2328
  }
2236
2329
  async syncInstalledEnvConnection(params) {
@@ -2309,6 +2402,9 @@ export default class Install extends Command {
2309
2402
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2310
2403
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2311
2404
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2405
+ const portalType = Install.toOptionalPromptString(params.appResults.portalType);
2406
+ const portalName = Install.toOptionalPromptString(params.appResults.portalName);
2407
+ const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2312
2408
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2313
2409
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2314
2410
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
@@ -2343,6 +2439,9 @@ export default class Install extends Command {
2343
2439
  ...(appPublicPath ? { appPublicPath } : {}),
2344
2440
  ...(envFile ? { envFile } : {}),
2345
2441
  lang: params.appResults.lang,
2442
+ portalType,
2443
+ portalName,
2444
+ portalTemplate,
2346
2445
  appKey: params.appResults.appKey,
2347
2446
  timezone: params.appResults.timeZone,
2348
2447
  builtinDb: params.dbResults.builtinDb,
@@ -2394,6 +2493,7 @@ export default class Install extends Command {
2394
2493
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2395
2494
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2396
2495
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2496
+ 'portal-template': parsed['portal-template'] ?? Install.toOptionalPromptString(appPreset.portalTemplate),
2397
2497
  },
2398
2498
  }),
2399
2499
  values: appPreset,
@@ -2565,6 +2665,7 @@ export default class Install extends Command {
2565
2665
  dbResults,
2566
2666
  rootResults,
2567
2667
  envAddResults,
2668
+ ensureEnvFileDefaults: false,
2568
2669
  });
2569
2670
  if (!parsed['skip-save-env-log']) {
2570
2671
  printInfo(`Saved env config for "${envName}".`);
@@ -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,105 @@
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 AI 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: 'AI 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 scope = resolveDefaultConfigScope();
80
+ const env = await getEnv(flags.env, { scope });
81
+ if (!env) {
82
+ this.error(flags.env
83
+ ? portalCreateText('errors.envNotConfigured', { envName: flags.env }, `Env "${flags.env}" is not configured. Run \`nb env add ${flags.env} --api-base-url <url>\` first.`)
84
+ : portalCreateText('errors.noEnvConfigured', undefined, 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
85
+ }
86
+ const result = await createPortalWorkspace({
87
+ portal: args.portal,
88
+ title: flags.title,
89
+ template: flags.template,
90
+ env,
91
+ envName: flags.env,
92
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
93
+ force: flags.force,
94
+ sourceStorage: flags['source-storage'],
95
+ gitRepo: flags['git-repo'],
96
+ gitBranch: flags['git-branch'],
97
+ gitPath: flags['git-path'],
98
+ onSkipInstall: (message) => printInfo(message),
99
+ });
100
+ printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}.`));
101
+ printInfo(portalCreateText('messages.app', { app: result.app }, `App: ${result.app}`));
102
+ printInfo(portalCreateText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
103
+ printInfo(portalCreateText('messages.sourceStorage', { sourceStorage: result.sourceStorage }, `Source storage: ${result.sourceStorage}`));
104
+ }
105
+ }
@@ -0,0 +1,81 @@
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 { findPortalListItem, formatPortalInfo } from '../../lib/portal-info.js';
15
+ import { deployPortalWorkspace } from '../../lib/portal-deploy.js';
16
+ import { listPortalWorkspaces } from '../../lib/portal-list.js';
17
+ import { printSuccess } from '../../lib/ui.js';
18
+ const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
19
+ export default class PortalDeploy extends Command {
20
+ static summary = 'Build and deploy a portal';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %> customer',
23
+ '<%= config.bin %> <%= command.id %> customer --env dev --yes',
24
+ ];
25
+ static args = {
26
+ portal: Args.string({
27
+ required: true,
28
+ description: 'Portal name',
29
+ }),
30
+ };
31
+ static flags = {
32
+ env: Flags.string({
33
+ char: 'e',
34
+ description: 'CLI env name; omitted uses the current env',
35
+ }),
36
+ yes: Flags.boolean({
37
+ char: 'y',
38
+ description: 'Confirm using --env when it targets a different env than the current env',
39
+ default: false,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(PortalDeploy);
44
+ const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
45
+ const confirmed = await ensureCrossEnvConfirmed({
46
+ command: this,
47
+ requestedEnv,
48
+ yes: flags.yes,
49
+ });
50
+ if (!confirmed) {
51
+ return;
52
+ }
53
+ const scope = resolveDefaultConfigScope();
54
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
55
+ const env = await getEnv(envName, { scope });
56
+ if (!env) {
57
+ this.error(portalDeployText(requestedEnv ? 'errors.envNotConfigured' : 'errors.noEnvConfigured', { envName }, requestedEnv
58
+ ? `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`
59
+ : 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
60
+ }
61
+ const result = await deployPortalWorkspace({
62
+ portal: args.portal,
63
+ env,
64
+ envName,
65
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
66
+ });
67
+ printSuccess(portalDeployText('messages.deployed', { portal: result.portal }, `Portal "${result.portal}" deployed.`));
68
+ const info = await listPortalWorkspaces({
69
+ env,
70
+ envName,
71
+ cliVersion: String(this.config.pjson.version ?? '').trim(),
72
+ });
73
+ const portal = findPortalListItem(info.items, result.portal);
74
+ if (!portal) {
75
+ this.error(translateCli(`commands.portalInfo.errors.notFound`, { portal: result.portal }, {
76
+ fallback: `Portal "${result.portal}" was not found.`,
77
+ }));
78
+ }
79
+ this.log(formatPortalInfo(portal));
80
+ }
81
+ }