@nocobase/cli 3.0.0-alpha.5 → 3.0.0-alpha.7

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 (36) hide show
  1. package/dist/commands/env/add.js +6 -0
  2. package/dist/commands/env/update.js +13 -0
  3. package/dist/commands/init.js +49 -0
  4. package/dist/commands/install.js +18 -62
  5. package/dist/commands/portal/config.js +16 -5
  6. package/dist/commands/portal/create.js +13 -25
  7. package/dist/commands/portal/destroy.js +28 -6
  8. package/dist/commands/portal/list.js +5 -5
  9. package/dist/commands/portal/pull.js +28 -2
  10. package/dist/lib/api-client.js +35 -9
  11. package/dist/lib/app-client-entry-mode.js +28 -0
  12. package/dist/lib/app-managed-resources.js +2 -0
  13. package/dist/lib/auth-store.js +52 -1
  14. package/dist/lib/env-auth.js +2 -36
  15. package/dist/lib/env-command-config.js +1 -0
  16. package/dist/lib/env-config.js +10 -2
  17. package/dist/lib/env-portal-config.js +30 -0
  18. package/dist/lib/env-proxy.js +1 -1
  19. package/dist/lib/managed-env-file.js +57 -1
  20. package/dist/lib/managed-init-env.js +0 -2
  21. package/dist/lib/portal-config.js +0 -17
  22. package/dist/lib/portal-configure.js +46 -54
  23. package/dist/lib/portal-create.js +21 -5
  24. package/dist/lib/portal-deploy.js +5 -42
  25. package/dist/lib/portal-destroy.js +27 -9
  26. package/dist/lib/portal-dev.js +2 -3
  27. package/dist/lib/portal-info.js +2 -5
  28. package/dist/lib/portal-list.js +16 -23
  29. package/dist/lib/portal-path-safety.js +76 -0
  30. package/dist/lib/portal-source.js +60 -43
  31. package/dist/lib/prompt-catalog-core.js +2 -2
  32. package/dist/lib/prompt-catalog-terminal.js +4 -5
  33. package/dist/lib/prompt-web-ui.js +8 -1
  34. package/dist/locale/en-US.json +44 -16
  35. package/dist/locale/zh-CN.json +44 -16
  36. package/package.json +2 -2
@@ -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';
@@ -47,6 +48,23 @@ const INIT_ENV_ADD_FLAG_NAMES = [
47
48
  'skip-auth',
48
49
  ];
49
50
  const initText = (key, values) => localeText(`commands.init.${key}`, values);
51
+ const PUBLIC_APP_CLIENT_ENTRY_MODE_OPTIONS = [
52
+ {
53
+ value: 'modern-only',
54
+ label: initText('prompts.appClientEntryMode.modernOnlyLabel'),
55
+ hint: initText('prompts.appClientEntryMode.modernOnlyHint'),
56
+ },
57
+ {
58
+ value: 'modern-default',
59
+ label: initText('prompts.appClientEntryMode.modernDefaultLabel'),
60
+ hint: initText('prompts.appClientEntryMode.modernDefaultHint'),
61
+ },
62
+ {
63
+ value: 'legacy-default',
64
+ label: initText('prompts.appClientEntryMode.legacyDefaultLabel'),
65
+ hint: initText('prompts.appClientEntryMode.legacyDefaultHint'),
66
+ },
67
+ ];
50
68
  function withExtraHidden(def, extraHidden) {
51
69
  if (def.type === 'run') {
52
70
  return def;
@@ -106,6 +124,11 @@ function resolveInitDownloadVersion(results) {
106
124
  }
107
125
  return preset;
108
126
  }
127
+ function resolveInitAppClientEntryMode(results, explicitValue) {
128
+ return (normalizePublicAppClientEntryMode(explicitValue) ??
129
+ normalizePublicAppClientEntryMode(results.appClientEntryMode) ??
130
+ defaultAppClientEntryModeForDownloadVersion(resolveInitDownloadVersion(results)));
131
+ }
109
132
  function initVersionPromptValue(version) {
110
133
  return version === 'latest' || version === 'beta' || version === 'alpha' ? version : 'other';
111
134
  }
@@ -366,6 +389,15 @@ Prompt modes:
366
389
  source: installLikeOnly(Download.prompts.source),
367
390
  version: installLikeOnly(Download.prompts.version),
368
391
  otherVersion: installLikeOnly(Download.prompts.otherVersion),
392
+ appClientEntryMode: installLikeOnly({
393
+ type: 'select',
394
+ message: initText('prompts.appClientEntryMode.message'),
395
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODE_OPTIONS],
396
+ variant: 'radio',
397
+ hidden: (values) => resolveInitDownloadVersion(values) === 'latest',
398
+ initialValue: (values) => defaultAppClientEntryModeForDownloadVersion(values.version),
399
+ required: true,
400
+ }),
369
401
  dockerRegistry: installLikeOnly(Download.prompts.dockerRegistry),
