@nocobase/cli 2.3.0-beta.3 → 3.0.0-alpha.11

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 (56) hide show
  1. package/dist/commands/api/swagger/get.js +96 -0
  2. package/dist/commands/api/swagger/index.js +20 -0
  3. package/dist/commands/api/swagger/list.js +92 -0
  4. package/dist/commands/config/set.js +1 -0
  5. package/dist/commands/env/add.js +6 -0
  6. package/dist/commands/env/update.js +13 -0
  7. package/dist/commands/init.js +51 -5
  8. package/dist/commands/install.js +60 -3
  9. package/dist/commands/portal/config.js +99 -0
  10. package/dist/commands/portal/create.js +96 -0
  11. package/dist/commands/portal/deploy.js +81 -0
  12. package/dist/commands/portal/destroy.js +126 -0
  13. package/dist/commands/portal/dev.js +73 -0
  14. package/dist/commands/portal/index.js +20 -0
  15. package/dist/commands/portal/info.js +82 -0
  16. package/dist/commands/portal/list.js +98 -0
  17. package/dist/commands/portal/pull.js +113 -0
  18. package/dist/commands/portal/push.js +79 -0
  19. package/dist/commands/source/dev.js +1 -1
  20. package/dist/lib/api-client.js +35 -9
  21. package/dist/lib/app-client-entry-mode.js +28 -0
  22. package/dist/lib/app-managed-resources.js +2 -0
  23. package/dist/lib/auth-store.js +55 -2
  24. package/dist/lib/bootstrap.js +3 -1
  25. package/dist/lib/cli-config.js +20 -1
  26. package/dist/lib/env-auth.js +2 -36
  27. package/dist/lib/env-command-config.js +1 -0
  28. package/dist/lib/env-config.js +11 -0
  29. package/dist/lib/env-portal-config.js +30 -0
  30. package/dist/lib/env-proxy.js +154 -7
  31. package/dist/lib/managed-env-file.js +119 -9
  32. package/dist/lib/managed-init-env.js +4 -1
  33. package/dist/lib/portal-build-html.js +27 -0
  34. package/dist/lib/portal-command-env.js +31 -0
  35. package/dist/lib/portal-config.js +119 -0
  36. package/dist/lib/portal-configure.js +110 -0
  37. package/dist/lib/portal-create.js +515 -0
  38. package/dist/lib/portal-deploy.js +266 -0
  39. package/dist/lib/portal-destroy.js +114 -0
  40. package/dist/lib/portal-dev.js +78 -0
  41. package/dist/lib/portal-env-files.js +54 -0
  42. package/dist/lib/portal-info.js +28 -0
  43. package/dist/lib/portal-list.js +205 -0
  44. package/dist/lib/portal-path-safety.js +76 -0
  45. package/dist/lib/portal-source.js +692 -0
  46. package/dist/lib/prompt-catalog-core.js +2 -2
  47. package/dist/lib/prompt-catalog-terminal.js +4 -5
  48. package/dist/lib/prompt-web-ui.js +12 -6
  49. package/dist/lib/proxy-caddy.js +2 -0
  50. package/dist/lib/proxy-nginx.js +1 -0
  51. package/dist/lib/run-npm.js +85 -20
  52. package/dist/lib/swagger-command.js +52 -0
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +245 -1
  55. package/dist/locale/zh-CN.json +245 -1
  56. package/package.json +5 -2
