@nocobase/cli 2.2.0-alpha.1 → 2.2.0-alpha.10

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 (78) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  3. package/bin/early-locale.js +89 -0
  4. package/bin/node-version.js +35 -0
  5. package/bin/run.js +9 -0
  6. package/bin/windows-admin.js +60 -0
  7. package/dist/commands/app/destroy.js +4 -3
  8. package/dist/commands/app/restart.js +38 -0
  9. package/dist/commands/app/shared.js +49 -3
  10. package/dist/commands/app/start.js +95 -0
  11. package/dist/commands/app/upgrade.js +11 -0
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/examples/prompts-stages.js +2 -2
  15. package/dist/commands/examples/prompts-test.js +2 -2
  16. package/dist/commands/init.js +152 -14
  17. package/dist/commands/install.js +256 -109
  18. package/dist/commands/license/activate.js +4 -1
  19. package/dist/commands/license/shared.js +24 -15
  20. package/dist/commands/portal/config.js +88 -0
  21. package/dist/commands/portal/create.js +104 -0
  22. package/dist/commands/portal/deploy.js +81 -0
  23. package/dist/commands/portal/destroy.js +104 -0
  24. package/dist/commands/portal/dev.js +71 -0
  25. package/dist/commands/portal/index.js +20 -0
  26. package/dist/commands/portal/info.js +82 -0
  27. package/dist/commands/portal/list.js +98 -0
  28. package/dist/commands/portal/pull.js +84 -0
  29. package/dist/commands/portal/push.js +79 -0
  30. package/dist/commands/proxy/caddy/generate.js +93 -7
  31. package/dist/commands/proxy/nginx/generate.js +98 -7
  32. package/dist/commands/revision/create.js +1 -1
  33. package/dist/commands/self/check.js +1 -1
  34. package/dist/commands/self/update.js +4 -4
  35. package/dist/commands/skills/check.js +4 -5
  36. package/dist/commands/skills/install.js +18 -1
  37. package/dist/commands/skills/update.js +19 -4
  38. package/dist/commands/source/dev.js +10 -6
  39. package/dist/commands/source/download.js +85 -16
  40. package/dist/lib/api-command-compat.js +51 -8
  41. package/dist/lib/app-managed-resources.js +104 -5
  42. package/dist/lib/auth-store.js +105 -13
  43. package/dist/lib/cli-config.js +93 -2
  44. package/dist/lib/docker-image.js +94 -6
  45. package/dist/lib/env-auth.js +291 -45
  46. package/dist/lib/env-config.js +14 -0
  47. package/dist/lib/env-proxy-config.js +48 -0
  48. package/dist/lib/env-proxy.js +266 -61
  49. package/dist/lib/hook-script.js +160 -0
  50. package/dist/lib/managed-init-env.js +6 -1
  51. package/dist/lib/portal-command-env.js +31 -0
  52. package/dist/lib/portal-config.js +133 -0
  53. package/dist/lib/portal-configure.js +117 -0
  54. package/dist/lib/portal-create.js +433 -0
  55. package/dist/lib/portal-deploy.js +283 -0
  56. package/dist/lib/portal-destroy.js +100 -0
  57. package/dist/lib/portal-dev.js +79 -0
  58. package/dist/lib/portal-env-files.js +53 -0
  59. package/dist/lib/portal-info.js +31 -0
  60. package/dist/lib/portal-list.js +211 -0
  61. package/dist/lib/portal-source.js +523 -0
  62. package/dist/lib/prompt-catalog-terminal.js +32 -19
  63. package/dist/lib/prompt-validators.js +1 -1
  64. package/dist/lib/prompt-web-ui.js +20 -13
  65. package/dist/lib/proxy-caddy.js +77 -9
  66. package/dist/lib/proxy-nginx.js +71 -11
  67. package/dist/lib/run-npm.js +21 -16
  68. package/dist/lib/self-manager.js +254 -46
  69. package/dist/lib/skills-manager.js +116 -23
  70. package/dist/lib/source-publish.js +2 -2
  71. package/dist/lib/startup-update.js +1 -1
  72. package/dist/lib/ui.js +28 -1
  73. package/dist/locale/en-US.json +227 -43
  74. package/dist/locale/zh-CN.json +227 -43
  75. package/package.json +11 -2
  76. package/scripts/build.mjs +0 -34
  77. package/scripts/clean.mjs +0 -9
  78. package/tsconfig.json +0 -19
@@ -13,11 +13,11 @@ import path from 'node:path';
13
13
  import { exit } from 'node:process';
14
14
  import { appendAppPublicPath } from '../lib/app-public-path.js';