370
402
  dockerPlatform: installLikeOnly(Download.prompts.dockerPlatform),
371
403
  dockerSave: installLikeDownloadExecutionOnly(Download.prompts.dockerSave),
@@ -490,6 +522,10 @@ Prompt modes:
490
522
  description: 'Skip installing NocoBase AI coding skills during init',
491
523
  default: false,
492
524
  }),
525
+ 'app-client-entry-mode': Flags.string({
526
+ description: 'UI entry mode for this app env: modern-only, modern-default, or legacy-default',
527
+ options: [...PUBLIC_APP_CLIENT_ENTRY_MODES],
528
+ }),
493
529
  'ui-host': Flags.string({
494
530
  description: 'Browser-accessible host for the --ui setup page URL (default: 127.0.0.1)',
495
531
  }),
@@ -778,6 +814,7 @@ Prompt modes:
778
814
  source: c.source,
779
815
  version: c.version,
780
816
  otherVersion: c.otherVersion,
817
+ appClientEntryMode: c.appClientEntryMode,
781
818
  dockerRegistry: c.dockerRegistry,
782
819
  dockerPlatform: c.dockerPlatform,
783
820
  dockerSave: c.dockerSave,
@@ -890,6 +927,9 @@ Prompt modes:
890
927
  if (flags['app-public-path'] !== undefined && String(flags['app-public-path']).trim() !== '') {
891
928
  preset.appPublicPath = String(flags['app-public-path']).trim();
892
929
  }
930
+ if (flags['app-client-entry-mode'] !== undefined && String(flags['app-client-entry-mode']).trim() !== '') {
931
+ preset.appClientEntryMode = String(flags['app-client-entry-mode']).trim();
932
+ }
893
933
  if (flags['root-username'] !== undefined) {
894
934
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
895
935
  }
@@ -1027,6 +1067,7 @@ Prompt modes:
1027
1067
  const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
1028
1068
  const appPort = String(results.appPort ?? '').trim();
1029
1069
  const appPublicPath = String(results.appPublicPath ?? '').trim();
1070
+ const appClientEntryMode = resolveInitAppClientEntryMode(results, flags['app-client-entry-mode']);
1030
1071
  const source = String(results.source ?? '').trim();
1031
1072
  const version = resolveInitDownloadVersion(results);
1032
1073
  const dockerRegistry = String(results.dockerRegistry ?? '').trim();
@@ -1100,6 +1141,7 @@ Prompt modes:
1100
1141
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
1101
1142
  ...(appPort ? { appPort } : {}),
1102
1143
  ...(appPublicPath ? { appPublicPath } : {}),
1144
+ ...(appClientEntryMode ? { appClientEntryMode } : {}),
1103
1145
  ...(appKey ? { appKey } : {}),
1104
1146
  ...(timeZone ? { timezone: timeZone } : {}),