@@ -0,0 +1,96 @@
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 { promises as fs } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { Command, Flags } from '@oclif/core';
12
+ import { translateCli } from '../../../lib/cli-locale.js';
13
+ import { executeSwaggerRequest, swaggerRequestFlags } from '../../../lib/swagger-command.js';
14
+ import { renderTable } from '../../../lib/ui.js';
15
+ const swaggerText = (key, values, fallback) => translateCli(`commands.swagger.${key}`, values, { fallback });
16
+ function isRecord(value) {
17
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
18
+ }
19
+ function unwrapData(value) {
20
+ if (!isRecord(value) || typeof value.openapi === 'string') {
21
+ return value;
22
+ }
23
+ return Object.prototype.hasOwnProperty.call(value, 'data') ? value.data : value;
24
+ }
25
+ function normalizeDocument(value) {
26
+ const document = unwrapData(value);
27
+ if (!isRecord(document) ||
28
+ typeof document.openapi !== 'string' ||
29
+ !isRecord(document.info) ||
30
+ !isRecord(document.paths)) {
31
+ return undefined;
32
+ }
33
+ return document;
34
+ }
35
+ export default class SwaggerGet extends Command {
36
+ static summary = 'Get a NocoBase OpenAPI document';
37
+ static examples = [
38
+ '<%= config.bin %> <%= command.id %> --json',
39
+ '<%= config.bin %> <%= command.id %> --namespace collections/orders --json',
40
+ '<%= config.bin %> <%= command.id %> --namespace plugins/ai --output ./openapi/ai.json',
41
+ ];
42
+ static flags = {
43
+ ...swaggerRequestFlags,
44
+ namespace: Flags.string({
45
+ aliases: ['ns'],
46
+ description: 'Document namespace, such as core, plugins/ai, or collections/orders',
47
+ }),
48
+ output: Flags.string({
49
+ char: 'o',
50
+ description: 'Write the OpenAPI document to a file',
51
+ }),
52
+ 'json-output': Flags.boolean({
53
+ char: 'j',
54
+ aliases: ['json'],
55
+ description: 'Print the complete OpenAPI document as JSON',
56
+ default: false,
57
+ }),
58
+ };
59
+ async run() {
60
+ const { flags } = await this.parse(SwaggerGet);
61
+ const namespace = flags.namespace?.trim() || undefined;
62
+ const response = await executeSwaggerRequest(this, flags, '/swagger:get', { ns: namespace });
63
+ if (!response) {
64
+ return;
65
+ }
66
+ if (!response.ok) {
67
+ const details = JSON.stringify(response.data, null, 2);
68
+ this.error(response.status === 404
69
+ ? swaggerText('errors.pluginDisabled', undefined, 'The API documentation plugin is not enabled. Enable it before requesting Swagger documents.')
70
+ : swaggerText('errors.requestFailed', { status: response.status, details }, `Swagger request failed with status ${response.status}\n${details}`));
71
+ }
72
+ const document = normalizeDocument(response.data);
73
+ if (!document) {
74
+ this.error(swaggerText('errors.invalidDocument', undefined, 'swagger:get returned an invalid OpenAPI document.'));
75
+ }
76
+ const json = `${JSON.stringify(document, null, 2)}\n`;
77
+ if (flags.output) {
78
+ const outputPath = path.resolve(flags.output);
79
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
80
+ await fs.writeFile(outputPath, json);
81
+ this.log(swaggerText('messages.saved', { output: outputPath }, `Saved Swagger document to ${outputPath}.`));
82
+ return;
83
+ }
84
+ if (flags['json-output']) {
85
+ this.log(json.trimEnd());
86
+ return;
87
+ }
88
+ this.log(renderTable([swaggerText('table.field', undefined, 'Field'), swaggerText('table.value', undefined, 'Value')], [
89
+ [swaggerText('fields.namespace', undefined, 'Namespace'), namespace ?? 'all'],
90
+ [swaggerText('fields.title', undefined, 'Title'), document.info.title ?? ''],
91
+ [swaggerText('fields.version', undefined, 'Version'), document.info.version ?? ''],
92
+ [swaggerText('fields.openapi', undefined, 'OpenAPI'), document.openapi],
93
+ [swaggerText('fields.paths', undefined, 'Paths'), String(Object.keys(document.paths).length)],
94
+ ]));
95
+ }
96
+ }
@@ -0,0 +1,20 @@
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 { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Swagger extends Command {
11
+ static summary = 'Inspect the current NocoBase OpenAPI documentation';
12
+ async run() {
13
+ await this.parse(Swagger);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'api:swagger',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,92 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { translateCli } from '../../../lib/cli-locale.js';
11
+ import { executeSwaggerRequest, swaggerRequestFlags } from '../../../lib/swagger-command.js';
12
+ import { renderTable } from '../../../lib/ui.js';
13
+ const swaggerText = (key, values, fallback) => translateCli(`commands.swagger.${key}`, values, { fallback });
14
+ function isRecord(value) {
15
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
16
+ }
17
+ function unwrapData(value) {
18
+ return isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'data') ? value.data : value;
19
+ }
20
+ function readNamespace(url) {
21
+ try {
22
+ return new URL(url, 'http://localhost').searchParams.get('ns') || 'all';
23
+ }
24
+ catch {
25
+ return 'all';
26
+ }
27
+ }
28
+ function normalizeDestinations(value) {
29
+ const data = unwrapData(value);
30
+ if (!Array.isArray(data)) {
31
+ return undefined;
32
+ }
33
+ const destinations = [];
34
+ for (const item of data) {
35
+ if (!isRecord(item) || typeof item.name !== 'string' || typeof item.url !== 'string') {
36
+ return undefined;
37
+ }
38
+ destinations.push({
39
+ name: item.name,
40
+ namespace: readNamespace(item.url),
41
+ url: item.url,
42
+ });
43
+ }
44
+ return destinations;
45
+ }
46
+ export default class SwaggerList extends Command {
47
+ static summary = 'List available NocoBase OpenAPI document namespaces';
48
+ static examples = [
49
+ '<%= config.bin %> <%= command.id %>',
50
+ '<%= config.bin %> <%= command.id %> --json',
51
+ '<%= config.bin %> <%= command.id %> --env dev --yes --json',
52
+ ];
53
+ static flags = {
54
+ ...swaggerRequestFlags,
55
+ 'json-output': Flags.boolean({
56
+ char: 'j',
57
+ aliases: ['json'],
58
+ description: 'Print namespaces as JSON',
59
+ default: false,
60
+ }),
61
+ };
62
+ async run() {
63
+ const { flags } = await this.parse(SwaggerList);
64
+ const response = await executeSwaggerRequest(this, flags, '/swagger:getUrls');
65
+ if (!response) {
66
+ return;
67
+ }
68
+ if (!response.ok) {
69
+ const details = JSON.stringify(response.data, null, 2);
70
+ this.error(response.status === 404
71
+ ? swaggerText('errors.pluginDisabled', undefined, 'The API documentation plugin is not enabled. Enable it before requesting Swagger documents.')
72
+ : swaggerText('errors.requestFailed', { status: response.status, details }, `Swagger request failed with status ${response.status}\n${details}`));
73
+ }
74
+ const destinations = normalizeDestinations(response.data);
75
+ if (!destinations) {
76
+ this.error(swaggerText('errors.invalidDestinations', undefined, 'swagger:getUrls returned an invalid destination list.'));
77
+ }
78
+ if (flags['json-output']) {
79
+ this.log(JSON.stringify(destinations, null, 2));
80
+ return;
81
+ }
82
+ if (!destinations.length) {
83
+ this.log(swaggerText('messages.empty', undefined, 'No Swagger document namespaces are available.'));
84
+ return;
85
+ }
86
+ this.log(renderTable([
87
+ swaggerText('table.name', undefined, 'Name'),
88
+ swaggerText('table.namespace', undefined, 'Namespace'),
89
+ swaggerText('table.url', undefined, 'URL'),
90
+ ], destinations.map((item) => [item.name, item.namespace, item.url])));
91
+ }
92
+ }
@@ -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',
@@ -11,6 +11,7 @@ import { setCurrentEnv, upsertEnv } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { ENV_BOOLEAN_CONFIG_FLAG_MAP, ENV_STRING_CONFIG_FLAG_MAP } from '../../lib/env-command-config.js';
13
13
  import { buildStoredEnvConfig } from '../../lib/env-config.js';
14
+ import { PUBLIC_APP_CLIENT_ENTRY_MODES } from '../../lib/app-client-entry-mode.js';
14
15
  import { runPromptCatalog, } from '../../lib/prompt-catalog.js';
15
16
  import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from '../../lib/cli-locale.js';
16
17
  import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
@@ -172,6 +173,11 @@ export default class EnvAdd extends Command {
172
173
  hidden: true,
173
174
  description: 'Docker env file saved with this env',
174
175
  }),
176
+ 'app-client-entry-mode': Flags.string({
177
+ hidden: true,
178
+ description: 'UI entry mode saved with this env',
179
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODES],
180
+ }),
175
181
  'app-port': Flags.string({
176
182
  hidden: true,
177
183
  description: 'Application HTTP port saved with this env',
@@ -16,6 +16,8 @@ import { appendDiagnosticLogPath } from '../../lib/cli-entry-error.js';
16
16
  import { getActiveCommandLogFile } from '../../lib/command-log.js';
17
17
  import { ENV_BOOLEAN_CONFIG_FLAG_MAP, ENV_STRING_CONFIG_FLAG_MAP } from '../../lib/env-command-config.js';
18
18
  import { buildStoredEnvConfig } from '../../lib/env-config.js';
19
+ import { PUBLIC_APP_CLIENT_ENTRY_MODES } from '../../lib/app-client-entry-mode.js';
20
+ import { upsertManagedEnvFileValues } from '../../lib/managed-env-file.js';
19
21
  import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
20
22
  import { failTask, printInfo, printVerbose, printWarningBlock, setVerboseMode, startTask, stopTask, succeedTask, } from '../../lib/ui.js';
21
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -35,6 +37,7 @@ const UPDATE_STRING_FLAGS = [
35
37
  'app-public-path',
36
38
  'cdn-base-url',
37
39
  'env-file',
40
+ 'app-client-entry-mode',
38
41
  'app-port',
39
42
  'app-key',
40
43
  'timezone',
@@ -68,6 +71,7 @@ const APP_RESTART_FIELDS = new Set([
68
71
  'storage-path',
69
72
  'app-public-path',
70
73
  'env-file',
74
+ 'app-client-entry-mode',
71
75
  'app-port',
72
76
  'app-key',
73
77
  'timezone',
@@ -255,6 +259,10 @@ export default class EnvUpdate extends Command {
255
259
  hidden: true,
256
260
  description: 'Saved Docker --env-file path for this env',
257
261
  }),
262
+ 'app-client-entry-mode': Flags.string({
263
+ description: 'Saved UI entry mode for this env',
264
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODES],
265
+ }),
258
266
  'app-port': Flags.string({
259
267
  description: 'Saved application HTTP port for this env',
260
268
  }),
@@ -423,6 +431,11 @@ export default class EnvUpdate extends Command {
423
431
  startTask(`Saving env config: ${envName}`);
424
432
  try {
425
433
  await replaceEnvConfig(envName, nextConfig, { scope: resolveDefaultConfigScope() });
434
+ if (providedFields.has('app-client-entry-mode') && nextConfig.appClientEntryMode) {
435
+ await upsertManagedEnvFileValues(envName, nextConfig, {
436
+ APP_CLIENT_ENTRY_MODE: nextConfig.appClientEntryMode,
437
+ });
438
+ }
426
439
  succeedTask(`Saved env config for "${envName}".`);
427
440
  }
428
441
  catch (error) {
@@ -13,6 +13,7 @@ import { existsSync } from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
15
15
  import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
+ import { defaultAppClientEntryModeForDownloadVersion, normalizePublicAppClientEntryMode, PUBLIC_APP_CLIENT_ENTRY_MODES, } from '../lib/app-client-entry-mode.js';
16
17
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
18
  import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
19
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
@@ -27,6 +28,7 @@ import { omitKeys, pickKeys } from "../lib/object-utils.js";
27
28
  import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
28
29
  import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
29
30
  import { persistHookScript } from '../lib/hook-script.js';
31
+ import { ensureManagedEnvFileDefaults } from '../lib/managed-env-file.js';
30
32
  import Download from "./download.js";
31
33
  import EnvAdd from "./env/add.js";
32
34
  import Install, { defaultDbPortForDialect } from "./install.js";
@@ -105,9 +107,23 @@ function resolveInitDownloadVersion(results) {
105
107
  }
106
108
  return preset;
107
109
  }
110
+ function resolveInitAppClientEntryMode(results, explicitValue) {
111
+ return (normalizePublicAppClientEntryMode(explicitValue) ??
112
+ normalizePublicAppClientEntryMode(results.appClientEntryMode) ??
113
+ defaultAppClientEntryModeForDownloadVersion(resolveInitDownloadVersion(results)));
114
+ }
108
115
  function initVersionPromptValue(version) {
109
116
  return version === 'latest' || version === 'beta' || version === 'alpha' ? version : 'other';
110
117
  }
118
+ export function defaultInitDownloadVersionForCliVersion(cliVersion) {
119
+ if (/-alpha(?:[.-]|$)/i.test(cliVersion)) {
120
+ return 'alpha';
121
+ }
122
+ if (/-beta(?:[.-]|$)/i.test(cliVersion)) {
123
+ return 'beta';
124
+ }
125
+ return 'latest';
126
+ }
111
127
  function yesInitialValue(def, fallback) {
112
128
  if ('yesInitialValue' in def && def.yesInitialValue !== undefined) {
113
129
  return String(def.yesInitialValue);
@@ -408,8 +424,15 @@ Prompt modes:
408
424
  installAccessToken: installConnectionAccessTokenPrompt,
409
425
  };
410
426
  buildPromptCatalog(flags, options) {
427
+ const downloadVersion = defaultInitDownloadVersionForCliVersion(String(this.config.pjson?.version ?? '').trim());
428
+ const versionPrompt = Init.prompts.version;
411
429
  const prompts = {
412
430
  ...Init.prompts,
431
+ version: {
432
+ ...versionPrompt,
433
+ initialValue: downloadVersion,
434
+ yesInitialValue: downloadVersion,
435
+ },
413
436
  installApiBaseUrl: createInstallConnectionApiBaseUrlPrompt(options.defaultApiHost),
414
437
  };
415
438
  if (flags['skip-auth']) {
@@ -473,6 +496,10 @@ Prompt modes:
473
496
  description: 'Skip installing NocoBase AI coding skills during init',
474
497
  default: false,
475
498
  }),
499
+ 'app-client-entry-mode': Flags.string({
500
+ description: 'UI entry mode for this app env: modern-only, modern-default, or legacy-default',
501
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODES],
502
+ }),
476
503
  'ui-host': Flags.string({
477
504
  description: 'Browser-accessible host for the --ui setup page URL (default: 127.0.0.1)',
478
505
  }),
@@ -607,6 +634,7 @@ Prompt modes:
607
634
  ? { setupMode: normalizeInitSetupMode(presetValues.hasNocobase) }
608
635
  : {}),
609
636
  },
637
+ yesInitialValues: {},
610
638
  values: presetValues,
611
639
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
612
640
  hooks: {
@@ -679,7 +707,8 @@ Prompt modes:
679
707
  }
680
708
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
681
709
  const out = {};
682
- if (!Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
710
+ const shouldResolveAppInitialValues = !Object.prototype.hasOwnProperty.call(presetValues, 'appPort');
711
+ if (shouldResolveAppInitialValues) {
683
712
  const appInitialValues = await Install.buildAppPromptInitialValues({
684
713
  envName: String(presetValues.appName ?? '').trim(),
685
714
  flags: {
@@ -690,7 +719,7 @@ Prompt modes:
690
719
  },
691
720
  warnOnPortFallback: false,
692
721
  });
693
- if (appInitialValues.appPort !== undefined) {
722
+ if (appInitialValues.appPort !== undefined && !Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
694
723
  out.appPort = appInitialValues.appPort;
695
724
  }
696
725
  }
@@ -871,6 +900,9 @@ Prompt modes:
871
900
  if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
872
901
  preset.appPublicPath = String(flags['app-public-path']).trim();
873
902
  }
903
+ if (flags['app-client-entry-mode'] !== undefined && String(flags['app-client-entry-mode']).trim() !== '') {
904
+ preset.appClientEntryMode = String(flags['app-client-entry-mode']).trim();
905
+ }
874
906
  if (flags['root-username'] !== undefined) {
875
907
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
876
908
  }
@@ -1008,6 +1040,7 @@ Prompt modes:
1008
1040
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
1009
1041
  const appPort = String(results.appPort ?? '').trim();
1010
1042
  const appPublicPath = String(results.appPublicPath ?? '').trim();
1043
+ const appClientEntryMode = resolveInitAppClientEntryMode(results, flags['app-client-entry-mode']);
1011
1044
  const source = String(results.source ?? '').trim();
1012
1045
  const version = resolveInitDownloadVersion(results);
1013
1046
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
@@ -1034,7 +1067,8 @@ Prompt modes:
1034
1067
  const dbSchema = String(results.dbSchema ?? '').trim();
1035
1068
  const dbTablePrefix = String(results.dbTablePrefix ?? '').trim();
1036
1069
  const apiBaseUrl = String(results.apiBaseUrl ?? '').trim();
1037
- const authType = String(results.authType ?? '').trim() || 'oauth';
1070
+ const authTypeInput = String(results.authType ?? '').trim();
1071
+ const authType = authTypeInput === 'basic' || authTypeInput === 'token' || authTypeInput === 'oauth' ? authTypeInput : 'oauth';
1038
1072
  const authUsername = authType === 'basic' ? String(results.username ?? results.rootUsername ?? '').trim() : '';
1039
1073
  const accessToken = String(results.accessToken ?? '');
1040
1074
  const skipDownload = results.skipDownload === true;
@@ -1055,7 +1089,7 @@ Prompt modes:
1055
1089
  : Boolean(results.builtinDb);
1056
1090
  results.appKey = appKey;
1057
1091
  results.timeZone = timeZone;
1058
- await upsertEnv(envName, {
1092
+ const savedEnvConfig = {
1059
1093
  schemaVersion: ENV_CONFIG_SCHEMA_VERSION,
1060
1094
  ...(source === 'docker'
1061
1095
  ? { kind: 'docker' }
@@ -1080,6 +1114,7 @@ Prompt modes:
1080
1114
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
1081
1115
  ...(appPort ? { appPort } : {}),
1082
1116
  ...(appPublicPath ? { appPublicPath } : {}),
1117
+ ...(appClientEntryMode ? { appClientEntryMode } : {}),
1083
1118
  ...(appKey ? { appKey } : {}),
1084
1119
  ...(timeZone ? { timezone: timeZone } : {}),
1085
1120
  ...(!skipDownload && results.devDependencies !== undefined
@@ -1100,7 +1135,11 @@ Prompt modes:
1100
1135
  ...(results.dbUnderscored !== undefined ? { dbUnderscored: Boolean(results.dbUnderscored) } : {}),
1101
1136
  setupState: 'prepared',
1102
1137
  ...(String(results.lang ?? '').trim() ? { lang: String(results.lang ?? '').trim() } : {}),
1103
- }, { scope: resolveDefaultConfigScope() });
1138
+ };
1139
+ await upsertEnv(envName, savedEnvConfig, { scope: resolveDefaultConfigScope() });
1140
+ if (source === 'docker' || appPath) {
1141
+ await ensureManagedEnvFileDefaults(envName, savedEnvConfig);
1142
+ }
1104
1143
  }
1105
1144
  buildEnvAddArgv(results) {
1106
1145
  const argv = [String(results.appName ?? DEFAULT_INIT_APP_NAME)];
@@ -1204,6 +1243,13 @@ Prompt modes:
1204
1243
  if (appPublicPath) {
1205
1244
  argv.push('--app-public-path', appPublicPath);
1206
1245
  }
1246
+ const appClientEntryMode = normalizePublicAppClientEntryMode(flags['app-client-entry-mode']) ||
1247
+ (results.setupMode === 'install-new' && results.hasNocobase === undefined
1248
+ ? resolveInitAppClientEntryMode(results)
1249
+ : undefined);
1250
+ if (appClientEntryMode) {
1251
+ argv.push('--app-client-entry-mode', appClientEntryMode);
1252
+ }
1207
1253
  if (flags.force) {
1208
1254
  argv.push('--force');
1209
1255
  }
@@ -25,10 +25,12 @@ import { commandOutput, commandSucceeds, ensureDockerDaemonRunning, run, runNoco
25
25
  import { printInfo, printStage, printVerbose, printWarning, setVerboseMode } from '../lib/ui.js';
26
26
  import { omitKeys, upperFirst } from "../lib/object-utils.js";
27
27
  import { clearEnvRootSetup, getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
28
+ import { defaultAppClientEntryModeForDownloadVersion, normalizePublicAppClientEntryMode, PUBLIC_APP_CLIENT_ENTRY_MODES, } from '../lib/app-client-entry-mode.js';
28
29
  import { buildStoredEnvConfig } from '../lib/env-config.js';
29
30
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
31
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
32
  import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
33
+ import { ensureManagedEnvFileDefaults } from '../lib/managed-env-file.js';
32
34
  import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
33
35
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
34
36
  import Download from './download.js';
@@ -53,6 +55,7 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
53
55
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
54
56
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
55
57
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
58
+ const DEFAULT_INSTALL_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
56
59
  function toOptionalPromptString(value) {
57
60
  const text = String(value ?? '').trim();
58
61
  return text || undefined;
@@ -404,6 +407,13 @@ export default class Install extends Command {
404
407
  'app-public-path': Flags.string({
405
408
  description: 'Public path for the local app, for example / or /console/',
406
409
  }),
410
+ 'app-client-entry-mode': Flags.string({
411
+ description: 'UI entry mode for this app env: modern-only, modern-default, or legacy-default',
412
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODES],
413
+ }),
414
+ 'portal-template': Flags.string({
415
+ description: 'Template npm package or local path for the default AI Portal "main"',
416
+ }),
407
417
  'root-username': Flags.string({
408
418
  description: 'Initial admin username for the installed app',
409
419
  required: false,
@@ -513,6 +523,14 @@ export default class Install extends Command {
513
523
  yesInitialValue: '/',
514
524
  validate: validateAppPublicPath,
515
525
  },
526
+ portalTemplate: {
527
+ type: 'text',
528
+ message: installText('prompts.portalTemplate.message'),
529
+ placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
530
+ initialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
531
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
532
+ required: true,
533
+ },
516
534
  };
517
535
  }
518
536
  static dbPrompts = {
@@ -772,6 +790,18 @@ export default class Install extends Command {
772
790
  preset.appPublicPath = v;
773
791
  }
774
792
  }
793
+ if (flags['app-client-entry-mode'] !== undefined) {
794
+ const v = normalizePublicAppClientEntryMode(flags['app-client-entry-mode']);
795
+ if (v) {
796
+ preset.appClientEntryMode = v;
797
+ }
798
+ }
799
+ if (flags['portal-template'] !== undefined) {
800
+ const v = String(flags['portal-template'] ?? '').trim();
801
+ if (v) {
802
+ preset.portalTemplate = v;
803
+ }
804
+ }
775
805
  if (flags['root-username'] !== undefined) {
776
806
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
777
807
  }
@@ -863,6 +893,8 @@ export default class Install extends Command {
863
893
  'appPort',
864
894
  'storagePath',
865
895
  'appPublicPath',
896
+ 'appClientEntryMode',
897
+ 'portalTemplate',
866
898
  ]);
867
899
  }
868
900
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -1112,6 +1144,7 @@ export default class Install extends Command {
1112
1144
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1113
1145
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1114
1146
  const lang = Install.toOptionalPromptString(config.lang);
1147
+ const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1115
1148
  const auth = config.auth;
1116
1149
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1117
1150
  const appPreset = {
@@ -1121,6 +1154,7 @@ export default class Install extends Command {
1121
1154
  ...(appPort ? { appPort } : {}),
1122
1155
  ...(storagePath ? { storagePath } : {}),
1123
1156
  ...(appPublicPath ? { appPublicPath } : {}),
1157
+ ...(portalTemplate ? { portalTemplate } : {}),
1124
1158
  ...(hookScript ? { hookScript } : {}),
1125
1159
  };
1126
1160
  const downloadPreset = {
@@ -1263,6 +1297,12 @@ export default class Install extends Command {
1263
1297
  warn: params.warnOnPortFallback ?? true,
1264
1298
  });
1265
1299
  }
1300
+ if (params.flags['portal-template'] === undefined) {
1301
+ const defaultPortalTemplate = Install.toOptionalPromptString(await getCliConfigValue('default-portal-template'));
1302
+ if (defaultPortalTemplate) {
1303
+ initialValues.portalTemplate = defaultPortalTemplate;
1304
+ }
1305
+ }
1266
1306
  return initialValues;
1267
1307
  }
1268
1308
  static shouldPublishBuiltinDbPortForValues(values) {
@@ -1430,14 +1470,15 @@ export default class Install extends Command {
1430
1470
  static buildDockerAppContainerName(envName, containerPrefix) {
1431
1471
  return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1432
1472
  }
1433
- static buildInitAppEnvVars(params) {
1473
+ static buildInitAppEnvVars(params, options = {}) {
1434
1474
  return buildInitAppEnvVarsFromConfig({
1435
1475
  lang: String(params.appResults.lang ?? ''),
1436
1476
  rootUsername: String(params.rootResults.rootUsername ?? ''),
1437
1477
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1438
1478
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1439
1479
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1440
- });
1480
+ portalTemplate: String(params.appResults.portalTemplate ?? ''),
1481
+ }, options);
1441
1482
  }
1442
1483
  static shouldPublishBuiltinDbPort(source) {
1443
1484
  return String(source ?? '').trim() !== 'docker';
@@ -1785,6 +1826,7 @@ export default class Install extends Command {
1785
1826
  const extractClientAssets = resolveExtractClientAssetsDefaultEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
1786
1827
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1787
1828
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
1829
+ const appClientEntryMode = Install.toOptionalPromptString(params.appResults.appClientEntryMode);
1788
1830
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1789
1831
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1790
1832
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
@@ -1812,6 +1854,7 @@ export default class Install extends Command {
1812
1854
  }
1813
1855
  args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:/app/nocobase/storage`);
1814
1856
  pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath);
1857
+ pushOptionalEnvArg(args, 'APP_CLIENT_ENTRY_MODE', appClientEntryMode);
1815
1858
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1816
1859
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1817
1860
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
@@ -2072,6 +2115,7 @@ export default class Install extends Command {
2072
2115
  }),
2073
2116
  };
2074
2117
  setOptionalEnvVar(env, 'APP_PUBLIC_PATH', Install.toOptionalPromptString(params.appResults.appPublicPath));
2118
+ setOptionalEnvVar(env, 'APP_CLIENT_ENTRY_MODE', Install.toOptionalPromptString(params.appResults.appClientEntryMode));
2075
2119
  setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
2076
2120
  setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
2077
2121
  setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
@@ -2228,9 +2272,13 @@ export default class Install extends Command {
2228
2272
  }
2229
2273
  async saveInstalledEnv(params) {
2230
2274
  const defaultApiHost = await resolveDefaultApiHost();
2231
- await upsertEnv(params.envName, Install.buildSavedEnvConfig(params, { defaultApiHost }), {
2275
+ const savedEnvConfig = Install.buildSavedEnvConfig(params, { defaultApiHost });
2276
+ await upsertEnv(params.envName, savedEnvConfig, {
2232
2277
  scope: resolveDefaultConfigScope(),
2233
2278
  });
2279
+ if (params.ensureEnvFileDefaults !== false) {
2280
+ await ensureManagedEnvFileDefaults(params.envName, savedEnvConfig);
2281
+ }
2234
2282
  await setCurrentEnv(params.envName, { scope: resolveDefaultConfigScope() });
2235
2283
  }
2236
2284
  async syncInstalledEnvConnection(params) {
@@ -2309,6 +2357,8 @@ export default class Install extends Command {
2309
2357
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2310
2358
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2311
2359
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2360
+ const appClientEntryMode = Install.toOptionalPromptString(params.appResults.appClientEntryMode);
2361
+ const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2312
2362
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2313
2363
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2314
2364
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
@@ -2341,8 +2391,10 @@ export default class Install extends Command {
2341
2391
  appPort,
2342
2392
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
2343
2393
  ...(appPublicPath ? { appPublicPath } : {}),
2394
+ ...(appClientEntryMode ? { appClientEntryMode } : {}),
2344
2395
  ...(envFile ? { envFile } : {}),
2345
2396
  lang: params.appResults.lang,
2397
+ portalTemplate,
2346
2398
  appKey: params.appResults.appKey,
2347
2399
  timezone: params.appResults.timeZone,
2348
2400
  builtinDb: params.dbResults.builtinDb,
@@ -2394,6 +2446,7 @@ export default class Install extends Command {
2394
2446
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2395
2447
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2396
2448
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2449
+ 'portal-template': parsed['portal-template'] ?? Install.toOptionalPromptString(appPreset.portalTemplate),
2397
2450
  },
2398
2451
  }),
2399
2452
  values: appPreset,
@@ -2412,6 +2465,9 @@ export default class Install extends Command {
2412
2465
  };
2413
2466
  downloadOpts.yes = yes;
2414
2467
  const downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2468
+ appResults.appClientEntryMode =
2469
+ normalizePublicAppClientEntryMode(appResults.appClientEntryMode) ??
2470
+ defaultAppClientEntryModeForDownloadVersion(downloadResultsValue(downloadResults, 'version'));
2415
2471
  if (parsed['skip-download']) {
2416
2472
  delete downloadResults.outputDir;
2417
2473
  delete downloadResults.replace;
@@ -2565,6 +2621,7 @@ export default class Install extends Command {
2565
2621
  dbResults,
2566
2622
  rootResults,
2567
2623
  envAddResults,
2624
+ ensureEnvFileDefaults: false,
2568
2625
  });
2569
2626
  if (!parsed['skip-save-env-log']) {
2570
2627
  printInfo(`Saved env config for "${envName}".`);