@nocobase/cli 2.1.0-alpha.46 → 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 (77) 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 -3
  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 +89 -24
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +142 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +9 -5
  27. package/dist/commands/config/delete.js +6 -1
  28. package/dist/commands/config/get.js +6 -1
  29. package/dist/commands/config/set.js +7 -2
  30. package/dist/commands/env/add.js +24 -44
  31. package/dist/commands/env/info.js +3 -2
  32. package/dist/commands/env/proxy/caddy.js +28 -0
  33. package/dist/commands/env/proxy/index.js +353 -0
  34. package/dist/commands/env/proxy/nginx.js +28 -0
  35. package/dist/commands/env/remove.js +112 -22
  36. package/dist/commands/env/shared.js +17 -9
  37. package/dist/commands/env/update.js +385 -21
  38. package/dist/commands/init.js +350 -98
  39. package/dist/commands/install.js +218 -75
  40. package/dist/commands/license/activate.js +63 -244
  41. package/dist/commands/license/plugins/shared.js +64 -13
  42. package/dist/commands/plugin/import.js +108 -0
  43. package/dist/commands/revision/create.js +89 -0
  44. package/dist/commands/source/test.js +13 -14
  45. package/dist/lib/app-health.js +5 -5
  46. package/dist/lib/app-managed-resources.js +24 -8
  47. package/dist/lib/app-public-path.js +80 -0
  48. package/dist/lib/app-runtime.js +10 -1
  49. package/dist/lib/auth-store.js +119 -4
  50. package/dist/lib/cli-config.js +191 -3
  51. package/dist/lib/cli-entry-error.js +8 -0
  52. package/dist/lib/command-log.js +284 -0
  53. package/dist/lib/docker-env-file.js +10 -2
  54. package/dist/lib/env-auth.js +4 -1
  55. package/dist/lib/env-command-config.js +45 -0
  56. package/dist/lib/env-config.js +7 -2
  57. package/dist/lib/env-guard.js +9 -10
  58. package/dist/lib/env-paths.js +101 -0
  59. package/dist/lib/env-proxy.js +1295 -0
  60. package/dist/lib/inquirer.js +4 -5
  61. package/dist/lib/managed-env-file.js +98 -0
  62. package/dist/lib/plugin-import.js +279 -0
  63. package/dist/lib/prompt-catalog-core.js +3 -2
  64. package/dist/lib/prompt-validators.js +14 -0
  65. package/dist/lib/prompt-web-ui.js +8 -1
  66. package/dist/lib/run-npm.js +56 -12
  67. package/dist/lib/skills-manager.js +3 -1
  68. package/dist/lib/source-publish.js +18 -1
  69. package/dist/lib/ui.js +2 -1
  70. package/dist/locale/en-US.json +116 -22
  71. package/dist/locale/zh-CN.json +113 -19
  72. package/package.json +5 -7
  73. package/scripts/build.mjs +34 -0
  74. package/scripts/clean.mjs +9 -0
  75. package/tsconfig.json +19 -0
  76. package/LICENSE.txt +0 -107
  77. package/README.zh-CN.md +0 -355
@@ -11,24 +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";
29
30
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
+ import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
30
32
  import Download, { defaultDockerRegistryForLang } from './download.js';
31
33
  import EnvAdd from "./env/add.js";
34
+ import { resolveAppUrlFromApiBaseUrl } from "./env/shared.js";
32
35
  const DEFAULT_INSTALL_ENV_NAME = 'local';
33
36
  const DEFAULT_INSTALL_LANG = 'en-US';
34
37
  const DEFAULT_INSTALL_APP_PORT = '13000';
@@ -59,6 +62,19 @@ const DEFAULT_INSTALL_ROOT_USERNAME = 'nocobase';
59
62
  const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
60
63
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
61
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
+ }
62
78
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
63
79
  const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
64
80
  const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
@@ -207,13 +223,41 @@ async function validateExternalDbPromptField(value, values) {
207
223
  }
208
224
  return await validateMysqlLowerCaseTableNamesCompatibility(values);