1105
1147
  ...(!skipDownload && results.devDependencies !== undefined
@@ -1228,6 +1270,13 @@ Prompt modes:
1228
1270
  if (appPublicPath) {
1229
1271
  argv.push('--app-public-path', appPublicPath);
1230
1272
  }
1273
+ const appClientEntryMode = normalizePublicAppClientEntryMode(flags['app-client-entry-mode']) ||
1274
+ (results.setupMode === 'install-new' && results.hasNocobase === undefined
1275
+ ? resolveInitAppClientEntryMode(results)
1276
+ : undefined);
1277
+ if (appClientEntryMode) {
1278
+ argv.push('--app-client-entry-mode', appClientEntryMode);
1279
+ }
1231
1280
  if (flags.force) {
1232
1281
  argv.push('--force');
1233
1282
  }
@@ -25,6 +25,7 @@ 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';
@@ -54,10 +55,7 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
54
55
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
55
56
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
56
57
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
57
- const DEFAULT_INSTALL_PORTAL_TYPE = 'ai';
58
- const DEFAULT_INSTALL_PORTAL_NAME = 'main';
59
58
  const DEFAULT_INSTALL_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
60
- const INSTALL_PORTAL_TYPES = ['no-code', 'ai'];
61
59
  function toOptionalPromptString(value) {
62
60
  const text = String(value ?? '').trim();
63
61
  return text || undefined;
@@ -185,9 +183,6 @@ function defaultBuiltinDbImageForDialect(value, options) {
185
183
  function defaultDbDatabaseForDialect(value) {
186
184
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
187
185
  }
188
- function isAiMode(values) {
189
- return String(values.portalType ?? DEFAULT_INSTALL_PORTAL_TYPE).trim() === 'ai';
190
- }
191
186
  function supportsDbSchemaPrompt(value) {
192
187
  const dialect = String(value ?? '').trim();
193
188
  return dialect === 'postgres' || dialect === 'kingbase';
@@ -412,15 +407,12 @@ export default class Install extends Command {
412
407
  'app-public-path': Flags.string({
413
408
  description: 'Public path for the local app, for example / or /console/',
414
409
  }),
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',
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],
421
413
  }),
422
414
  'portal-template': Flags.string({
423
- description: 'Initial portal template npm package or local path when --portal-type ai is used',
415
+ description: 'Template npm package or local path for the default AI Portal "main"',
424
416
  }),
425
417
  'root-username': Flags.string({
426
418
  description: 'Initial admin username for the installed app',
@@ -531,39 +523,12 @@ export default class Install extends Command {
531
523
  yesInitialValue: '/',
532
524
  validate: validateAppPublicPath,
533
525
  },
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
526
  portalTemplate: {
562
527
  type: 'text',
563
528
  message: installText('prompts.portalTemplate.message'),
564
529
  placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
530
+ initialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
565
531
  yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
566
- hidden: (values) => !isAiMode(values),
567
532
  required: true,
568
533
  },
569
534
  };
@@ -825,16 +790,10 @@ export default class Install extends Command {
825
790
  preset.appPublicPath = v;
826
791
  }
827
792
  }
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();
793
+ if (flags['app-client-entry-mode'] !== undefined) {
794
+ const v = normalizePublicAppClientEntryMode(flags['app-client-entry-mode']);
836
795
  if (v) {
837
- preset.portalName = v;
796
+ preset.appClientEntryMode = v;
838
797
  }
839
798
  }
840
799
  if (flags['portal-template'] !== undefined) {
@@ -934,8 +893,7 @@ export default class Install extends Command {
934
893
  'appPort',
935
894
  'storagePath',
936
895
  'appPublicPath',
937
- 'portalType',
938
- 'portalName',
896
+ 'appClientEntryMode',
939
897
  'portalTemplate',
940
898
  ]);
941
899
  }
@@ -1186,8 +1144,6 @@ export default class Install extends Command {
1186
1144
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1187
1145
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1188
1146
  const lang = Install.toOptionalPromptString(config.lang);
1189
- const portalType = Install.toOptionalPromptString(config.portalType);
1190
- const portalName = Install.toOptionalPromptString(config.portalName);
1191
1147
  const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1192
1148
  const auth = config.auth;
1193
1149
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
@@ -1198,8 +1154,6 @@ export default class Install extends Command {
1198
1154
  ...(appPort ? { appPort } : {}),
1199
1155
  ...(storagePath ? { storagePath } : {}),
1200
1156
  ...(appPublicPath ? { appPublicPath } : {}),
1201
- ...(portalType ? { portalType } : {}),
1202
- ...(portalName ? { portalName } : {}),
1203
1157
  ...(portalTemplate ? { portalTemplate } : {}),
1204
1158
  ...(hookScript ? { hookScript } : {}),
1205
1159
  };
@@ -1523,8 +1477,6 @@ export default class Install extends Command {
1523
1477
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1524
1478
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1525
1479
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1526
- portalType: String(params.appResults.portalType ?? ''),
1527
- portalName: String(params.appResults.portalName ?? ''),
1528
1480
  portalTemplate: String(params.appResults.portalTemplate ?? ''),
1529
1481
  }, options);
1530
1482
  }
