@nocobase/cli 2.1.0-alpha.45 → 2.1.0-alpha.47

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 (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -32
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +92 -20
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +143 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +11 -7
  27. package/dist/commands/config/delete.js +10 -2
  28. package/dist/commands/config/get.js +7 -1
  29. package/dist/commands/config/set.js +12 -2
  30. package/dist/commands/db/ps.js +5 -18
  31. package/dist/commands/env/add.js +24 -44
  32. package/dist/commands/env/auth.js +1 -1
  33. package/dist/commands/env/info.js +3 -2
  34. package/dist/commands/env/list.js +4 -5
  35. package/dist/commands/env/proxy/caddy.js +28 -0
  36. package/dist/commands/env/proxy/index.js +353 -0
  37. package/dist/commands/env/proxy/nginx.js +28 -0
  38. package/dist/commands/env/remove.js +112 -22
  39. package/dist/commands/env/shared.js +17 -9
  40. package/dist/commands/env/status.js +10 -7
  41. package/dist/commands/env/update.js +385 -21
  42. package/dist/commands/init.js +368 -98
  43. package/dist/commands/install.js +271 -104
  44. package/dist/commands/license/activate.js +63 -244
  45. package/dist/commands/license/plugins/shared.js +64 -13
  46. package/dist/commands/plugin/import.js +108 -0
  47. package/dist/commands/revision/create.js +89 -0
  48. package/dist/commands/self/update.js +58 -1
  49. package/dist/commands/source/dev.js +5 -5
  50. package/dist/commands/source/test.js +13 -14
  51. package/dist/lib/app-health.js +39 -32
  52. package/dist/lib/app-managed-resources.js +25 -9
  53. package/dist/lib/app-public-path.js +80 -0
  54. package/dist/lib/app-runtime.js +12 -3
  55. package/dist/lib/auth-store.js +128 -4
  56. package/dist/lib/backup.js +3 -3
  57. package/dist/lib/bootstrap.js +14 -8
  58. package/dist/lib/cli-config.js +229 -3
  59. package/dist/lib/cli-entry-error.js +52 -0
  60. package/dist/lib/command-log.js +284 -0
  61. package/dist/lib/docker-env-file.js +10 -2
  62. package/dist/lib/docker-log-stream.js +45 -0
  63. package/dist/lib/env-auth.js +25 -14
  64. package/dist/lib/env-command-config.js +45 -0
  65. package/dist/lib/env-config.js +7 -2
  66. package/dist/lib/env-guard.js +9 -10
  67. package/dist/lib/env-paths.js +101 -0
  68. package/dist/lib/env-proxy.js +1295 -0
  69. package/dist/lib/inquirer.js +4 -5
  70. package/dist/lib/managed-env-file.js +98 -0
  71. package/dist/lib/plugin-import.js +279 -0
  72. package/dist/lib/prompt-catalog-core.js +3 -2
  73. package/dist/lib/prompt-validators.js +14 -0
  74. package/dist/lib/prompt-web-ui.js +8 -1
  75. package/dist/lib/run-npm.js +56 -12
  76. package/dist/lib/skills-manager.js +3 -1
  77. package/dist/lib/source-publish.js +22 -2
  78. package/dist/lib/startup-update.js +74 -50
  79. package/dist/lib/ui.js +2 -1
  80. package/dist/locale/en-US.json +122 -22
  81. package/dist/locale/zh-CN.json +119 -19
  82. package/package.json +5 -7
  83. package/scripts/build.mjs +34 -0
  84. package/scripts/clean.mjs +9 -0
  85. package/tsconfig.json +19 -0
  86. package/LICENSE.txt +0 -107
  87. package/README.zh-CN.md +0 -355
@@ -11,23 +11,27 @@ import crypto from 'node:crypto';
11
11
  import { access, mkdir } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
13
  import { exit } from 'node:process';
14
+ import { appendAppPublicPath } from '../lib/app-public-path.js';
14
15
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
15
16
  import { applyCliLocale, localeText, resolveCliLocale, translateCli } from "../lib/cli-locale.js";
16
17
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
17
18
  import { defaultDockerContainerPrefix, defaultDockerNetworkName } from '../lib/app-runtime.js';
18
- import { resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
19
+ import { resolveDefaultApiHost, resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
19
20
  import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../lib/docker-image.js";
20
- import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
21
+ import { findAvailableTcpPort, validateAppPublicPath, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
21
22
  import { validateExternalDbConfig, validateMysqlLowerCaseTableNamesCompatibility } from "../lib/db-connection-check.js";
22
23
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
23
- import { commandOutput, commandSucceeds, run, runNocoBaseCommand } from '../lib/run-npm.js';
24
+ import { commandOutput, commandSucceeds, ensureDockerDaemonRunning, run, runNocoBaseCommand } from '../lib/run-npm.js';
24
25
  import { printInfo, printStage, printVerbose, printWarning, setVerboseMode } from '../lib/ui.js';
25
26
  import { omitKeys, upperFirst } from "../lib/object-utils.js";
26
- import { getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
27
+ import { clearEnvRootSetup, getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
27
28
  import { buildStoredEnvConfig } from '../lib/env-config.js';
28
29
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
+ import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
+ import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
29
32
  import Download, { defaultDockerRegistryForLang } from './download.js';
30
33
  import EnvAdd from "./env/add.js";
34
+ import { resolveAppUrlFromApiBaseUrl } from "./env/shared.js";
31
35
  const DEFAULT_INSTALL_ENV_NAME = 'local';
32
36
  const DEFAULT_INSTALL_LANG = 'en-US';
33
37
  const DEFAULT_INSTALL_APP_PORT = '13000';
@@ -58,6 +62,19 @@ const DEFAULT_INSTALL_ROOT_USERNAME = 'nocobase';
58
62
  const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
59
63
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
60
64
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
65
+ const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
66
+ function toOptionalPromptString(value) {
67
+ const text = String(value ?? '').trim();
68
+ return text || undefined;
69
+ }
70
+ function buildInstallApiBaseUrl(appResults, defaultApiHost = DEFAULT_INSTALL_API_HOST) {
71
+ const appPort = String(appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
72
+ const appPublicPath = String(appResults.appPublicPath ?? '').trim();
73
+ return `http://${defaultApiHost}:${appPort}${appendAppPublicPath(appPublicPath, 'api', { trailingSlash: false })}`;
74
+ }
75
+ function formatInstallDisplayUrl(apiBaseUrl) {
76
+ return resolveAppUrlFromApiBaseUrl(apiBaseUrl) || apiBaseUrl.replace(/\/api\/?$/, '');
77
+ }
61
78
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
62
79
  const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
63
80
  const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
@@ -206,13 +223,41 @@ async function validateExternalDbPromptField(value, values) {
206
223
  }
207
224
  return await validateMysqlLowerCaseTableNamesCompatibility(values);
208
225
  }
209
- function defaultInstallAppRootPath(envName) {
226
+ function defaultInstallAppPath(envName) {
210
227
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
211
- return `./${name}/source/`;
228
+ return `./${name}/`;
229
+ }
230
+ function defaultInstallAppRootPath(envName) {
231
+ return deriveConfiguredSourcePath(defaultInstallAppPath(envName));
212
232
  }
213
233
  function defaultInstallStoragePath(envName) {
214
- const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
215
- return `./${name}/storage/`;
234
+ return deriveConfiguredStoragePath(defaultInstallAppPath(envName));
235
+ }
236
+ function resolveConfiguredAppPathValue(values) {
237
+ const explicitAppPath = toOptionalPromptString(values.appPath);
238
+ if (explicitAppPath) {
239
+ return explicitAppPath;
240
+ }
241
+ return inferConfiguredAppPathFromLegacyConfig({
242
+ appRootPath: toOptionalPromptString(values.appRootPath),
243
+ storagePath: toOptionalPromptString(values.storagePath),
244
+ });
245
+ }
246
+ function resolveConfiguredSourcePathValue(values, envName) {
247
+ const legacyAppRootPath = toOptionalPromptString(values.appRootPath);
248
+ if (legacyAppRootPath) {
249
+ return legacyAppRootPath;
250
+ }
251
+ const appPath = resolveConfiguredAppPathValue(values) ?? defaultInstallAppPath(envName);
252
+ return deriveConfiguredSourcePath(appPath);
253
+ }
254
+ function resolveConfiguredStoragePathValue(values, envName) {
255
+ const legacyStoragePath = toOptionalPromptString(values.storagePath);
256
+ if (legacyStoragePath) {
257
+ return legacyStoragePath;
258
+ }
259
+ const appPath = resolveConfiguredAppPathValue(values) ?? defaultInstallAppPath(envName);
260
+ return deriveConfiguredStoragePath(appPath);
216
261
  }
217
262
  function pickPresetKeys(source, keys) {
218
263
  const out = {};
@@ -233,6 +278,15 @@ function optionalEnvBoolean(value) {
233
278
  }
234
279
  return Boolean(value);
235
280
  }
281
+ function resolveExtractClientAssetsDefaultEnabled(value) {
282
+ const text = String(value ?? '')
283
+ .trim()
284
+ .toLowerCase();
285
+ if (!text) {
286
+ return true;
287
+ }
288
+ return !['0', 'false', 'no', 'off'].includes(text);
289
+ }
236
290
  function pushOptionalEnvArg(args, key, value) {
237
291
  if (typeof value === 'string') {
238
292
  if (!value) {
@@ -275,9 +329,9 @@ export default class Install extends Command {
275
329
  '<%= config.bin %> <%= command.id %> --env app1 -l zh-CN',
276
330
  '<%= config.bin %> <%= command.id %> --env app1 --root-username nocobase --root-email admin@nocobase.com --root-password admin123',
277
331
  '<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
278
- '<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
279
- '<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
280
- '<%= config.bin %> <%= command.id %> --env dev -y --skip-download --source git --app-root-path=./dev/source/',
332
+ '<%= config.bin %> <%= command.id %> --env myenv --app-path=./myenv/',
333
+ '<%= config.bin %> <%= command.id %> --env dev -y --app-path=./dev/',
334
+ '<%= config.bin %> <%= command.id %> --env dev -y --skip-download --source git --app-path=./dev/',
281
335
  ];
282
336
  static flags = {
283
337
  yes: Flags.boolean({
@@ -299,7 +353,7 @@ export default class Install extends Command {
299
353
  }),
300
354
  env: Flags.string({
301
355
  char: 'e',
302
- description: 'App/env name to create or update. Defaults app paths to ./<envName>/source/ and ./<envName>/storage/.',
356
+ description: 'App/env name to create or update. Defaults the app path to ./<envName>/ and derives source/storage inside it.',
303
357
  }),
304
358
  'default-api-base-url': Flags.string({
305
359
  char: 'd',
@@ -336,14 +390,24 @@ export default class Install extends Command {
336
390
  char: 'f',
337
391
  required: false,
338
392
  }),
393
+ 'app-path': Flags.string({
394
+ description: 'App directory for a local npm/git app (default: ./<envName>/)',
395
+ }),
339
396
  'app-root-path': Flags.string({
340
- description: 'Source directory for a local npm/git app (default: ./<envName>/source/)',
397
+ hidden: true,
398
+ deprecated: true,
399
+ description: 'Legacy source directory for a local npm/git app',
341
400
  }),
342
401
  'app-port': Flags.string({
343
402
  description: 'HTTP port for the local app (default: 13000, or an available port with --yes)',
344
403
  }),
345
404
  'storage-path': Flags.string({
346
- description: 'Storage directory for uploads and managed database data (default: ./<envName>/storage/)',
405
+ hidden: true,
406
+ deprecated: true,
407
+ description: 'Legacy storage directory for uploads and managed database data',
408
+ }),
409
+ 'app-public-path': Flags.string({
410
+ description: 'Public path for the local app, for example / or /console/',
347
411
  }),
348
412
  'root-username': Flags.string({
349
413
  description: 'Initial admin username for the installed app',
@@ -415,39 +479,44 @@ export default class Install extends Command {
415
479
  validate: validateEnvKey,
416
480
  },
417
481
  };
418
- static appPrompts = {
419
- lang: {
420
- type: 'select',
421
- message: installText('prompts.lang.message'),
422
- options: INSTALL_LANGUAGE_OPTIONS,
423
- initialValue: DEFAULT_INSTALL_LANG,
424
- yesInitialValue: DEFAULT_INSTALL_LANG,
425
- },
426
- // force: {
427
- // type: 'boolean',
428
- // message: 'Reinstall the application by clearing the database? (-f / --force)',
429
- // initialValue: false,
430
- // yesInitialValue: false,
431
- // },
432
- appRootPath: {
433
- type: 'text',
434
- message: installText('prompts.appRootPath.message'),
435
- placeholder: installText('prompts.appRootPath.placeholder'),
436
- initialValue: (values) => defaultInstallAppRootPath(values.env ?? values.appName),
437
- },
438
- appPort: {
439
- type: 'text',
440
- message: installText('prompts.appPort.message'),
441
- placeholder: installText('prompts.appPort.placeholder'),
442
- validate: Install.validateAppPort,
443
- },
444
- storagePath: {
445
- type: 'text',
446
- message: installText('prompts.storagePath.message'),
447
- placeholder: installText('prompts.storagePath.placeholder'),
448
- initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
449
- },
450
- };
482
+ static get appPrompts() {
483
+ return {
484
+ lang: {
485
+ type: 'select',
486
+ message: installText('prompts.lang.message'),
487
+ options: INSTALL_LANGUAGE_OPTIONS,
488
+ initialValue: DEFAULT_INSTALL_LANG,
489
+ yesInitialValue: DEFAULT_INSTALL_LANG,
490
+ },
491
+ // force: {
492
+ // type: 'boolean',
493
+ // message: 'Reinstall the application by clearing the database? (-f / --force)',
494
+ // initialValue: false,
495
+ // yesInitialValue: false,
496
+ // },
497
+ appPath: {
498
+ type: 'text',
499
+ message: installText('prompts.appPath.message', { root: resolveEnvRoot() }),
500
+ placeholder: installText('prompts.appPath.placeholder'),
501
+ initialValue: (values) => defaultInstallAppPath(values.env ?? values.appName),
502
+ hidden: (values) => Boolean(Install.toOptionalPromptString(values.appRootPath)),
503
+ },
504
+ appPort: {
505
+ type: 'text',
506
+ message: installText('prompts.appPort.message'),
507
+ placeholder: installText('prompts.appPort.placeholder'),
508
+ validate: Install.validateAppPort,
509
+ },
510
+ appPublicPath: {
511
+ type: 'text',
512
+ message: installText('prompts.appPublicPath.message'),
513
+ placeholder: installText('prompts.appPublicPath.placeholder'),
514
+ initialValue: '/',
515
+ yesInitialValue: '/',
516
+ validate: validateAppPublicPath,
517
+ },
518
+ };
519
+ }
451
520
  static dbPrompts = {
452
521
  dbDialect: {
453
522
  type: 'select',
@@ -571,8 +640,8 @@ export default class Install extends Command {
571
640
  },
572
641
  };
573
642
  /**
574
- * App catalog with `env` seeded into `out` first so `storagePath`’s `initialValue(values)`
575
- * sees `values.env` (same iteration order as {@link runPromptCatalog}).
643
+ * App catalog with `env` seeded into `out` first so app path defaults can see `values.env`
644
+ * (same iteration order as {@link runPromptCatalog}).
576
645
  */
577
646
  static buildAppPromptsCatalog(seedEnv, options) {
578
647
  return {
@@ -673,6 +742,12 @@ export default class Install extends Command {
673
742
  if (argvHasToken(argv, ['--force', '-f'])) {
674
743
  preset.force = flags.force;
675
744
  }
745
+ if (flags['app-path'] !== undefined) {
746
+ const v = flags['app-path']?.trim();
747
+ if (v) {
748
+ preset.appPath = v;
749
+ }
750
+ }
676
751
  if (flags['app-root-path'] !== undefined) {
677
752
  const v = flags['app-root-path']?.trim();
678
753
  if (v) {
@@ -691,6 +766,12 @@ export default class Install extends Command {
691
766
  preset.storagePath = v;
692
767
  }
693
768
  }
769
+ if (flags['app-public-path'] !== undefined) {
770
+ const v = String(flags['app-public-path'] ?? '').trim();
771
+ if (v) {
772
+ preset.appPublicPath = v;
773
+ }
774
+ }
694
775
  if (flags['root-username'] !== undefined) {
695
776
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
696
777
  }
@@ -777,9 +858,11 @@ export default class Install extends Command {
777
858
  return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
778
859
  'lang',
779
860
  'force',
861
+ 'appPath',
780
862
  'appRootPath',
781
863
  'appPort',
782
864
  'storagePath',
865
+ 'appPublicPath',
783
866
  ]);
784
867
  }
785
868
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -836,11 +919,26 @@ export default class Install extends Command {
836
919
  };
837
920
  }
838
921
  static toOptionalPromptString(value) {
839
- if (value === undefined || value === null) {
840
- return undefined;
841
- }
842
- const text = String(value).trim();
843
- return text || undefined;
922
+ return toOptionalPromptString(value);
923
+ }
924
+ static resolveManagedAppKey(value) {
925
+ return Install.toOptionalPromptString(value) ?? crypto.randomBytes(32).toString('hex');
926
+ }
927
+ static resolveManagedTimeZone(value) {
928
+ return Install.toOptionalPromptString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
929
+ }
930
+ static formatLocalClientExtractWarning(envName, message) {
931
+ return [
932
+ `Client assets were not extracted for "${envName}".`,
933
+ 'NocoBase will keep starting, but versioned client files for CDN or external distribution may be stale or missing.',
934
+ `Details: ${message}`,
935
+ ].join('\n');
936
+ }
937
+ async ensureManagedAppRuntimeConfig(params) {
938
+ const savedEnv = await getEnv(params.envName, { scope: resolveDefaultConfigScope() });
939
+ const savedConfig = savedEnv?.config;
940
+ params.appResults.appKey = Install.resolveManagedAppKey(params.appResults.appKey ?? savedConfig?.appKey);
941
+ params.appResults.timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone ?? savedConfig?.timezone);
844
942
  }
845
943
  static async validateAppPort(value, values) {
846
944
  const formatError = validateTcpPort(value);
@@ -916,7 +1014,7 @@ export default class Install extends Command {
916
1014
  const source = Install.toOptionalPromptString(values.source);
917
1015
  const builtinDb = values.builtinDb === undefined ? undefined : Boolean(values.builtinDb);
918
1016
  const dbDialect = Install.toOptionalPromptString(values.dbDialect);
919
- const appRootPath = Install.toOptionalPromptString(values.appRootPath);
1017
+ const appRootPath = resolveConfiguredSourcePathValue(values, envName);
920
1018
  const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
921
1019
  const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
922
1020
  return {
@@ -987,9 +1085,12 @@ export default class Install extends Command {
987
1085
  const envName = String(env.name ?? '').trim();
988
1086
  const config = env.config ?? {};
989
1087
  const source = Install.toOptionalPromptString(config.source);
1088
+ const appPath = Install.toOptionalPromptString(config.appPath) ?? inferConfiguredAppPathFromLegacyConfig(config);
990
1089
  const appRootPath = Install.toOptionalPromptString(config.appRootPath);
991
1090
  const appPort = Install.toOptionalPromptString(config.appPort);
992
1091
  const storagePath = Install.toOptionalPromptString(config.storagePath);
1092
+ const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
1093
+ const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
993
1094
  const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
994
1095
  const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
995
1096
  const dockerPlatform = Install.toOptionalPromptString(config.dockerPlatform);
@@ -1012,9 +1113,11 @@ export default class Install extends Command {
1012
1113
  const auth = config.auth;
1013
1114
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1014
1115
  const appPreset = {
1116
+ ...(appPath ? { appPath } : {}),
1015
1117
  ...(appRootPath ? { appRootPath } : {}),
1016
1118
  ...(appPort ? { appPort } : {}),
1017
1119
  ...(storagePath ? { storagePath } : {}),
1120
+ ...(appPublicPath ? { appPublicPath } : {}),
1018
1121
  };
1019
1122
  const downloadPreset = {
1020
1123
  ...(source ? { source } : {}),
@@ -1052,6 +1155,9 @@ export default class Install extends Command {
1052
1155
  ...(rootNickname ? { rootNickname } : {}),
1053
1156
  };
1054
1157
  const envAddPreset = {};
1158
+ if (apiBaseUrl) {
1159
+ envAddPreset.apiBaseUrl = apiBaseUrl;
1160
+ }
1055
1161
  if (savedAuthType === 'token') {
1056
1162
  envAddPreset.authType = 'token';
1057
1163
  if (Install.toOptionalPromptString(auth.accessToken)) {
@@ -1112,7 +1218,7 @@ export default class Install extends Command {
1112
1218
  throw new Error([
1113
1219
  `Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
1114
1220
  `These setup-only flags are not saved in the env config: ${missingFlags.join(', ')}`,
1115
- `Run \`nb init --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1221
+ `Run \`nb init --ui --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1116
1222
  ].join('\n'));
1117
1223
  }
1118
1224
  }
@@ -1133,6 +1239,9 @@ export default class Install extends Command {
1133
1239
  static async buildAppPromptInitialValues(params) {
1134
1240
  const initialValues = {};
1135
1241
  const envName = params.envName ?? DEFAULT_INSTALL_ENV_NAME;
1242
+ if (params.flags['app-path'] === undefined && params.flags['app-root-path'] === undefined) {
1243
+ initialValues.appPath = defaultInstallAppPath(envName);
1244
+ }
1136
1245
  if (params.flags['app-root-path'] === undefined) {
1137
1246
  initialValues.appRootPath = defaultInstallAppRootPath(envName);
1138
1247
  }
@@ -1176,7 +1285,7 @@ export default class Install extends Command {
1176
1285
  * output directory with app settings, while Docker registry defaults follow the CLI locale.
1177
1286
  */
1178
1287
  static buildDownloadPromptOptionsForInstall(appResults, envName) {
1179
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1288
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1180
1289
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1181
1290
  const initialValues = {
1182
1291
  lang,
@@ -1207,7 +1316,7 @@ export default class Install extends Command {
1207
1316
  */
1208
1317
  static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes, argv = process.argv.slice(2)) {
1209
1318
  const preset = {};
1210
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1319
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1211
1320
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1212
1321
  preset.lang = lang;
1213
1322
  if (flags.source !== undefined && String(flags.source).trim() !== '') {
@@ -1338,7 +1447,8 @@ export default class Install extends Command {
1338
1447
  : dbHostInput && dbHostInput !== DEFAULT_INSTALL_DB_HOST && dbHostInput !== 'localhost'
1339
1448
  ? dbHostInput
1340
1449
  : containerName;
1341
- const storagePath = resolveConfiguredEnvPath(params.storagePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1450
+ const storagePath = resolveConfiguredEnvPath(params.storagePath) ??
1451
+ resolveEnvRelativePath(resolveConfiguredStoragePathValue({ appPath: params.appPath, storagePath: params.storagePath }, params.envName));
1342
1452
  if (dbDialect === 'postgres') {
1343
1453
  const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1344
1454
  const dataDir = path.resolve(storagePath, 'db', 'postgres');
@@ -1529,6 +1639,7 @@ export default class Install extends Command {
1529
1639
  if (this.ensuredDockerNetworks.has(name)) {
1530
1640
  return;
1531
1641
  }
1642
+ await ensureDockerDaemonRunning('prepare Docker resources for this environment');
1532
1643
  printVerbose(`Checking Docker network: ${name}`);
1533
1644
  const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
1534
1645
  if (exists) {
@@ -1607,12 +1718,13 @@ export default class Install extends Command {
1607
1718
  });
1608
1719
  }
1609
1720
  async startBuiltinDb(params) {
1610
- const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1721
+ const storagePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
1611
1722
  const plan = Install.buildBuiltinDbPlan({
1612
1723
  envName: params.envName,
1613
1724
  workspaceName: params.workspaceName,
1614
1725
  dockerNetworkName: params.dockerNetworkName,
1615
1726
  dockerContainerPrefix: params.dockerContainerPrefix,
1727
+ appPath: params.appResults.appPath,
1616
1728
  storagePath,
1617
1729
  source: params.downloadResults.source,
1618
1730
  dbDialect: params.dbResults.dbDialect,
@@ -1653,7 +1765,9 @@ export default class Install extends Command {
1653
1765
  defaultVersion: DEFAULT_DOCKER_VERSION,
1654
1766
  });
1655
1767
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1656
- const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName)) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1768
+ const configuredStoragePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
1769
+ const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1770
+ resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1657
1771
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1658
1772
  const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
1659
1773
  const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
@@ -1664,8 +1778,10 @@ export default class Install extends Command {
1664
1778
  const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1665
1779
  const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1666
1780
  const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1667
- const appKey = crypto.randomBytes(32).toString('hex');
1668
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1781
+ const extractClientAssets = resolveExtractClientAssetsDefaultEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
1782
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1783
+ const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
1784
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1669
1785
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1670
1786
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
1671
1787
  const envFile = await resolveDockerEnvFileArg(params.envName, configuredEnvFile ? { envFile: configuredEnvFile } : undefined);
@@ -1692,9 +1808,11 @@ export default class Install extends Command {
1692
1808
  args.push('-e', `${key}=${value}`);
1693
1809
  }
1694
1810
  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`);
1811
+ pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath);
1695
1812
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1696
1813
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1697
1814
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
1815
+ pushOptionalEnvArg(args, 'NOCOBASE_EXTRACT_CLIENT_ASSETS', extractClientAssets);
1698
1816
  args.push(imageRef);
1699
1817
  return {
1700
1818
  source: 'docker',
@@ -1801,17 +1919,15 @@ export default class Install extends Command {
1801
1919
  return projectRoot;
1802
1920
  }
1803
1921
  const outputDir = String(params.downloadResults.outputDir ?? '').trim() ||
1804
- String(params.appResults.appRootPath ?? '').trim() ||
1805
- defaultInstallAppRootPath(params.envName);
1922
+ resolveConfiguredSourcePathValue(params.appResults, params.envName);
1806
1923
  return resolveConfiguredEnvPath(outputDir) ?? resolveEnvRelativePath(defaultInstallAppRootPath(params.envName));
1807
1924
  }
1808
1925
  static resolveLocalProjectConfigPath(params) {
1809
1926
  return (String(params.downloadResults.outputDir ?? '').trim() ||
1810
- String(params.appResults.appRootPath ?? '').trim() ||
1811
- defaultInstallAppRootPath(params.envName));
1927
+ resolveConfiguredSourcePathValue(params.appResults, params.envName));
1812
1928
  }
1813
1929
  static buildSkipDownloadValues(envName, appResults) {
1814
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1930
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1815
1931
  return {
1816
1932
  outputDir: appRoot,
1817
1933
  replace: false,
@@ -1884,12 +2000,12 @@ export default class Install extends Command {
1884
2000
  return downloadedProjectRoot;
1885
2001
  }
1886
2002
  static buildLocalAppEnvVars(params) {
1887
- const configuredStoragePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
2003
+ const configuredStoragePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
1888
2004
  const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1889
2005
  resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1890
2006
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1891
- const appKey = crypto.randomBytes(32).toString('hex');
1892
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
2007
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
2008
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1893
2009
  const env = {
1894
2010
  STORAGE_PATH: storagePath,
1895
2011
  APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
@@ -1907,6 +2023,7 @@ export default class Install extends Command {
1907
2023
  rootResults: params.rootResults,
1908
2024
  }),
1909
2025
  };
2026
+ setOptionalEnvVar(env, 'APP_PUBLIC_PATH', Install.toOptionalPromptString(params.appResults.appPublicPath));
1910
2027
  setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
1911
2028
  setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
1912
2029
  setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
@@ -1932,6 +2049,24 @@ export default class Install extends Command {
1932
2049
  const message = error instanceof Error ? error.message : String(error);
1933
2050
  this.logDetail(`Skipped local process cleanup before start: ${message}`);
1934
2051
  }
2052
+ this.logDetail(`Running local postinstall in ${params.projectRoot}`);
2053
+ await runNocoBaseCommand(['postinstall'], {
2054
+ cwd: params.projectRoot,
2055
+ env,
2056
+ stdio: params.commandStdio ?? 'ignore',
2057
+ });
2058
+ this.logDetail(`Extracting local client assets in ${params.projectRoot}`);
2059
+ try {
2060
+ await runNocoBaseCommand(['client:extract'], {
2061
+ cwd: params.projectRoot,
2062
+ env,
2063
+ stdio: params.commandStdio ?? 'ignore',
2064
+ });
2065
+ }
2066
+ catch (error) {
2067
+ const message = error instanceof Error ? error.message : String(error);
2068
+ printWarning(Install.formatLocalClientExtractWarning(params.envName, message));
2069
+ }
1935
2070
  this.logDetail(`Starting local NocoBase app from ${params.projectRoot}`);
1936
2071
  await runNocoBaseCommand(args, {
1937
2072
  cwd: params.projectRoot,
@@ -1951,8 +2086,8 @@ export default class Install extends Command {
1951
2086
  };
1952
2087
  }
1953
2088
  static resolveApiBaseUrl(params) {
1954
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1955
- return String(params.envAddResults.apiBaseUrl ?? '').trim() || `http://127.0.0.1:${appPort}/api`;
2089
+ return (String(params.envAddResults.apiBaseUrl ?? '').trim() ||
2090
+ buildInstallApiBaseUrl(params.appResults, params.defaultApiHost));
1956
2091
  }
1957
2092
  static buildHealthCheckUrl(apiBaseUrl) {
1958
2093
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
@@ -2010,35 +2145,44 @@ export default class Install extends Command {
2010
2145
  let lastMessage = 'No response yet';
2011
2146
  let lastLoggedStatus = '';
2012
2147
  printInfo('Waiting for NocoBase to become ready...');
2013
- while (Date.now() - startedAt < timeoutMs) {
2014
- const result = await Install.requestAppHealthCheck({
2015
- healthCheckUrl,
2016
- fetchImpl,
2017
- requestTimeoutMs,
2018
- });
2019
- if (result.ok) {
2020
- return;
2021
- }
2022
- lastMessage = result.message;
2023
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2024
- const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2025
- if (statusLine !== lastLoggedStatus) {
2026
- printInfo(statusLine);
2027
- lastLoggedStatus = statusLine;
2028
- }
2029
- const remainingMs = timeoutMs - (Date.now() - startedAt);
2030
- if (remainingMs <= 0) {
2031
- break;
2148
+ const dockerLogFollower = options?.verbose && options.containerName ? startDockerLogFollower(options.containerName) : undefined;
2149
+ try {
2150
+ while (Date.now() - startedAt < timeoutMs) {
2151
+ const result = await Install.requestAppHealthCheck({
2152
+ healthCheckUrl,
2153
+ fetchImpl,
2154
+ requestTimeoutMs,
2155
+ });
2156
+ if (result.ok) {
2157
+ return;
2158
+ }
2159
+ lastMessage = result.message;
2160
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2161
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2162
+ if (statusLine !== lastLoggedStatus) {
2163
+ printInfo(statusLine);
2164
+ lastLoggedStatus = statusLine;
2165
+ }
2166
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
2167
+ if (remainingMs <= 0) {
2168
+ break;
2169
+ }
2170
+ await Install.sleep(Math.min(intervalMs, remainingMs));
2032
2171
  }
2033
- await Install.sleep(Math.min(intervalMs, remainingMs));
2172
+ const logHint = options?.containerName
2173
+ ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2174
+ : '';
2175
+ throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2176
+ }
2177
+ finally {
2178
+ await dockerLogFollower?.stop();
2034
2179
  }
2035
- const logHint = options?.containerName
2036
- ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2037
- : '';
2038
- throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2039
2180
  }
2040
2181
  async saveInstalledEnv(params) {
2041
- await upsertEnv(params.envName, Install.buildSavedEnvConfig(params), { scope: resolveDefaultConfigScope() });
2182
+ const defaultApiHost = await resolveDefaultApiHost();
2183
+ await upsertEnv(params.envName, Install.buildSavedEnvConfig(params, { defaultApiHost }), {
2184
+ scope: resolveDefaultConfigScope(),
2185
+ });
2042
2186
  await setCurrentEnv(params.envName, { scope: resolveDefaultConfigScope() });
2043
2187
  }
2044
2188
  async syncInstalledEnvConnection(params) {
@@ -2067,13 +2211,19 @@ export default class Install extends Command {
2067
2211
  }
2068
2212
  await this.config.runCommand('env:update', [params.envName]);
2069
2213
  }
2070
- static buildSavedEnvConfig(params) {
2214
+ static buildSavedEnvConfig(params, options = {}) {
2215
+ const appPath = resolveConfiguredAppPathValue(params.appResults);
2216
+ const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2217
+ const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2218
+ const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2219
+ const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2220
+ const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2071
2221
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
2072
- const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
2073
2222
  const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
2074
2223
  const apiBaseUrl = Install.resolveApiBaseUrl({
2075
2224
  appResults: params.appResults,
2076
2225
  envAddResults: params.envAddResults,
2226
+ defaultApiHost: options.defaultApiHost,
2077
2227
  });
2078
2228
  const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2079
2229
  const authUsername = authType === 'basic' ? String(params.envAddResults.username ?? params.rootResults.rootUsername ?? '').trim() : '';
@@ -2091,9 +2241,11 @@ export default class Install extends Command {
2091
2241
  devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2092
2242
  build: downloadResultsValue(params.downloadResults, 'build'),
2093
2243
  buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2094
- appRootPath: params.appResults.appRootPath,
2244
+ ...(appPath ? { appPath } : {}),
2245
+ ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
2095
2246
  appPort,
2096
- storagePath,
2247
+ ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
2248
+ ...(appPublicPath ? { appPublicPath } : {}),
2097
2249
  ...(envFile ? { envFile } : {}),
2098
2250
  appKey: params.appResults.appKey,
2099
2251
  timezone: params.appResults.timeZone,
@@ -2116,6 +2268,7 @@ export default class Install extends Command {
2116
2268
  }
2117
2269
  async collectPromptResults(parsed, yes) {
2118
2270
  const commandArgv = this.argv ?? process.argv.slice(2);
2271
+ const defaultApiHost = await resolveDefaultApiHost();
2119
2272
  const resumePreset = await this.resolveResumePresetValues(parsed, yes);
2120
2273
  const envPreset = {
2121
2274
  ...(resumePreset?.envPreset ?? {}),
@@ -2141,6 +2294,7 @@ export default class Install extends Command {
2141
2294
  envName,
2142
2295
  flags: {
2143
2296
  ...parsed,
2297
+ 'app-path': parsed['app-path'] ?? Install.toOptionalPromptString(appPreset.appPath),
2144
2298
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2145
2299
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2146
2300
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
@@ -2210,7 +2364,7 @@ export default class Install extends Command {
2210
2364
  };
2211
2365
  const resolvedEnvAddAuthType = String(envAddPreset.authType ?? '').trim();
2212
2366
  const envAddInitialValues = {
2213
- apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2367
+ apiBaseUrl: buildInstallApiBaseUrl(appResults, defaultApiHost),
2214
2368
  ...envAddResumePreset,
2215
2369
  ...(!parsed['skip-auth'] && resolvedEnvAddAuthType === 'basic'
2216
2370
  ? {
@@ -2257,6 +2411,7 @@ export default class Install extends Command {
2257
2411
  const parsed = {
2258
2412
  ...flags,
2259
2413
  };
2414
+ const defaultApiHost = await resolveDefaultApiHost();
2260
2415
  if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2261
2416
  this.error('--skip-auth cannot be used with --access-token or --token.');
2262
2417
  }
@@ -2273,6 +2428,10 @@ export default class Install extends Command {
2273
2428
  }
2274
2429
  const promptResults = await this.collectPromptResults(parsed, flags.yes);
2275
2430
  const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults } = promptResults;
2431
+ await this.ensureManagedAppRuntimeConfig({
2432
+ envName,
2433
+ appResults,
2434
+ });
2276
2435
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2277
2436
  const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2278
2437
  const dockerNetworkName = usesDockerResources
@@ -2390,10 +2549,17 @@ export default class Install extends Command {
2390
2549
  await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2391
2550
  appResults,
2392
2551
  envAddResults,
2552
+ defaultApiHost,
2393
2553
  }), {
2394
2554
  containerName: dockerAppPlan?.containerName,
2555
+ verbose: parsed.verbose,
2556
+ });
2557
+ const displayApiBaseUrl = Install.resolveApiBaseUrl({
2558
+ appResults,
2559
+ envAddResults,
2560
+ defaultApiHost,
2395
2561
  });
2396
- printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2562
+ printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
2397
2563
  }
2398
2564
  if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2399
2565
  await this.saveInstalledEnv({
@@ -2411,6 +2577,7 @@ export default class Install extends Command {
2411
2577
  appReady: Boolean(dockerAppPlan || localAppPlan),
2412
2578
  skipAuth: Boolean(parsed['skip-auth']),
2413
2579
  });
2580
+ await clearEnvRootSetup(envName, { scope: resolveDefaultConfigScope() });
2414
2581
  if (!dockerAppPlan && !localAppPlan) {
2415
2582
  printInfo(`Install config for "${envName}" has been saved.`);
2416
2583
  }