209
225
  }
210
- function defaultInstallAppRootPath(envName) {
226
+ function defaultInstallAppPath(envName) {
211
227
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
212
- return `./${name}/source/`;
228
+ return `./${name}/`;
229
+ }
230
+ function defaultInstallAppRootPath(envName) {
231
+ return deriveConfiguredSourcePath(defaultInstallAppPath(envName));
213
232
  }
214
233
  function defaultInstallStoragePath(envName) {
215
- const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
216
- 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);
217
261
  }
218
262
  function pickPresetKeys(source, keys) {
219
263
  const out = {};
@@ -234,6 +278,15 @@ function optionalEnvBoolean(value) {
234
278
  }
235
279
  return Boolean(value);
236
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
+ }
237
290
  function pushOptionalEnvArg(args, key, value) {
238
291
  if (typeof value === 'string') {
239
292
  if (!value) {
@@ -276,9 +329,9 @@ export default class Install extends Command {
276
329
  '<%= config.bin %> <%= command.id %> --env app1 -l zh-CN',
277
330
  '<%= config.bin %> <%= command.id %> --env app1 --root-username nocobase --root-email admin@nocobase.com --root-password admin123',
278
331
  '<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
279
- '<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
280
- '<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
281
- '<%= 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/',
282
335
  ];
283
336
  static flags = {
284
337
  yes: Flags.boolean({
@@ -300,7 +353,7 @@ export default class Install extends Command {
300
353
  }),
301
354
  env: Flags.string({
302
355
  char: 'e',
303
- 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.',
304
357
  }),
305
358
  'default-api-base-url': Flags.string({
306
359
  char: 'd',
@@ -337,14 +390,24 @@ export default class Install extends Command {
337
390
  char: 'f',
338
391
  required: false,
339
392
  }),
393
+ 'app-path': Flags.string({
394
+ description: 'App directory for a local npm/git app (default: ./<envName>/)',
395
+ }),
340
396
  'app-root-path': Flags.string({
341
- 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',
342
400
  }),
343
401
  'app-port': Flags.string({
344
402
  description: 'HTTP port for the local app (default: 13000, or an available port with --yes)',
345
403
  }),
346
404
  'storage-path': Flags.string({
347
- 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/',
348
411
  }),
349
412
  'root-username': Flags.string({
350
413
  description: 'Initial admin username for the installed app',
@@ -416,39 +479,44 @@ export default class Install extends Command {
416
479
  validate: validateEnvKey,
417
480
  },
418
481
  };
419
- static appPrompts = {
420
- lang: {
421
- type: 'select',
422
- message: installText('prompts.lang.message'),
423
- options: INSTALL_LANGUAGE_OPTIONS,
424
- initialValue: DEFAULT_INSTALL_LANG,
425
- yesInitialValue: DEFAULT_INSTALL_LANG,
426
- },
427
- // force: {
428
- // type: 'boolean',
429
- // message: 'Reinstall the application by clearing the database? (-f / --force)',
430
- // initialValue: false,
431
- // yesInitialValue: false,
432
- // },
433
- appRootPath: {
434
- type: 'text',
435
- message: installText('prompts.appRootPath.message'),
436
- placeholder: installText('prompts.appRootPath.placeholder'),
437
- initialValue: (values) => defaultInstallAppRootPath(values.env ?? values.appName),
438
- },
439
- appPort: {
440
- type: 'text',
441
- message: installText('prompts.appPort.message'),
442
- placeholder: installText('prompts.appPort.placeholder'),
443
- validate: Install.validateAppPort,
444
- },
445
- storagePath: {
446
- type: 'text',
447
- message: installText('prompts.storagePath.message'),
448
- placeholder: installText('prompts.storagePath.placeholder'),
449
- initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
450
- },
451
- };
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
+ }
452
520
  static dbPrompts = {
453
521
  dbDialect: {
454
522
  type: 'select',
@@ -572,8 +640,8 @@ export default class Install extends Command {
572
640
  },
573
641
  };
574
642
  /**
575
- * App catalog with `env` seeded into `out` first so `storagePath`’s `initialValue(values)`
576
- * 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}).
577
645
  */
578
646
  static buildAppPromptsCatalog(seedEnv, options) {
579
647
  return {
@@ -674,6 +742,12 @@ export default class Install extends Command {
674
742
  if (argvHasToken(argv, ['--force', '-f'])) {
675
743
  preset.force = flags.force;
676
744
  }
745
+ if (flags['app-path'] !== undefined) {
746
+ const v = flags['app-path']?.trim();
747
+ if (v) {
748
+ preset.appPath = v;
749
+ }
750
+ }
677
751
  if (flags['app-root-path'] !== undefined) {
678
752
  const v = flags['app-root-path']?.trim();
679
753
  if (v) {
@@ -692,6 +766,12 @@ export default class Install extends Command {
692
766
  preset.storagePath = v;
693
767
  }
694
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
+ }
695
775
  if (flags['root-username'] !== undefined) {
696
776
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
697
777
  }
@@ -778,9 +858,11 @@ export default class Install extends Command {
778
858
  return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
779
859
  'lang',
780
860
  'force',
861
+ 'appPath',
781
862
  'appRootPath',
782
863
  'appPort',
783
864
  'storagePath',
865
+ 'appPublicPath',
784
866
  ]);
785
867
  }
786
868
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -837,11 +919,7 @@ export default class Install extends Command {
837
919
  };
838
920
  }
839
921
  static toOptionalPromptString(value) {
840
- if (value === undefined || value === null) {
841
- return undefined;
842
- }
843
- const text = String(value).trim();
844
- return text || undefined;
922
+ return toOptionalPromptString(value);
845
923
  }
846
924
  static resolveManagedAppKey(value) {
847
925
  return Install.toOptionalPromptString(value) ?? crypto.randomBytes(32).toString('hex');
@@ -849,6 +927,13 @@ export default class Install extends Command {
849
927
  static resolveManagedTimeZone(value) {
850
928
  return Install.toOptionalPromptString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
851
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
+ }
852
937
  async ensureManagedAppRuntimeConfig(params) {
853
938
  const savedEnv = await getEnv(params.envName, { scope: resolveDefaultConfigScope() });
854
939
  const savedConfig = savedEnv?.config;
@@ -929,7 +1014,7 @@ export default class Install extends Command {
929
1014
  const source = Install.toOptionalPromptString(values.source);
930
1015
  const builtinDb = values.builtinDb === undefined ? undefined : Boolean(values.builtinDb);
931
1016
  const dbDialect = Install.toOptionalPromptString(values.dbDialect);
932
- const appRootPath = Install.toOptionalPromptString(values.appRootPath);
1017
+ const appRootPath = resolveConfiguredSourcePathValue(values, envName);
933
1018
  const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
934
1019
  const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
935
1020
  return {
@@ -1000,9 +1085,12 @@ export default class Install extends Command {
1000
1085
  const envName = String(env.name ?? '').trim();
1001
1086
  const config = env.config ?? {};
1002
1087
  const source = Install.toOptionalPromptString(config.source);
1088
+ const appPath = Install.toOptionalPromptString(config.appPath) ?? inferConfiguredAppPathFromLegacyConfig(config);
1003
1089
  const appRootPath = Install.toOptionalPromptString(config.appRootPath);
1004
1090
  const appPort = Install.toOptionalPromptString(config.appPort);
1005
1091
  const storagePath = Install.toOptionalPromptString(config.storagePath);
1092
+ const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
1093
+ const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
1006
1094
  const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
1007
1095
  const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
1008
1096
  const dockerPlatform = Install.toOptionalPromptString(config.dockerPlatform);
@@ -1025,9 +1113,11 @@ export default class Install extends Command {
1025
1113
  const auth = config.auth;
1026
1114
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1027
1115
  const appPreset = {
1116
+ ...(appPath ? { appPath } : {}),
1028
1117
  ...(appRootPath ? { appRootPath } : {}),
1029
1118
  ...(appPort ? { appPort } : {}),
1030
1119
  ...(storagePath ? { storagePath } : {}),
1120
+ ...(appPublicPath ? { appPublicPath } : {}),
1031
1121
  };
1032
1122
  const downloadPreset = {
1033
1123
  ...(source ? { source } : {}),
@@ -1065,6 +1155,9 @@ export default class Install extends Command {
1065
1155
  ...(rootNickname ? { rootNickname } : {}),
1066
1156
  };
1067
1157
  const envAddPreset = {};
1158
+ if (apiBaseUrl) {
1159
+ envAddPreset.apiBaseUrl = apiBaseUrl;
1160
+ }
1068
1161
  if (savedAuthType === 'token') {
1069
1162
  envAddPreset.authType = 'token';
1070
1163
  if (Install.toOptionalPromptString(auth.accessToken)) {
@@ -1146,6 +1239,9 @@ export default class Install extends Command {
1146
1239
  static async buildAppPromptInitialValues(params) {
1147
1240
  const initialValues = {};
1148
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
+ }
1149
1245
  if (params.flags['app-root-path'] === undefined) {
1150
1246
  initialValues.appRootPath = defaultInstallAppRootPath(envName);
1151
1247
  }
@@ -1189,7 +1285,7 @@ export default class Install extends Command {
1189
1285
  * output directory with app settings, while Docker registry defaults follow the CLI locale.
1190
1286
  */
1191
1287
  static buildDownloadPromptOptionsForInstall(appResults, envName) {
1192
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1288
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1193
1289
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1194
1290
  const initialValues = {
1195
1291
  lang,
@@ -1220,7 +1316,7 @@ export default class Install extends Command {
1220
1316
  */
1221
1317
  static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes, argv = process.argv.slice(2)) {
1222
1318
  const preset = {};
1223
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1319
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1224
1320
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1225
1321
  preset.lang = lang;
1226
1322
  if (flags.source !== undefined && String(flags.source).trim() !== '') {
@@ -1351,7 +1447,8 @@ export default class Install extends Command {
1351
1447
  : dbHostInput && dbHostInput !== DEFAULT_INSTALL_DB_HOST && dbHostInput !== 'localhost'
1352
1448
  ? dbHostInput
1353
1449
  : containerName;
1354
- 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));
1355
1452
  if (dbDialect === 'postgres') {
1356
1453
  const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1357
1454
  const dataDir = path.resolve(storagePath, 'db', 'postgres');
@@ -1542,6 +1639,7 @@ export default class Install extends Command {
1542
1639
  if (this.ensuredDockerNetworks.has(name)) {
1543
1640
  return;
1544
1641
  }
1642
+ await ensureDockerDaemonRunning('prepare Docker resources for this environment');
1545
1643
  printVerbose(`Checking Docker network: ${name}`);
1546
1644
  const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
1547
1645
  if (exists) {
@@ -1620,12 +1718,13 @@ export default class Install extends Command {
1620
1718
  });
1621
1719
  }
1622
1720
  async startBuiltinDb(params) {
1623
- const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1721
+ const storagePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
1624
1722
  const plan = Install.buildBuiltinDbPlan({
1625
1723
  envName: params.envName,
1626
1724
  workspaceName: params.workspaceName,
1627
1725
  dockerNetworkName: params.dockerNetworkName,
1628
1726
  dockerContainerPrefix: params.dockerContainerPrefix,
1727
+ appPath: params.appResults.appPath,
1629
1728
  storagePath,
1630
1729
  source: params.downloadResults.source,
1631
1730
  dbDialect: params.dbResults.dbDialect,
@@ -1666,7 +1765,9 @@ export default class Install extends Command {
1666
1765
  defaultVersion: DEFAULT_DOCKER_VERSION,
1667
1766
  });
1668
1767
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1669
- 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));
1670
1771
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1671
1772
  const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
1672
1773
  const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
@@ -1677,7 +1778,9 @@ export default class Install extends Command {
1677
1778
  const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1678
1779
  const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1679
1780
  const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1781
+ const extractClientAssets = resolveExtractClientAssetsDefaultEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
1680
1782
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1783
+ const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
1681
1784
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1682
1785
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1683
1786
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
@@ -1705,9 +1808,11 @@ export default class Install extends Command {
1705
1808
  args.push('-e', `${key}=${value}`);
1706
1809
  }
1707
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);
1708
1812
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1709
1813
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1710
1814
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
1815
+ pushOptionalEnvArg(args, 'NOCOBASE_EXTRACT_CLIENT_ASSETS', extractClientAssets);
1711
1816
  args.push(imageRef);
1712
1817
  return {
1713
1818
  source: 'docker',
@@ -1814,17 +1919,15 @@ export default class Install extends Command {
1814
1919
  return projectRoot;
1815
1920
  }
1816
1921
  const outputDir = String(params.downloadResults.outputDir ?? '').trim() ||
1817
- String(params.appResults.appRootPath ?? '').trim() ||
1818
- defaultInstallAppRootPath(params.envName);
1922
+ resolveConfiguredSourcePathValue(params.appResults, params.envName);
1819
1923
  return resolveConfiguredEnvPath(outputDir) ?? resolveEnvRelativePath(defaultInstallAppRootPath(params.envName));
1820
1924
  }
1821
1925
  static resolveLocalProjectConfigPath(params) {
1822
1926
  return (String(params.downloadResults.outputDir ?? '').trim() ||
1823
- String(params.appResults.appRootPath ?? '').trim() ||
1824
- defaultInstallAppRootPath(params.envName));
1927
+ resolveConfiguredSourcePathValue(params.appResults, params.envName));
1825
1928
  }
1826
1929
  static buildSkipDownloadValues(envName, appResults) {
1827
- const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1930
+ const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1828
1931
  return {
1829
1932
  outputDir: appRoot,
1830
1933
  replace: false,
@@ -1897,7 +2000,7 @@ export default class Install extends Command {
1897
2000
  return downloadedProjectRoot;
1898
2001
  }
1899
2002
  static buildLocalAppEnvVars(params) {
1900
- const configuredStoragePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
2003
+ const configuredStoragePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
1901
2004
  const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1902
2005
  resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1903
2006
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
@@ -1920,6 +2023,7 @@ export default class Install extends Command {
1920
2023
  rootResults: params.rootResults,
1921
2024
  }),
1922
2025
  };
2026
+ setOptionalEnvVar(env, 'APP_PUBLIC_PATH', Install.toOptionalPromptString(params.appResults.appPublicPath));
1923
2027
  setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
1924
2028
  setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
1925
2029
  setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
@@ -1945,6 +2049,24 @@ export default class Install extends Command {
1945
2049
  const message = error instanceof Error ? error.message : String(error);
1946
2050
  this.logDetail(`Skipped local process cleanup before start: ${message}`);
1947
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
+ }
1948
2070
  this.logDetail(`Starting local NocoBase app from ${params.projectRoot}`);
1949
2071
  await runNocoBaseCommand(args, {
1950
2072
  cwd: params.projectRoot,
@@ -1964,8 +2086,8 @@ export default class Install extends Command {
1964
2086
  };
1965
2087
  }
1966
2088
  static resolveApiBaseUrl(params) {
1967
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1968
- 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));
1969
2091
  }
1970
2092
  static buildHealthCheckUrl(apiBaseUrl) {
1971
2093
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
@@ -2057,7 +2179,10 @@ export default class Install extends Command {
2057
2179
  }
2058
2180
  }
2059
2181
  async saveInstalledEnv(params) {
2060
- 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
+ });
2061
2186
  await setCurrentEnv(params.envName, { scope: resolveDefaultConfigScope() });
2062
2187
  }
2063
2188
  async syncInstalledEnvConnection(params) {
@@ -2086,13 +2211,19 @@ export default class Install extends Command {
2086
2211
  }
2087
2212
  await this.config.runCommand('env:update', [params.envName]);
2088
2213
  }
2089
- 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;
2090
2221
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
2091
- const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
2092
2222
  const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
2093
2223
  const apiBaseUrl = Install.resolveApiBaseUrl({
2094
2224
  appResults: params.appResults,
2095
2225
  envAddResults: params.envAddResults,
2226
+ defaultApiHost: options.defaultApiHost,
2096
2227
  });
2097
2228
  const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2098
2229
  const authUsername = authType === 'basic' ? String(params.envAddResults.username ?? params.rootResults.rootUsername ?? '').trim() : '';
@@ -2110,9 +2241,11 @@ export default class Install extends Command {
2110
2241
  devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2111
2242
  build: downloadResultsValue(params.downloadResults, 'build'),
2112
2243
  buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2113
- appRootPath: params.appResults.appRootPath,
2244
+ ...(appPath ? { appPath } : {}),
2245
+ ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
2114
2246
  appPort,
2115
- storagePath,
2247
+ ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
2248
+ ...(appPublicPath ? { appPublicPath } : {}),
2116
2249
  ...(envFile ? { envFile } : {}),
2117
2250
  appKey: params.appResults.appKey,
2118
2251
  timezone: params.appResults.timeZone,
@@ -2135,6 +2268,7 @@ export default class Install extends Command {
2135
2268
  }
2136
2269
  async collectPromptResults(parsed, yes) {
2137
2270
  const commandArgv = this.argv ?? process.argv.slice(2);
2271
+ const defaultApiHost = await resolveDefaultApiHost();
2138
2272
  const resumePreset = await this.resolveResumePresetValues(parsed, yes);
2139
2273
  const envPreset = {
2140
2274
  ...(resumePreset?.envPreset ?? {}),
@@ -2160,6 +2294,7 @@ export default class Install extends Command {
2160
2294
  envName,
2161
2295
  flags: {
2162
2296
  ...parsed,
2297
+ 'app-path': parsed['app-path'] ?? Install.toOptionalPromptString(appPreset.appPath),
2163
2298
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2164
2299
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2165
2300
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
@@ -2229,7 +2364,7 @@ export default class Install extends Command {
2229
2364
  };
2230
2365
  const resolvedEnvAddAuthType = String(envAddPreset.authType ?? '').trim();
2231
2366
  const envAddInitialValues = {
2232
- apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2367
+ apiBaseUrl: buildInstallApiBaseUrl(appResults, defaultApiHost),
2233
2368
  ...envAddResumePreset,
2234
2369
  ...(!parsed['skip-auth'] && resolvedEnvAddAuthType === 'basic'
2235
2370
  ? {
@@ -2276,6 +2411,7 @@ export default class Install extends Command {
2276
2411
  const parsed = {
2277
2412
  ...flags,
2278
2413
  };
2414
+ const defaultApiHost = await resolveDefaultApiHost();
2279
2415
  if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2280
2416
  this.error('--skip-auth cannot be used with --access-token or --token.');
2281
2417
  }
@@ -2413,11 +2549,17 @@ export default class Install extends Command {
2413
2549
  await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2414
2550
  appResults,
2415
2551
  envAddResults,
2552
+ defaultApiHost,
2416
2553
  }), {
2417
2554
  containerName: dockerAppPlan?.containerName,
2418
2555
  verbose: parsed.verbose,
2419
2556
  });
2420
- printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2557
+ const displayApiBaseUrl = Install.resolveApiBaseUrl({
2558
+ appResults,
2559
+ envAddResults,
2560
+ defaultApiHost,
2561
+ });
2562
+ printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
2421
2563
  }
2422
2564
  if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2423
2565
  await this.saveInstalledEnv({
@@ -2435,6 +2577,7 @@ export default class Install extends Command {
2435
2577
  appReady: Boolean(dockerAppPlan || localAppPlan),
2436
2578
  skipAuth: Boolean(parsed['skip-auth']),
2437
2579
  });
2580
+ await clearEnvRootSetup(envName, { scope: resolveDefaultConfigScope() });
2438
2581
  if (!dockerAppPlan && !localAppPlan) {
2439
2582
  printInfo(`Install config for "${envName}" has been saved.`);
2440
2583
  }