@@ -1874,6 +1826,7 @@ export default class Install extends Command {
1874
1826
  const extractClientAssets = resolveExtractClientAssetsDefaultEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
1875
1827
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1876
1828
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
1829
+ const appClientEntryMode = Install.toOptionalPromptString(params.appResults.appClientEntryMode);
1877
1830
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1878
1831
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1879
1832
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
@@ -1901,6 +1854,7 @@ export default class Install extends Command {
1901
1854
  }
1902
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`);
1903
1856
  pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath);
1857
+ pushOptionalEnvArg(args, 'APP_CLIENT_ENTRY_MODE', appClientEntryMode);
1904
1858
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1905
1859
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1906
1860
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
@@ -2161,6 +2115,7 @@ export default class Install extends Command {
2161
2115
  }),
2162
2116
  };
2163
2117
  setOptionalEnvVar(env, 'APP_PUBLIC_PATH', Install.toOptionalPromptString(params.appResults.appPublicPath));
2118
+ setOptionalEnvVar(env, 'APP_CLIENT_ENTRY_MODE', Install.toOptionalPromptString(params.appResults.appClientEntryMode));
2164
2119
  setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
2165
2120
  setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
2166
2121
  setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
@@ -2402,8 +2357,7 @@ export default class Install extends Command {
2402
2357
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2403
2358
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2404
2359
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2405
- const portalType = Install.toOptionalPromptString(params.appResults.portalType);
2406
- const portalName = Install.toOptionalPromptString(params.appResults.portalName);
2360
+ const appClientEntryMode = Install.toOptionalPromptString(params.appResults.appClientEntryMode);
2407
2361
  const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2408
2362
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2409
2363
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
@@ -2437,10 +2391,9 @@ export default class Install extends Command {
2437
2391
  appPort,
2438
2392
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
2439
2393
  ...(appPublicPath ? { appPublicPath } : {}),
2394
+ ...(appClientEntryMode ? { appClientEntryMode } : {}),
2440
2395
  ...(envFile ? { envFile } : {}),
2441
2396
  lang: params.appResults.lang,
2442
- portalType,
2443
- portalName,
2444
2397
  portalTemplate,
2445
2398
  appKey: params.appResults.appKey,
2446
2399
  timezone: params.appResults.timeZone,
@@ -2512,6 +2465,9 @@ export default class Install extends Command {
2512
2465
  };
2513
2466
  downloadOpts.yes = yes;
2514
2467
  const downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2468
+ appResults.appClientEntryMode =
2469
+ normalizePublicAppClientEntryMode(appResults.appClientEntryMode) ??
2470
+ defaultAppClientEntryModeForDownloadVersion(downloadResultsValue(downloadResults, 'version'));
2515
2471
  if (parsed['skip-download']) {
2516
2472
  delete downloadResults.outputDir;
2517
2473
  delete downloadResults.replace;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, setEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
@@ -17,6 +17,7 @@ const portalConfigureText = (key, values, fallback) => translateCli(`commands.po
17
17
  export default class PortalConfig extends Command {
18
18
  static summary = 'Update portal source configuration';
19
19
  static examples = [
20
+ '<%= config.bin %> <%= command.id %> customer --path ./portals/customer',
20
21
  '<%= config.bin %> <%= command.id %> customer --source-storage nocobase',
21
22
  '<%= config.bin %> <%= command.id %> customer --source-storage git --git-repo git@github.com:nocobase/customer-portal.git',
22
23
  '<%= config.bin %> <%= command.id %> customer --git-branch main --git-path portals/customer',
@@ -41,6 +42,9 @@ export default class PortalConfig extends Command {
41
42
  description: 'Where portal source code is managed',
42
43
  options: ['nocobase', 'git'],
43
44
  }),
45
+ path: Flags.string({
46
+ description: 'Portal development workspace directory',
47
+ }),
44
48
  'git-repo': Flags.string({
45
49
  description: 'Git repository URL used when --source-storage=git',
46
50
  }),
@@ -79,10 +83,17 @@ export default class PortalConfig extends Command {
79
83
  gitRepo: flags['git-repo'],
80
84
  gitBranch: flags['git-branch'],
81
85
  gitPath: flags['git-path'],
86
+ sourcePath: flags.path,
82
87
  });
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'));
88
+ if (flags.path) {
89
+ await setEnvPortalPath(envName, result.portal, result.portalDir, { scope });
90
+ }
91
+ printSuccess(portalConfigureText('messages.updated', { portal: result.portal }, `Portal "${result.portal}" configuration updated.`));
92
+ if (result.pathUpdated) {
93
+ printInfo(portalConfigureText('messages.pathUpdated', { portalDir: result.portalDir }, `Development path: ${result.portalDir}`));
94
+ }
95
+ if (result.config) {
96
+ printInfo(portalConfigureText('messages.remoteSynced', undefined, 'Remote portal record: synced'));
97
+ }
87
98
  }