15
15
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
16
- import { applyCliLocale, localeText, resolveCliLocale, translateCli } from "../lib/cli-locale.js";
16
+ import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
17
17
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
18
- import { defaultDockerContainerPrefix, defaultDockerNetworkName } from '../lib/app-runtime.js';
19
- import { resolveDefaultApiHost, resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
20
- import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../lib/docker-image.js";
18
+ import { defaultDockerContainerPrefix, defaultDockerNetworkName, managedAppLifecycleEnvVars, } from '../lib/app-runtime.js';
19
+ import { getCliConfigValue, resolveDefaultApiHost, resolveDockerContainerPrefix, resolveDockerNetworkName, } from '../lib/cli-config.js';
20
+ import { DEFAULT_DOCKER_VERSION, DEFAULT_NB_IMAGE_VARIANT, inferNbImageRegistryFromRepository, normalizeNbImageVariant, resolveBuiltinDbImage, resolveDockerImageContainerPort, resolveDockerImageRef, resolveOfficialDockerRegistry, } from "../lib/docker-image.js";
21
21
  import { findAvailableTcpPort, validateAppPublicPath, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
22
22
  import { validateExternalDbConfig, validateMysqlLowerCaseTableNamesCompatibility } from "../lib/db-connection-check.js";
23
23
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
@@ -29,8 +29,9 @@ import { buildStoredEnvConfig } from '../lib/env-config.js';
29
29
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
30
30
  import { startDockerLogFollower } from '../lib/docker-log-stream.js';
31
31
  import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
32
+ import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
32
33
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
33
- import Download, { defaultDockerRegistryForLang } from './download.js';
34
+ import Download from './download.js';
34
35
  import EnvAdd from "./env/add.js";
35
36
  import { resolveAppUrlFromApiBaseUrl } from "./env/shared.js";
36
37
  const DEFAULT_INSTALL_ENV_NAME = 'local';
@@ -44,18 +45,6 @@ const DEFAULT_INSTALL_DB_PORTS = {
44
45
  mariadb: '3306',
45
46
  kingbase: '54321',
46
47
  };
47
- const DEFAULT_INSTALL_BUILTIN_DB_IMAGES = {
48
- postgres: 'postgres:16',
49
- mysql: 'mysql:8',
50
- mariadb: 'mariadb:11',
51
- kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
52
- };
53
- const DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN = {
54
- postgres: 'registry.cn-shanghai.aliyuncs.com/nocobase/postgres:16',
55
- mysql: 'registry.cn-shanghai.aliyuncs.com/nocobase/mysql:8',
56
- mariadb: 'registry.cn-shanghai.aliyuncs.com/nocobase/mariadb:11',
57
- kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
58
- };
59
48
  const DEFAULT_INSTALL_DB_DATABASE = 'nocobase';
60
49
  const DEFAULT_INSTALL_DB_USER = 'nocobase';
61
50
  const DEFAULT_INSTALL_DB_PASSWORD = 'nocobase';
@@ -64,6 +53,10 @@ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
64
53
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
65
54
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
66
55
  const DEFAULT_INSTALL_API_HOST = '127.0.0.1';
56
+ const DEFAULT_INSTALL_PORTAL_TYPE = 'no-code';
57
+ const DEFAULT_INSTALL_PORTAL_NAME = 'admin';
58
+ const DEFAULT_INSTALL_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
59
+ const INSTALL_PORTAL_TYPES = ['no-code', 'ai'];
67
60
  function toOptionalPromptString(value) {
68
61
  const text = String(value ?? '').trim();
69
62
  return text || undefined;
@@ -178,22 +171,26 @@ function downloadVersionPromptValue(version) {
178
171
  }
179
172
  function supportsBuiltinDbDialect(value) {
180
173
  const dialect = String(value ?? '').trim();
181
- return Object.prototype.hasOwnProperty.call(DEFAULT_INSTALL_BUILTIN_DB_IMAGES, dialect);
174
+ return INSTALL_DB_DIALECTS.includes(dialect);
182
175
  }
183
176
  export function defaultDbPortForDialect(value) {
184
177
  const dialect = String(value ?? 'postgres').trim();
185
178
  return DEFAULT_INSTALL_DB_PORTS[isInstallDbDialect(dialect) ? dialect : 'postgres'];
186
179
  }
187
- function defaultBuiltinDbImageForDialect(value) {
180
+ function defaultBuiltinDbImageForDialect(value, options) {
188
181
  const dialect = String(value ?? 'postgres').trim();
189
- const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
190
- ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN
191
- : DEFAULT_INSTALL_BUILTIN_DB_IMAGES;
192
- return supportsBuiltinDbDialect(dialect) ? defaults[dialect] : defaults.postgres;
182
+ return resolveBuiltinDbImage(dialect, { registry: options?.registry });
193
183
  }
194
184
  function defaultDbDatabaseForDialect(value) {
195
185
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
196
186
  }
187
+ function isAiMode(values) {
188
+ return String(values.portalType ?? DEFAULT_INSTALL_PORTAL_TYPE).trim() === 'ai';
189
+ }
190
+ function supportsDbSchemaPrompt(value) {
191
+ const dialect = String(value ?? '').trim();
192
+ return dialect === 'postgres' || dialect === 'kingbase';
193
+ }
197
194
  function defaultDbHostForBuiltinDb(values) {
198
195
  return values.builtinDb ? DEFAULT_INSTALL_BUILTIN_DB_HOST : DEFAULT_INSTALL_DB_HOST;
199
196
  }
@@ -414,6 +411,16 @@ export default class Install extends Command {
414
411
  'app-public-path': Flags.string({
415
412
  description: 'Public path for the local app, for example / or /console/',
416
413
  }),
414
+ 'portal-type': Flags.string({
415
+ description: 'Initial portal type for the installed app',
416
+ options: [...INSTALL_PORTAL_TYPES],
417
+ }),
418
+ 'portal-name': Flags.string({
419
+ description: 'Initial portal name',
420
+ }),
421
+ 'portal-template': Flags.string({
422
+ description: 'Initial portal template npm package or local path when --portal-type ai is used',
423
+ }),
417
424
  'root-username': Flags.string({
418
425
  description: 'Initial admin username for the installed app',
419
426
  required: false,
@@ -472,7 +479,10 @@ export default class Install extends Command {
472
479
  description: 'Skip the download step and reuse an existing local app directory or Docker image',
473
480
  default: false,
474
481
  }),
475
- ...omitKeys(Download.flags, ['yes']),
482
+ 'hook-script': Flags.string({
483
+ description: 'Hook module copied to <appPath>/.nb/hooks.mjs and run before npm/git dependency installation.',
484
+ }),
485
+ ...omitKeys(Download.flags, ['yes', 'hook-script']),
476
486
  };
477
487
  /** Environment name only: run before {@link Install.prompts} (see `run`). */
478
488
  static envPrompts = {
@@ -520,6 +530,41 @@ export default class Install extends Command {
520
530
  yesInitialValue: '/',
521
531
  validate: validateAppPublicPath,
522
532
  },
533
+ portalType: {
534
+ type: 'select',
535
+ message: installText('prompts.portalType.message'),
536
+ options: [
537
+ {
538
+ value: 'no-code',
539
+ label: installText('prompts.portalType.noCodeLabel'),
540
+ hint: installText('prompts.portalType.noCodeHint'),
541
+ },
542
+ {
543
+ value: 'ai',
544
+ label: installText('prompts.portalType.aiLabel'),
545
+ hint: installText('prompts.portalType.aiHint'),
546
+ },
547
+ ],
548
+ initialValue: DEFAULT_INSTALL_PORTAL_TYPE,
549
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TYPE,
550
+ required: true,
551
+ },
552
+ portalName: {
553
+ type: 'text',
554
+ message: installText('prompts.portalName.message'),
555
+ placeholder: DEFAULT_INSTALL_PORTAL_NAME,
556
+ initialValue: DEFAULT_INSTALL_PORTAL_NAME,
557
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_NAME,
558
+ required: true,
559
+ },
560
+ portalTemplate: {
561
+ type: 'text',
562
+ message: installText('prompts.portalTemplate.message'),
563
+ placeholder: DEFAULT_INSTALL_PORTAL_TEMPLATE,
564
+ yesInitialValue: DEFAULT_INSTALL_PORTAL_TEMPLATE,
565
+ hidden: (values) => !isAiMode(values),
566
+ required: true,
567
+ },
523
568
  };
524
569
  }
525
570
  static dbPrompts = {
@@ -547,7 +592,9 @@ export default class Install extends Command {
547
592
  type: 'text',
548
593
  message: installText('prompts.builtinDbImage.message'),
549
594
  placeholder: installText('prompts.builtinDbImage.placeholder'),
550
- initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
595
+ initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect, {
596
+ registry: String(values.builtinDbImageRegistry ?? '').trim() || undefined,
597
+ }),
551
598
  hidden: (values) => !values.builtinDb || !supportsBuiltinDbDialect(values.dbDialect),
552
599
  required: true,
553
600
  },
@@ -597,7 +644,7 @@ export default class Install extends Command {
597
644
  type: 'text',
598
645
  message: installText('prompts.dbSchema.message'),
599
646
  placeholder: installText('prompts.dbSchema.placeholder'),
600
- hidden: (values) => String(values.dbDialect ?? '').trim() !== 'postgres',
647
+ hidden: (values) => !supportsDbSchemaPrompt(values.dbDialect),
601
648
  },
602
649
  dbTablePrefix: {
603
650
  type: 'text',
@@ -777,6 +824,24 @@ export default class Install extends Command {
777
824
  preset.appPublicPath = v;
778
825
  }
779
826
  }
827
+ if (flags['portal-type'] !== undefined) {
828
+ const v = String(flags['portal-type'] ?? '').trim();
829
+ if (v) {
830
+ preset.portalType = v;
831
+ }
832
+ }
833
+ if (flags['portal-name'] !== undefined) {
834
+ const v = String(flags['portal-name'] ?? '').trim();
835
+ if (v) {
836
+ preset.portalName = v;
837
+ }
838
+ }
839
+ if (flags['portal-template'] !== undefined) {
840
+ const v = String(flags['portal-template'] ?? '').trim();
841
+ if (v) {
842
+ preset.portalTemplate = v;
843
+ }
844
+ }
780
845
  if (flags['root-username'] !== undefined) {
781
846
  preset.rootUsername = String(flags['root-username'] ?? '').trim();
782
847
  }
@@ -868,6 +933,9 @@ export default class Install extends Command {
868
933
  'appPort',
869
934
  'storagePath',
870
935
  'appPublicPath',
936
+ 'portalType',
937
+ 'portalName',
938
+ 'portalTemplate',
871
939
  ]);
872
940
  }
873
941
  static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
@@ -1095,6 +1163,7 @@ export default class Install extends Command {
1095
1163
  const appPort = Install.toOptionalPromptString(config.appPort);
1096
1164
  const storagePath = Install.toOptionalPromptString(config.storagePath);
1097
1165
  const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
1166
+ const hookScript = Install.toOptionalPromptString(config.hookScript);
1098
1167
  const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
1099
1168
  const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
1100
1169
  const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
@@ -1116,6 +1185,9 @@ export default class Install extends Command {
1116
1185
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
1117
1186
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
1118
1187
  const lang = Install.toOptionalPromptString(config.lang);
1188
+ const portalType = Install.toOptionalPromptString(config.portalType);
1189
+ const portalName = Install.toOptionalPromptString(config.portalName);
1190
+ const portalTemplate = Install.toOptionalPromptString(config.portalTemplate);
1119
1191
  const auth = config.auth;
1120
1192
  const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
1121
1193
  const appPreset = {
@@ -1125,6 +1197,10 @@ export default class Install extends Command {
1125
1197
  ...(appPort ? { appPort } : {}),
1126
1198
  ...(storagePath ? { storagePath } : {}),
1127
1199
  ...(appPublicPath ? { appPublicPath } : {}),
1200
+ ...(portalType ? { portalType } : {}),
1201
+ ...(portalName ? { portalName } : {}),
1202
+ ...(portalTemplate ? { portalTemplate } : {}),
1203
+ ...(hookScript ? { hookScript } : {}),
1128
1204
  };
1129
1205
  const downloadPreset = {
1130
1206
  ...(source ? { source } : {}),
@@ -1266,6 +1342,12 @@ export default class Install extends Command {
1266
1342
  warn: params.warnOnPortFallback ?? true,
1267
1343
  });
1268
1344
  }
1345
+ if (params.flags['portal-template'] === undefined) {
1346
+ const defaultPortalTemplate = Install.toOptionalPromptString(await getCliConfigValue('default-portal-template'));
1347
+ if (defaultPortalTemplate) {
1348
+ initialValues.portalTemplate = defaultPortalTemplate;
1349
+ }
1350
+ }
1269
1351
  return initialValues;
1270
1352
  }
1271
1353
  static shouldPublishBuiltinDbPortForValues(values) {
@@ -1273,19 +1355,25 @@ export default class Install extends Command {
1273
1355
  return builtinDb && Install.shouldPublishBuiltinDbPort(values.source);
1274
1356
  }
1275
1357
  static async buildDbPromptInitialValues(params) {
1276
- if (params.flags['db-port'] !== undefined) {
1277
- return {};
1278
- }
1358
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
1279
1359
  const values = {
1280
1360
  ...params.downloadResults,
1281
1361
  ...params.dbPreset,
1282
1362
  };
1363
+ const dockerRegistry = String(values.dockerRegistry ?? '').trim() || resolveOfficialDockerRegistry(configuredRegistry);
1364
+ const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
1365
+ const initialValues = values.builtinDb !== false && params.dbPreset.builtinDbImage === undefined
1366
+ ? { builtinDbImage: defaultBuiltinDbImageForDialect(dialect, { registry: dockerRegistry }) }
1367
+ : {};
1368
+ if (params.flags['db-port'] !== undefined) {
1369
+ return initialValues;
1370
+ }
1283
1371
  if (!Install.shouldPublishBuiltinDbPortForValues(values)) {
1284
- return {};
1372
+ return initialValues;
1285
1373
  }
1286
- const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
1287
1374
  const defaultPort = defaultDbPortForDialect(dialect);
1288
1375
  return {
1376
+ ...initialValues,
1289
1377
  dbPort: await Install.resolveAvailableDefaultPort(defaultPort, {
1290
1378
  label: `Default ${dialect} port`,
1291
1379
  warn: params.warnOnPortFallback ?? true,
@@ -1296,12 +1384,13 @@ export default class Install extends Command {
1296
1384
  * When install runs {@link Download.prompts} after app prompts, align the download
1297
1385
  * output directory with app settings, while Docker registry defaults follow the CLI locale.
1298
1386
  */
1299
- static buildDownloadPromptOptionsForInstall(appResults, envName) {
1387
+ static async buildDownloadPromptOptionsForInstall(appResults, envName) {
1300
1388
  const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1301
1389
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1390
+ const dockerRegistry = resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
1302
1391
  const initialValues = {
1303
1392
  lang,
1304
- dockerRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1393
+ dockerRegistry,
1305
1394
  outputDir: appRoot,
1306
1395
  };
1307
1396
  const values = {
@@ -1426,14 +1515,17 @@ export default class Install extends Command {
1426
1515
  static buildDockerAppContainerName(envName, containerPrefix) {
1427
1516
  return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1428
1517
  }
1429
- static buildInitAppEnvVars(params) {
1518
+ static buildInitAppEnvVars(params, options = {}) {
1430
1519
  return buildInitAppEnvVarsFromConfig({
1431
1520
  lang: String(params.appResults.lang ?? ''),
1432
1521
  rootUsername: String(params.rootResults.rootUsername ?? ''),
1433
1522
  rootEmail: String(params.rootResults.rootEmail ?? ''),
1434
1523
  rootPassword: String(params.rootResults.rootPassword ?? ''),
1435
1524
  rootNickname: String(params.rootResults.rootNickname ?? ''),
1436
- });
1525
+ portalType: String(params.appResults.portalType ?? ''),
1526
+ portalName: String(params.appResults.portalName ?? ''),
1527
+ portalTemplate: String(params.appResults.portalTemplate ?? ''),
1528
+ }, options);
1437
1529
  }
1438
1530
  static shouldPublishBuiltinDbPort(source) {
1439
1531
  return String(source ?? '').trim() !== 'docker';
@@ -1462,8 +1554,6 @@ export default class Install extends Command {
1462
1554
  '-d',
1463
1555
  '--name',
1464
1556
  containerName,
1465
- '--restart',
1466
- 'always',
1467
1557
  '--network',
1468
1558
  networkName,
1469
1559
  '-e',
@@ -1506,8 +1596,6 @@ export default class Install extends Command {
1506
1596
  '-d',
1507
1597
  '--name',
1508
1598
  containerName,
1509
- '--restart',
1510
- 'always',
1511
1599
  '--network',
1512
1600
  networkName,
1513
1601
  '-e',
@@ -1552,8 +1640,6 @@ export default class Install extends Command {
1552
1640
  '-d',
1553
1641
  '--name',
1554
1642
  containerName,
1555
- '--restart',
1556
- 'always',
1557
1643
  '--network',
1558
1644
  networkName,
1559
1645
  '-e',
@@ -1598,8 +1684,6 @@ export default class Install extends Command {
1598
1684
  '-d',
1599
1685
  '--name',
1600
1686
  containerName,
1601
- '--restart',
1602
- 'always',
1603
1687
  '--network',
1604
1688
  networkName,
1605
1689
  '--platform',
@@ -1762,12 +1846,15 @@ export default class Install extends Command {
1762
1846
  return plan;
1763
1847
  }
1764
1848
  static async buildDockerAppPlan(params) {
1849
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
1850
+ const configuredVariant = normalizeNbImageVariant(await getCliConfigValue('nb-image-variant')) ?? DEFAULT_NB_IMAGE_VARIANT;
1765
1851
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1766
- defaultDockerRegistryForLang(process.env.NB_LOCALE);
1852
+ resolveOfficialDockerRegistry(configuredRegistry);
1767
1853
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1768
1854
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1769
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1855
+ defaultRegistry: resolveOfficialDockerRegistry(configuredRegistry),
1770
1856
  defaultVersion: DEFAULT_DOCKER_VERSION,
1857
+ variant: inferNbImageRegistryFromRepository(dockerRegistry) ? configuredVariant : undefined,
1771
1858
  });
1772
1859
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1773
1860
  const configuredStoragePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
@@ -1794,17 +1881,16 @@ export default class Install extends Command {
1794
1881
  appResults: params.appResults,
1795
1882
  rootResults: params.rootResults,
1796
1883
  });
1884
+ const containerPort = resolveDockerImageContainerPort(imageRef);
1797
1885
  const args = [
1798
1886
  'run',
1799
1887
  '-d',
1800
1888
  '--name',
1801
1889
  containerName,
1802
- '--restart',
1803
- 'always',
1804
1890
  '--network',
1805
1891
  params.networkName,
1806
1892
  '-p',
1807
- `${appPort}:80`,
1893
+ `${appPort}:${containerPort}`,
1808
1894
  ];
1809
1895
  if (envFile) {
1810
1896
  args.push('--env-file', envFile);
@@ -1916,6 +2002,12 @@ export default class Install extends Command {
1916
2002
  if (results.buildDts) {
1917
2003
  argv.push('--build-dts');
1918
2004
  }
2005
+ Install.pushDownloadArgIfValue(argv, '--hook-script', results.hookScript);
2006
+ Install.pushDownloadArgIfValue(argv, '--hook-phase', results.hookPhase);
2007
+ Install.pushDownloadArgIfValue(argv, '--hook-command', results.hookCommand);
2008
+ Install.pushDownloadArgIfValue(argv, '--hook-env-name', results.hookEnvName);
2009
+ Install.pushDownloadArgIfValue(argv, '--hook-app-path', results.hookAppPath);
2010
+ Install.pushDownloadArgIfValue(argv, '--hook-storage-path', results.hookStoragePath);
1919
2011
  return argv;
1920
2012
  }
1921
2013
  static resolveLocalProjectRoot(params) {
@@ -1942,6 +2034,40 @@ export default class Install extends Command {
1942
2034
  buildDts: false,
1943
2035
  };
1944
2036
  }
2037
+ static resolveAbsoluteAppPath(envName, appResults) {
2038
+ const configuredAppPath = resolveConfiguredAppPathValue(appResults) ?? defaultInstallAppPath(envName);
2039
+ return resolveConfiguredEnvPath(configuredAppPath) ?? resolveEnvRelativePath(defaultInstallAppPath(envName));
2040
+ }
2041
+ static resolveAbsoluteStoragePath(envName, appResults) {
2042
+ const configuredStoragePath = resolveConfiguredStoragePathValue(appResults, envName);
2043
+ return (resolveConfiguredEnvPath(configuredStoragePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(envName)));
2044
+ }
2045
+ async prepareHookScriptForInstall(params) {
2046
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2047
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
2048
+ const inputHookScript = Install.toOptionalPromptString(params.hookScript);
2049
+ if (inputHookScript) {
2050
+ params.appResults.hookScript = await persistHookScript({
2051
+ sourcePath: inputHookScript,
2052
+ appPath,
2053
+ });
2054
+ printInfo(`Saved hook script to ${path.join(appPath, String(params.appResults.hookScript))}.`);
2055
+ }
2056
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
2057
+ const hookScriptPath = resolveHookScriptPath({
2058
+ appPath,
2059
+ hookScript: savedHookScript,
2060
+ });
2061
+ if (!hookScriptPath) {
2062
+ return;
2063
+ }
2064
+ params.downloadResults.hookScript = hookScriptPath;
2065
+ params.downloadResults.hookPhase = 'init';
2066
+ params.downloadResults.hookCommand = 'init';
2067
+ params.downloadResults.hookEnvName = params.envName;
2068
+ params.downloadResults.hookAppPath = appPath;
2069
+ params.downloadResults.hookStoragePath = storagePath;
2070
+ }
1945
2071
  commandStdio(verbose) {
1946
2072
  return verbose ? 'inherit' : 'ignore';
1947
2073
  }
@@ -1954,12 +2080,15 @@ export default class Install extends Command {
1954
2080
  }
1955
2081
  async ensureSkippedDownloadInputsReady(params) {
1956
2082
  if (params.source === 'docker') {
2083
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
2084
+ const configuredVariant = normalizeNbImageVariant(await getCliConfigValue('nb-image-variant')) ?? DEFAULT_NB_IMAGE_VARIANT;
1957
2085
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1958
- defaultDockerRegistryForLang(process.env.NB_LOCALE);
2086
+ resolveOfficialDockerRegistry(configuredRegistry);
1959
2087
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1960
2088
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1961
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
2089
+ defaultRegistry: resolveOfficialDockerRegistry(configuredRegistry),
1962
2090
  defaultVersion: DEFAULT_DOCKER_VERSION,
2091
+ variant: inferNbImageRegistryFromRepository(dockerRegistry) ? configuredVariant : undefined,
1963
2092
  });
1964
2093
  const imageExists = await commandSucceeds('docker', ['image', 'inspect', imageRef]);
1965
2094
  if (!imageExists) {
@@ -2011,7 +2140,9 @@ export default class Install extends Command {
2011
2140
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
2012
2141
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
2013
2142
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
2143
+ const lifecycleEnvVars = managedAppLifecycleEnvVars();
2014
2144
  const env = {
2145
+ ...lifecycleEnvVars,
2015
2146
  STORAGE_PATH: storagePath,
2016
2147
  APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
2017
2148
  APP_KEY: appKey,
@@ -2216,11 +2347,59 @@ export default class Install extends Command {
2216
2347
  }
2217
2348
  await this.config.runCommand('env:update', [params.envName]);
2218
2349
  }
2350
+ buildAppStartArgv(params) {
2351
+ const argv = ['--env', params.envName, '--yes', '--no-sync-licensed-plugins', '--hook-command', 'init'];
2352
+ if (params.verbose) {
2353
+ argv.push('--verbose');
2354
+ }
2355
+ return argv;
2356
+ }
2357
+ async runInstallHookIfNeeded(params) {
2358
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2359
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
2360
+ const hookScriptPath = resolveHookScriptPath({
2361
+ appPath,
2362
+ hookScript: savedHookScript,
2363
+ });
2364
+ if (!hookScriptPath) {
2365
+ return;
2366
+ }
2367
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
2368
+ const sourcePath = params.projectRoot ??
2369
+ resolveConfiguredEnvPath(resolveConfiguredSourcePathValue(params.appResults, params.envName)) ??
2370
+ path.join(appPath, 'source');
2371
+ const context = buildHookContext({
2372
+ phase: 'init',
2373
+ command: 'init',
2374
+ envName: params.envName,
2375
+ source: params.source,
2376
+ version: downloadResultsValue(params.downloadResults, 'version'),
2377
+ appPath,
2378
+ sourcePath,
2379
+ storagePath,
2380
+ hookScript: savedHookScript,
2381
+ envConfig: Install.buildSavedEnvConfig(params, {
2382
+ defaultApiHost: params.defaultApiHost,
2383
+ }),
2384
+ });
2385
+ if (!context) {
2386
+ return;
2387
+ }
2388
+ this.log(`Running hook ${params.hookName}: ${hookScriptPath}`);
2389
+ await runHookScriptHook({
2390
+ hookScriptPath,
2391
+ hookName: params.hookName,
2392
+ context,
2393
+ });
2394
+ }
2219
2395
  static buildSavedEnvConfig(params, options = {}) {
2220
2396
  const appPath = resolveConfiguredAppPathValue(params.appResults);
2221
2397
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
2222
2398
  const storagePath = Install.toOptionalPromptString(params.appResults.storagePath);
2223
2399
  const appPublicPath = Install.toOptionalPromptString(params.appResults.appPublicPath);
2400
+ const portalType = Install.toOptionalPromptString(params.appResults.portalType);
2401
+ const portalName = Install.toOptionalPromptString(params.appResults.portalName);
2402
+ const portalTemplate = Install.toOptionalPromptString(params.appResults.portalTemplate);
2224
2403
  const derivedAppRootPath = appPath ? deriveConfiguredSourcePath(appPath) : undefined;
2225
2404
  const derivedStoragePath = appPath ? deriveConfiguredStoragePath(appPath) : undefined;
2226
2405
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
@@ -2247,6 +2426,7 @@ export default class Install extends Command {
2247
2426
  devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2248
2427
  build: downloadResultsValue(params.downloadResults, 'build'),
2249
2428
  buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2429
+ hookScript: params.appResults.hookScript,
2250
2430
  ...(appPath ? { appPath } : {}),
2251
2431
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
2252
2432
  appPort,
@@ -2254,6 +2434,9 @@ export default class Install extends Command {
2254
2434
  ...(appPublicPath ? { appPublicPath } : {}),
2255
2435
  ...(envFile ? { envFile } : {}),
2256
2436
  lang: params.appResults.lang,
2437
+ portalType,
2438
+ portalName,
2439
+ portalTemplate,
2257
2440
  appKey: params.appResults.appKey,
2258
2441
  timezone: params.appResults.timeZone,
2259
2442
  builtinDb: params.dbResults.builtinDb,
@@ -2305,13 +2488,17 @@ export default class Install extends Command {
2305
2488
  'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2306
2489
  'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2307
2490
  'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2491
+ 'portal-template': parsed['portal-template'] ?? Install.toOptionalPromptString(appPreset.portalTemplate),
2308
2492
  },
2309
2493
  }),
2310
2494
  values: appPreset,
2311
2495
  yesInitialValues: { resume: parsed.resume },
2312
2496
  yes,
2313
2497
  });
2314
- const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2498
+ if (resumePreset?.appPreset?.hookScript !== undefined && appResults.hookScript === undefined) {
2499
+ appResults.hookScript = resumePreset.appPreset.hookScript;
2500
+ }
2501
+ const downloadOpts = await Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2315
2502
  downloadOpts.values = {
2316
2503
  ...(resumePreset?.downloadPreset ?? {}),
2317
2504
  ...downloadOpts.values,
@@ -2418,7 +2605,6 @@ export default class Install extends Command {
2418
2605
  const parsed = {
2419
2606
  ...flags,
2420
2607
  };
2421
- const defaultApiHost = await resolveDefaultApiHost();
2422
2608
  if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2423
2609
  this.error('--skip-auth cannot be used with --access-token or --token.');
2424
2610
  }
@@ -2440,6 +2626,12 @@ export default class Install extends Command {
2440
2626
  appResults,
2441
2627
  });
2442
2628
  appResults.setupState = 'prepared';
2629
+ await this.prepareHookScriptForInstall({
2630
+ envName,
2631
+ appResults,
2632
+ downloadResults,
2633
+ hookScript: parsed['hook-script'],
2634
+ });
2443
2635
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2444
2636
  const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2445
2637
  const dockerNetworkName = usesDockerResources
@@ -2492,8 +2684,7 @@ export default class Install extends Command {
2492
2684
  dbResults.dbUser = builtinDbPlan.dbUser;
2493
2685
  dbResults.dbPassword = builtinDbPlan.dbPassword;
2494
2686
  }
2495
- let dockerAppPlan;
2496
- let localAppPlan;
2687
+ let shouldStartApp = false;
2497
2688
  if (source === 'docker' || source === 'npm' || source === 'git') {
2498
2689
  this.logStage('Preparing application');
2499
2690
  if (source === 'docker') {
@@ -2505,76 +2696,28 @@ export default class Install extends Command {
2505
2696
  printInfo('Application image ready.');
2506
2697
  }
2507
2698
  if (!parsed['prepare-only']) {
2508
- dockerAppPlan = await this.installDockerApp({
2509
- envName,
2510
- dockerNetworkName,
2511
- dockerContainerPrefix,
2512
- appResults,
2513
- downloadResults,
2514
- dbResults,
2515
- rootResults,
2516
- builtinDbPlan,
2517
- force: parsed.force,
2518
- commandStdio,
2519
- });
2520
- appResults.appKey = dockerAppPlan.appKey;
2521
- appResults.timeZone = dockerAppPlan.timeZone;
2699
+ shouldStartApp = true;
2522
2700
  }
2523
2701
  }
2524
2702
  else if (source === 'npm' || source === 'git') {
2525
- const localSource = source === 'npm' ? 'npm' : 'git';
2526
- const projectRoot = parsed['skip-download']
2527
- ? Install.resolveLocalProjectRoot({
2528
- envName,
2529
- appResults,
2530
- downloadResults,
2531
- })
2532
- : await this.downloadLocalApp({
2703
+ if (!parsed['skip-download'] && !parsed['prepare-only']) {
2704
+ await this.downloadLocalApp({
2533
2705
  envName,
2534
2706
  appResults,
2535
2707
  downloadResults,
2536
2708
  verbose: parsed.verbose,
2537
2709
  });
2538
- if (!parsed['skip-download']) {
2539
2710
  printInfo('Application files ready.');
2540
2711
  }
2541
2712
  if (!parsed['prepare-only']) {
2542
- localAppPlan = await this.startLocalApp({
2543
- envName,
2544
- source: localSource,
2545
- projectRoot,
2546
- appResults,
2547
- dbResults,
2548
- rootResults,
2549
- commandStdio,
2550
- });
2551
- appResults.appKey = localAppPlan.appKey;
2552
- appResults.timeZone = localAppPlan.timeZone;
2713
+ shouldStartApp = true;
2553
2714
  }
2554
2715
  }
2555
2716
  }
2556
2717
  else {
2557
2718
  this.logDetail('Skipped app download and install.');
2558
2719
  }
2559
- if (dockerAppPlan || localAppPlan) {
2560
- this.logStage('Starting NocoBase');
2561
- await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2562
- appResults,
2563
- envAddResults,
2564
- defaultApiHost,
2565
- }), {
2566
- containerName: dockerAppPlan?.containerName,
2567
- verbose: parsed.verbose,
2568
- });
2569
- const displayApiBaseUrl = Install.resolveApiBaseUrl({
2570
- appResults,
2571
- envAddResults,
2572
- defaultApiHost,
2573
- });
2574
- printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
2575
- appResults.setupState = 'installed';
2576
- }
2577
- if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2720
+ if (shouldStartApp || builtinDbPlan) {
2578
2721
  await this.saveInstalledEnv({
2579
2722
  envName,
2580
2723
  appResults,
@@ -2584,10 +2727,14 @@ export default class Install extends Command {
2584
2727
  envAddResults,
2585
2728
  });
2586
2729
  }
2730
+ if (shouldStartApp) {
2731
+ this.logStage('Starting NocoBase');
2732
+ await this.config.runCommand('app:start', this.buildAppStartArgv({ envName, verbose: parsed.verbose }));
2733
+ }
2587
2734
  await this.syncInstalledEnvConnection({
2588
2735
  envName,
2589
2736
  envAddResults,
2590
- appReady: Boolean(dockerAppPlan || localAppPlan),
2737
+ appReady: shouldStartApp,
2591
2738
  skipAuth: Boolean(parsed['skip-auth']),
2592
2739
  });
2593
2740
  if (!parsed['prepare-only']) {
@@ -2596,7 +2743,7 @@ export default class Install extends Command {
2596
2743
  if (parsed['prepare-only']) {
2597
2744
  printInfo(`Preparation complete for "${envName}". Activate the license, then run \`nb app start --env ${envName}\`.`);
2598
2745
  }
2599
- else if (!dockerAppPlan && !localAppPlan) {
2746
+ else if (!shouldStartApp) {
2600
2747
  printInfo(`Install config for "${envName}" has been saved.`);
2601
2748
  }
2602
2749
  }