88
99
  }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, setEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
@@ -19,9 +19,9 @@ export default class PortalCreate extends Command {
19
19
  static summary = 'Create a local AI portal from a template';
20
20
  static examples = [
21
21
  '<%= config.bin %> <%= command.id %> customer',
22
+ '<%= config.bin %> <%= command.id %> customer --path ./portals/customer',
22
23
  '<%= config.bin %> <%= command.id %> customer --template @nocobase/portal-template-default',
23
24
  '<%= 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
25
  ];
26
26
  static args = {
27
27
  portal: Args.string({
@@ -46,24 +46,13 @@ export default class PortalCreate extends Command {
46
46
  title: Flags.string({
47
47
  description: 'Portal display title; defaults to a title generated from the portal slug',
48
48
  }),
49
+ path: Flags.string({
50
+ description: 'Portal workspace directory; defaults to ./<portal>',
51
+ }),
49
52
  force: Flags.boolean({
50
53
  description: 'Delete the existing portal and recreate it',
51
54
  default: false,
52
55
  }),
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
56
  };
68
57
  async run() {
69
58
  const { args, flags } = await this.parse(PortalCreate);
@@ -77,10 +66,11 @@ export default class PortalCreate extends Command {
77
66
  return;
78
67
  }
79
68
  const scope = resolveDefaultConfigScope();
80
- const env = await getEnv(flags.env, { scope });
69
+ const envName = requestedEnv ?? (await getCurrentEnvName({ scope }));
70
+ const env = await getEnv(envName, { scope });
81
71
  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.`)
72
+ this.error(requestedEnv
73
+ ? portalCreateText('errors.envNotConfigured', { envName }, `Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`)
84
74
  : portalCreateText('errors.noEnvConfigured', undefined, 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.'));
85
75
  }
86
76
  const result = await createPortalWorkspace({
@@ -88,16 +78,14 @@ export default class PortalCreate extends Command {
88
78
  title: flags.title,
89
79
  template: flags.template,
90
80
  env,
91
- envName: flags.env,
81
+ envName,
92
82
  cliVersion: String(this.config.pjson.version ?? '').trim(),
93
83
  force: flags.force,
94
- sourceStorage: flags['source-storage'],
95
- gitRepo: flags['git-repo'],
96
- gitBranch: flags['git-branch'],
97
- gitPath: flags['git-path'],
84
+ sourcePath: flags.path,
98
85
  onSkipInstall: (message) => printInfo(message),
99
86
  });
100
- printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}.`));
87
+ await setEnvPortalPath(envName, result.portal, result.portalDir, { scope });
88
+ printSuccess(portalCreateText('messages.created', { portal: result.portal, portalDir: result.portalDir }, `Portal "${result.portal}" created at ${result.portalDir}`));
101
89
  printInfo(portalCreateText('messages.app', { app: result.app }, `App: ${result.app}`));
102
90
  printInfo(portalCreateText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
103
91
  printInfo(portalCreateText('messages.sourceStorage', { sourceStorage: result.sourceStorage }, `Source storage: ${result.sourceStorage}`));
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, unsetEnvPortalPath } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { translateCli } from '../../lib/cli-locale.js';
13
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
@@ -15,6 +15,9 @@ import { confirm } from "../../lib/inquirer.js";
15
15
  import { destroyPortalWorkspace } from '../../lib/portal-destroy.js';
16
16
  import { isInteractiveTerminal, printInfo, printSuccess } from '../../lib/ui.js';
17
17
  const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
18
+ function portalDestroyStatus(status) {
19
+ return portalDestroyText(`statuses.${status}`, undefined, status);
20
+ }
18
21
  async function ensureDestroyConfirmed(options) {
19
22
  if (options.yes) {
20
23
  return true;
@@ -24,7 +27,7 @@ async function ensureDestroyConfirmed(options) {
24
27
  }
25
28
  try {
26
29
  return Boolean(await confirm({
27
- message: portalDestroyText('prompts.confirm', { portal: options.portal }, `Destroy portal "${options.portal}" and delete its storage directory?`),
30
+ message: portalDestroyText('prompts.confirm', { portal: options.portal }, `Destroy portal "${options.portal}" and delete its deployment directory?`),
28
31
  default: false,
29
32
  }));
30
33
  }
@@ -33,9 +36,10 @@ async function ensureDestroyConfirmed(options) {
33
36
  }
34
37
  }
35
38
  export default class PortalDestroy extends Command {
36
- static summary = 'Destroy a portal record and local files';
39
+ static summary = 'Destroy a portal record and deployed files';
37
40
  static examples = [
38
41
  '<%= config.bin %> <%= command.id %> customer --yes',
42
+ '<%= config.bin %> <%= command.id %> customer --delete-dev-path --yes',
39
43
  '<%= config.bin %> <%= command.id %> customer --env dev --yes',
40
44
  '<%= config.bin %> <%= command.id %> customer --force --yes',
41
45
  ];
@@ -56,7 +60,12 @@ export default class PortalDestroy extends Command {
56
60
  default: false,
57
61
  }),
58
62
  force: Flags.boolean({
59
- description: 'Ignore missing portal records or local files',
63
+ description: 'Ignore missing portal records or deployment files',
64
+ default: false,
65
+ }),
66
+ 'delete-dev-path': Flags.boolean({
67
+ char: 'D',
68
+ description: 'Delete the portal development directory in addition to the deployed portal',
60
69
  default: false,
61
70
  }),
62
71
  };
@@ -93,12 +102,25 @@ export default class PortalDestroy extends Command {
93
102
  envName,
94
103
  cliVersion: String(this.config.pjson.version ?? '').trim(),
95
104
  force: flags.force,
105
+ deleteDevPath: flags['delete-dev-path'],
96
106
  });
107
+ await unsetEnvPortalPath(envName, result.portal, { scope });
97
108
  printSuccess(portalDestroyText('messages.destroyed', { portal: result.portal }, `Portal "${result.portal}" destroyed.`));
98
109
  printInfo(portalDestroyText('messages.mode', { mode: result.mode }, `Mode: ${result.mode}`));
99
110
  printInfo(portalDestroyText('messages.app', { app: result.app }, `App: ${result.app}`));
100
111
  printInfo(portalDestroyText('messages.base', { base: result.portalBase }, `Base: ${result.portalBase}`));
101
- printInfo(portalDestroyText('messages.record', { status: result.recordDeleted ? 'deleted' : 'missing' }, `Record: ${result.recordDeleted ? 'deleted' : 'missing'}`));
102
- printInfo(portalDestroyText('messages.workspace', { dir: result.portalDir, status: result.workspaceDeleted ? 'deleted' : 'missing' }, `Portal files: ${result.workspaceDeleted ? 'deleted' : 'missing'} (${result.portalDir})`));
112
+ printInfo(portalDestroyText('messages.record', { status: portalDestroyStatus(result.recordDeleted ? 'deleted' : 'missing') }, `Record: ${result.recordDeleted ? 'deleted' : 'missing'}`));
113
+ const deploymentPathStatus = result.deploymentPathDeleted ? 'deleted' : 'missing';
114
+ printInfo(portalDestroyText('messages.deploymentPath', { dir: result.deploymentPath, status: portalDestroyStatus(deploymentPathStatus) }, `Deployment path: ${deploymentPathStatus} (${result.deploymentPath})`));
115
+ let developmentPathStatus = 'missing';
116
+ if (result.developmentPath) {
117
+ developmentPathStatus = result.developmentPathDeleted ? 'deleted' : 'retained';
118
+ }
119
+ printInfo(portalDestroyText(result.developmentPath ? 'messages.developmentPath' : 'messages.developmentPathMissing', {
120
+ dir: result.developmentPath,
121
+ status: portalDestroyStatus(developmentPathStatus),
122
+ }, result.developmentPath
123
+ ? `Development path: ${developmentPathStatus} (${result.developmentPath})`
124
+ : 'Development path: missing'));
103
125
  }
104
126
  }
@@ -21,7 +21,7 @@ function formatBoolean(value) {
21
21
  return value ? 'yes' : 'no';
22
22
  }
23
23
  export default class PortalList extends Command {
24
- static summary = 'List portal records and local sync status';
24
+ static summary = 'List portal records and development paths';
25
25
  static examples = [
26
26
  '<%= config.bin %> <%= command.id %>',
27
27
  '<%= config.bin %> <%= command.id %> --env dev --yes',
@@ -82,17 +82,17 @@ export default class PortalList extends Command {
82
82
  portalListText('table.url', undefined, 'URL'),
83
83
  portalListText('table.portalType', undefined, 'Portal type'),
84
84
  portalListText('table.sourceStorage', undefined, 'Source storage'),
85
- portalListText('table.path', undefined, 'Local path'),
85
+ portalListText('table.path', undefined, 'Development path'),
86
86
  portalListText('table.enabled', undefined, 'Enabled'),
87
- portalListText('table.localSynced', undefined, 'Local synced'),
87
+ portalListText('table.default', undefined, 'Default'),
88
88
  ], outputItems.map((item) => [
89
89
  item.name,
90
90
  item.url,
91
91
  item.portalType,
92
92
  item.sourceStorage,
93
- item.localPath,
93
+ item.developmentPath,
94
94
  formatBoolean(item.enabled),
95
- formatBoolean(item.localSynced),
95
+ formatBoolean(item.isDefault),
96
96
  ])));
97
97
  }
98
98
  }