@nocobase/cli 2.2.0-beta.1 → 2.2.0-beta.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 (53) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/bin/early-locale.js +89 -0
  3. package/bin/node-version.js +35 -0
  4. package/bin/run.js +9 -0
  5. package/bin/windows-admin.js +60 -0
  6. package/dist/commands/app/destroy.js +4 -3
  7. package/dist/commands/app/restart.js +38 -0
  8. package/dist/commands/app/shared.js +49 -3
  9. package/dist/commands/app/start.js +95 -0
  10. package/dist/commands/app/upgrade.js +11 -0
  11. package/dist/commands/env/info.js +11 -1
  12. package/dist/commands/examples/prompts-stages.js +2 -2
  13. package/dist/commands/examples/prompts-test.js +2 -2
  14. package/dist/commands/init.js +33 -11
  15. package/dist/commands/install.js +159 -107
  16. package/dist/commands/license/activate.js +4 -1
  17. package/dist/commands/license/shared.js +24 -15
  18. package/dist/commands/proxy/caddy/generate.js +93 -7
  19. package/dist/commands/proxy/nginx/generate.js +98 -7
  20. package/dist/commands/revision/create.js +1 -1
  21. package/dist/commands/self/check.js +1 -1
  22. package/dist/commands/self/update.js +4 -4
  23. package/dist/commands/skills/check.js +4 -5
  24. package/dist/commands/skills/install.js +18 -1
  25. package/dist/commands/skills/update.js +19 -4
  26. package/dist/commands/source/dev.js +9 -5
  27. package/dist/commands/source/download.js +85 -16
  28. package/dist/lib/api-command-compat.js +51 -8
  29. package/dist/lib/app-managed-resources.js +104 -5
  30. package/dist/lib/auth-store.js +102 -12
  31. package/dist/lib/cli-config.js +73 -1
  32. package/dist/lib/docker-image.js +94 -6
  33. package/dist/lib/env-auth.js +291 -45
  34. package/dist/lib/env-config.js +11 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +164 -58
  37. package/dist/lib/hook-script.js +160 -0
  38. package/dist/lib/prompt-catalog-terminal.js +32 -19
  39. package/dist/lib/prompt-validators.js +1 -1
  40. package/dist/lib/prompt-web-ui.js +20 -13
  41. package/dist/lib/proxy-caddy.js +77 -9
  42. package/dist/lib/proxy-nginx.js +71 -11
  43. package/dist/lib/run-npm.js +4 -0
  44. package/dist/lib/self-manager.js +254 -46
  45. package/dist/lib/skills-manager.js +116 -23
  46. package/dist/lib/source-publish.js +2 -2
  47. package/dist/lib/startup-update.js +1 -1
  48. package/dist/locale/en-US.json +49 -43
  49. package/dist/locale/zh-CN.json +49 -43
  50. package/package.json +8 -2
  51. package/scripts/build.mjs +0 -34
  52. package/scripts/clean.mjs +0 -9
  53. package/tsconfig.json +0 -19
@@ -15,15 +15,18 @@ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
15
15
  import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
16
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
17
  import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
- import { resolveDefaultConfigScope } from '../lib/cli-home.js';
19
- import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
18
+ import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
19
+ import { getCliConfigValue, resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
20
+ import { resolveOfficialDockerRegistry } from '../lib/docker-image.js';
20
21
  import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
21
22
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
22
23
  import { runPromptCatalogWebUI } from "../lib/prompt-web-ui.js";
23
24
  import { validateApiBaseUrl, validateEnvKey } from "../lib/prompt-validators.js";
24
25
  import { installNocoBaseSkills, isNpmRegistryUnavailable } from '../lib/skills-manager.js';
25
26
  import { omitKeys, pickKeys } from "../lib/object-utils.js";
27
+ import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
26
28
  import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
29
+ import { persistHookScript } from '../lib/hook-script.js';
27
30
  import Download from "./download.js";
28
31
  import EnvAdd from "./env/add.js";
29
32
  import Install, { defaultDbPortForDialect } from "./install.js";
@@ -246,16 +249,12 @@ function formatBrowserOpenError(error) {
246
249
  return String(error);
247
250
  }
248
251
  export default class Init extends Command {
249
- static summary = 'Set up NocoBase so coding agents can connect and work with it';
250
- static description = `Set up NocoBase for coding agents in the current workspace.
252
+ static summary = 'Set up or connect a NocoBase environment in the current workspace';
253
+ static description = `Set up NocoBase in the current workspace.
251
254
 
252
- \`nb init\` prepares a NocoBase environment that coding agents can use. It supports three setup paths:
255
+ \`nb init\` helps you install a new NocoBase app, take over managing one that already exists on this machine, or connect a remote NocoBase app and save it as a CLI env.
253
256
 
254
- - Install a new NocoBase app, then save it as a CLI env.
255
- - Take over managing an app that already exists on this machine by reusing its database.
256
- - Connect a remote NocoBase app and save it as a CLI env.
257
-
258
- It can also install NocoBase AI coding skills (\`nocobase/skills\`) so agents get the project-specific workflow guidance.
257
+ You can use the saved environment directly, or let a coding agent access it later. It can also install NocoBase AI coding skills (\`nocobase/skills\`) when you want agent-specific workflow guidance.
259
258
 
260
259
  If setup was interrupted earlier, use \`--resume\` with an existing env name to continue from the saved workspace config.
261
260
 
@@ -475,7 +474,7 @@ Prompt modes:
475
474
  default: false,
476
475
  }),
477
476
  'ui-host': Flags.string({
478
- description: 'Host for the local --ui setup server (default: 127.0.0.1)',
477
+ description: 'Browser-accessible host for the --ui setup page URL (default: 127.0.0.1)',
479
478
  }),
480
479
  'ui-port': Flags.integer({
481
480
  description: 'Port for the local --ui setup server; 0 lets the OS choose an available port',
@@ -699,6 +698,12 @@ Prompt modes:
699
698
  if (flags.yes && !Object.prototype.hasOwnProperty.call(downloadSeed, 'source')) {
700
699
  downloadSeed.source = 'docker';
701
700
  }
701
+ const builtinDbImageRegistry = String(presetValues.dockerRegistry ?? '').trim() ||
702
+ resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
703
+ if (!Object.prototype.hasOwnProperty.call(presetValues, 'dockerRegistry')) {
704
+ out.dockerRegistry = builtinDbImageRegistry;
705
+ }
706
+ out.builtinDbImageRegistry = builtinDbImageRegistry;
702
707
  const dbInitial = await Install.buildDbPromptInitialValues({
703
708
  flags,
704
709
  downloadResults: downloadSeed,
@@ -706,6 +711,9 @@ Prompt modes:
706
711
  warnOnPortFallback: false,
707
712
  });
708
713
  for (const [key, value] of Object.entries(dbInitial)) {
714
+ if (key === 'builtinDbImage') {
715
+ continue;
716
+ }
709
717
  if (!Object.prototype.hasOwnProperty.call(presetValues, key)) {
710
718
  out[key] = value;
711
719
  }
@@ -1032,6 +1040,14 @@ Prompt modes:
1032
1040
  const skipDownload = results.skipDownload === true;
1033
1041
  const appKey = resolveManagedAppKey(results.appKey ?? existingEnv?.config.appKey);
1034
1042
  const timeZone = resolveManagedTimeZone(results.timeZone ?? existingEnv?.config.timezone);
1043
+ const hookScriptInput = String(flags['hook-script'] ?? '').trim();
1044
+ const hookAppPath = appPath || `./${envName}/`;
1045
+ const hookScript = hookScriptInput
1046
+ ? await persistHookScript({
1047
+ sourcePath: hookScriptInput,
1048
+ appPath: resolveConfiguredEnvPath(hookAppPath) ?? resolveEnvRelativePath(hookAppPath),
1049
+ })
1050
+ : String(results.hookScript ?? '').trim();
1035
1051
  const builtinDb = explicitDbHostFlag(flags)
1036
1052
  ? false
1037
1053
  : results.builtinDb === undefined
@@ -1040,6 +1056,7 @@ Prompt modes:
1040
1056
  results.appKey = appKey;
1041
1057
  results.timeZone = timeZone;
1042
1058
  await upsertEnv(envName, {
1059
+ schemaVersion: ENV_CONFIG_SCHEMA_VERSION,
1043
1060
  ...(source === 'docker'
1044
1061
  ? { kind: 'docker' }
1045
1062
  : source || appPath || appRootPath
@@ -1057,6 +1074,7 @@ Prompt modes:
1057
1074
  ...(dockerPlatform ? { dockerPlatform } : {}),
1058
1075
  ...(gitUrl ? { gitUrl } : {}),
1059
1076
  ...(npmRegistry ? { npmRegistry } : {}),
1077
+ ...(hookScript ? { hookScript } : {}),
1060
1078
  ...(appPath ? { appPath } : {}),
1061
1079
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
1062
1080
  ...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
@@ -1132,6 +1150,10 @@ Prompt modes:
1132
1150
  if (flags['prepare-only']) {
1133
1151
  argv.push('--prepare-only');
1134
1152
  }
1153
+ const hookScript = String(results.hookScript ?? flags['hook-script'] ?? '').trim();
1154
+ if (hookScript) {
1155
+ argv.push('--hook-script', hookScript);
1156
+ }
1135
1157
  if (flags.verbose) {
1136
1158
  argv.push('--verbose');
1137
1159
  }
@@ -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';
@@ -178,22 +167,23 @@ function downloadVersionPromptValue(version) {
178
167
  }
179
168
  function supportsBuiltinDbDialect(value) {
180
169
  const dialect = String(value ?? '').trim();
181
- return Object.prototype.hasOwnProperty.call(DEFAULT_INSTALL_BUILTIN_DB_IMAGES, dialect);
170
+ return INSTALL_DB_DIALECTS.includes(dialect);
182
171
  }
183
172
  export function defaultDbPortForDialect(value) {
184
173
  const dialect = String(value ?? 'postgres').trim();
185
174
  return DEFAULT_INSTALL_DB_PORTS[isInstallDbDialect(dialect) ? dialect : 'postgres'];
186
175
  }
187
- function defaultBuiltinDbImageForDialect(value) {
176
+ function defaultBuiltinDbImageForDialect(value, options) {
188
177
  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;
178
+ return resolveBuiltinDbImage(dialect, { registry: options?.registry });
193
179
  }
194
180
  function defaultDbDatabaseForDialect(value) {
195
181
  return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
196
182
  }
183
+ function supportsDbSchemaPrompt(value) {
184
+ const dialect = String(value ?? '').trim();
185
+ return dialect === 'postgres' || dialect === 'kingbase';
186
+ }
197
187
  function defaultDbHostForBuiltinDb(values) {
198
188
  return values.builtinDb ? DEFAULT_INSTALL_BUILTIN_DB_HOST : DEFAULT_INSTALL_DB_HOST;
199
189
  }
@@ -472,7 +462,10 @@ export default class Install extends Command {
472
462
  description: 'Skip the download step and reuse an existing local app directory or Docker image',
473
463
  default: false,
474
464
  }),
475
- ...omitKeys(Download.flags, ['yes']),
465
+ 'hook-script': Flags.string({
466
+ description: 'Hook module copied to <appPath>/.nb/hooks.mjs and run before npm/git dependency installation.',
467
+ }),
468
+ ...omitKeys(Download.flags, ['yes', 'hook-script']),
476
469
  };
477
470
  /** Environment name only: run before {@link Install.prompts} (see `run`). */
478
471
  static envPrompts = {
@@ -547,7 +540,9 @@ export default class Install extends Command {
547
540
  type: 'text',
548
541
  message: installText('prompts.builtinDbImage.message'),
549
542
  placeholder: installText('prompts.builtinDbImage.placeholder'),
550
- initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
543
+ initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect, {
544
+ registry: String(values.builtinDbImageRegistry ?? '').trim() || undefined,
545
+ }),
551
546
  hidden: (values) => !values.builtinDb || !supportsBuiltinDbDialect(values.dbDialect),
552
547
  required: true,
553
548
  },
@@ -597,7 +592,7 @@ export default class Install extends Command {
597
592
  type: 'text',
598
593
  message: installText('prompts.dbSchema.message'),
599
594
  placeholder: installText('prompts.dbSchema.placeholder'),
600
- hidden: (values) => String(values.dbDialect ?? '').trim() !== 'postgres',
595
+ hidden: (values) => !supportsDbSchemaPrompt(values.dbDialect),
601
596
  },
602
597
  dbTablePrefix: {
603
598
  type: 'text',
@@ -1095,6 +1090,7 @@ export default class Install extends Command {
1095
1090
  const appPort = Install.toOptionalPromptString(config.appPort);
1096
1091
  const storagePath = Install.toOptionalPromptString(config.storagePath);
1097
1092
  const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
1093
+ const hookScript = Install.toOptionalPromptString(config.hookScript);
1098
1094
  const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
1099
1095
  const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
1100
1096
  const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
@@ -1125,6 +1121,7 @@ export default class Install extends Command {
1125
1121
  ...(appPort ? { appPort } : {}),
1126
1122
  ...(storagePath ? { storagePath } : {}),
1127
1123
  ...(appPublicPath ? { appPublicPath } : {}),
1124
+ ...(hookScript ? { hookScript } : {}),
1128
1125
  };
1129
1126
  const downloadPreset = {
1130
1127
  ...(source ? { source } : {}),
@@ -1273,19 +1270,25 @@ export default class Install extends Command {
1273
1270
  return builtinDb && Install.shouldPublishBuiltinDbPort(values.source);
1274
1271
  }
1275
1272
  static async buildDbPromptInitialValues(params) {
1276
- if (params.flags['db-port'] !== undefined) {
1277
- return {};
1278
- }
1273
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
1279
1274
  const values = {
1280
1275
  ...params.downloadResults,
1281
1276
  ...params.dbPreset,
1282
1277
  };
1278
+ const dockerRegistry = String(values.dockerRegistry ?? '').trim() || resolveOfficialDockerRegistry(configuredRegistry);
1279
+ const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
1280
+ const initialValues = values.builtinDb !== false && params.dbPreset.builtinDbImage === undefined
1281
+ ? { builtinDbImage: defaultBuiltinDbImageForDialect(dialect, { registry: dockerRegistry }) }
1282
+ : {};
1283
+ if (params.flags['db-port'] !== undefined) {
1284
+ return initialValues;
1285
+ }
1283
1286
  if (!Install.shouldPublishBuiltinDbPortForValues(values)) {
1284
- return {};
1287
+ return initialValues;
1285
1288
  }
1286
- const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
1287
1289
  const defaultPort = defaultDbPortForDialect(dialect);
1288
1290
  return {
1291
+ ...initialValues,
1289
1292
  dbPort: await Install.resolveAvailableDefaultPort(defaultPort, {
1290
1293
  label: `Default ${dialect} port`,
1291
1294
  warn: params.warnOnPortFallback ?? true,
@@ -1296,12 +1299,13 @@ export default class Install extends Command {
1296
1299
  * When install runs {@link Download.prompts} after app prompts, align the download
1297
1300
  * output directory with app settings, while Docker registry defaults follow the CLI locale.
1298
1301
  */
1299
- static buildDownloadPromptOptionsForInstall(appResults, envName) {
1302
+ static async buildDownloadPromptOptionsForInstall(appResults, envName) {
1300
1303
  const appRoot = resolveConfiguredSourcePathValue(appResults, envName);
1301
1304
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1305
+ const dockerRegistry = resolveOfficialDockerRegistry(await getCliConfigValue('nb-image-registry'));
1302
1306
  const initialValues = {
1303
1307
  lang,
1304
- dockerRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1308
+ dockerRegistry,
1305
1309
  outputDir: appRoot,
1306
1310
  };
1307
1311
  const values = {
@@ -1462,8 +1466,6 @@ export default class Install extends Command {
1462
1466
  '-d',
1463
1467
  '--name',
1464
1468
  containerName,
1465
- '--restart',
1466
- 'always',
1467
1469
  '--network',
1468
1470
  networkName,
1469
1471
  '-e',
@@ -1506,8 +1508,6 @@ export default class Install extends Command {
1506
1508
  '-d',
1507
1509
  '--name',
1508
1510
  containerName,
1509
- '--restart',
1510
- 'always',
1511
1511
  '--network',
1512
1512
  networkName,
1513
1513
  '-e',
@@ -1552,8 +1552,6 @@ export default class Install extends Command {
1552
1552
  '-d',
1553
1553
  '--name',
1554
1554
  containerName,
1555
- '--restart',
1556
- 'always',
1557
1555
  '--network',
1558
1556
  networkName,
1559
1557
  '-e',
@@ -1598,8 +1596,6 @@ export default class Install extends Command {
1598
1596
  '-d',
1599
1597
  '--name',
1600
1598
  containerName,
1601
- '--restart',
1602
- 'always',
1603
1599
  '--network',
1604
1600
  networkName,
1605
1601
  '--platform',
@@ -1762,12 +1758,15 @@ export default class Install extends Command {
1762
1758
  return plan;
1763
1759
  }
1764
1760
  static async buildDockerAppPlan(params) {
1761
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
1762
+ const configuredVariant = normalizeNbImageVariant(await getCliConfigValue('nb-image-variant')) ?? DEFAULT_NB_IMAGE_VARIANT;
1765
1763
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1766
- defaultDockerRegistryForLang(process.env.NB_LOCALE);
1764
+ resolveOfficialDockerRegistry(configuredRegistry);
1767
1765
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1768
1766
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1769
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1767
+ defaultRegistry: resolveOfficialDockerRegistry(configuredRegistry),
1770
1768
  defaultVersion: DEFAULT_DOCKER_VERSION,
1769
+ variant: inferNbImageRegistryFromRepository(dockerRegistry) ? configuredVariant : undefined,
1771
1770
  });
1772
1771
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1773
1772
  const configuredStoragePath = resolveConfiguredStoragePathValue(params.appResults, params.envName);
@@ -1794,17 +1793,16 @@ export default class Install extends Command {
1794
1793
  appResults: params.appResults,
1795
1794
  rootResults: params.rootResults,
1796
1795
  });
1796
+ const containerPort = resolveDockerImageContainerPort(imageRef);
1797
1797
  const args = [
1798
1798
  'run',
1799
1799
  '-d',
1800
1800
  '--name',
1801
1801
  containerName,
1802
- '--restart',
1803
- 'always',
1804
1802
  '--network',
1805
1803
  params.networkName,
1806
1804
  '-p',
1807
- `${appPort}:80`,
1805
+ `${appPort}:${containerPort}`,
1808
1806
  ];
1809
1807
  if (envFile) {
1810
1808
  args.push('--env-file', envFile);
@@ -1916,6 +1914,12 @@ export default class Install extends Command {
1916
1914
  if (results.buildDts) {
1917
1915
  argv.push('--build-dts');
1918
1916
  }
1917
+ Install.pushDownloadArgIfValue(argv, '--hook-script', results.hookScript);
1918
+ Install.pushDownloadArgIfValue(argv, '--hook-phase', results.hookPhase);
1919
+ Install.pushDownloadArgIfValue(argv, '--hook-command', results.hookCommand);
1920
+ Install.pushDownloadArgIfValue(argv, '--hook-env-name', results.hookEnvName);
1921
+ Install.pushDownloadArgIfValue(argv, '--hook-app-path', results.hookAppPath);
1922
+ Install.pushDownloadArgIfValue(argv, '--hook-storage-path', results.hookStoragePath);
1919
1923
  return argv;
1920
1924
  }
1921
1925
  static resolveLocalProjectRoot(params) {
@@ -1942,6 +1946,40 @@ export default class Install extends Command {
1942
1946
  buildDts: false,
1943
1947
  };
1944
1948
  }
1949
+ static resolveAbsoluteAppPath(envName, appResults) {
1950
+ const configuredAppPath = resolveConfiguredAppPathValue(appResults) ?? defaultInstallAppPath(envName);
1951
+ return resolveConfiguredEnvPath(configuredAppPath) ?? resolveEnvRelativePath(defaultInstallAppPath(envName));
1952
+ }
1953
+ static resolveAbsoluteStoragePath(envName, appResults) {
1954
+ const configuredStoragePath = resolveConfiguredStoragePathValue(appResults, envName);
1955
+ return (resolveConfiguredEnvPath(configuredStoragePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(envName)));
1956
+ }
1957
+ async prepareHookScriptForInstall(params) {
1958
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
1959
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
1960
+ const inputHookScript = Install.toOptionalPromptString(params.hookScript);
1961
+ if (inputHookScript) {
1962
+ params.appResults.hookScript = await persistHookScript({
1963
+ sourcePath: inputHookScript,
1964
+ appPath,
1965
+ });
1966
+ printInfo(`Saved hook script to ${path.join(appPath, String(params.appResults.hookScript))}.`);
1967
+ }
1968
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
1969
+ const hookScriptPath = resolveHookScriptPath({
1970
+ appPath,
1971
+ hookScript: savedHookScript,
1972
+ });
1973
+ if (!hookScriptPath) {
1974
+ return;
1975
+ }
1976
+ params.downloadResults.hookScript = hookScriptPath;
1977
+ params.downloadResults.hookPhase = 'init';
1978
+ params.downloadResults.hookCommand = 'init';
1979
+ params.downloadResults.hookEnvName = params.envName;
1980
+ params.downloadResults.hookAppPath = appPath;
1981
+ params.downloadResults.hookStoragePath = storagePath;
1982
+ }
1945
1983
  commandStdio(verbose) {
1946
1984
  return verbose ? 'inherit' : 'ignore';
1947
1985
  }
@@ -1954,12 +1992,15 @@ export default class Install extends Command {
1954
1992
  }
1955
1993
  async ensureSkippedDownloadInputsReady(params) {
1956
1994
  if (params.source === 'docker') {
1995
+ const configuredRegistry = await getCliConfigValue('nb-image-registry');
1996
+ const configuredVariant = normalizeNbImageVariant(await getCliConfigValue('nb-image-variant')) ?? DEFAULT_NB_IMAGE_VARIANT;
1957
1997
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1958
- defaultDockerRegistryForLang(process.env.NB_LOCALE);
1998
+ resolveOfficialDockerRegistry(configuredRegistry);
1959
1999
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1960
2000
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1961
- defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
2001
+ defaultRegistry: resolveOfficialDockerRegistry(configuredRegistry),
1962
2002
  defaultVersion: DEFAULT_DOCKER_VERSION,
2003
+ variant: inferNbImageRegistryFromRepository(dockerRegistry) ? configuredVariant : undefined,
1963
2004
  });
1964
2005
  const imageExists = await commandSucceeds('docker', ['image', 'inspect', imageRef]);
1965
2006
  if (!imageExists) {
@@ -2011,7 +2052,9 @@ export default class Install extends Command {
2011
2052
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
2012
2053
  const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
2013
2054
  const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
2055
+ const lifecycleEnvVars = managedAppLifecycleEnvVars();
2014
2056
  const env = {
2057
+ ...lifecycleEnvVars,
2015
2058
  STORAGE_PATH: storagePath,
2016
2059
  APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
2017
2060
  APP_KEY: appKey,
@@ -2216,6 +2259,51 @@ export default class Install extends Command {
2216
2259
  }
2217
2260
  await this.config.runCommand('env:update', [params.envName]);
2218
2261
  }
2262
+ buildAppStartArgv(params) {
2263
+ const argv = ['--env', params.envName, '--yes', '--no-sync-licensed-plugins', '--hook-command', 'init'];
2264
+ if (params.verbose) {
2265
+ argv.push('--verbose');
2266
+ }
2267
+ return argv;
2268
+ }
2269
+ async runInstallHookIfNeeded(params) {
2270
+ const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2271
+ const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
2272
+ const hookScriptPath = resolveHookScriptPath({
2273
+ appPath,
2274
+ hookScript: savedHookScript,
2275
+ });
2276
+ if (!hookScriptPath) {
2277
+ return;
2278
+ }
2279
+ const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
2280
+ const sourcePath = params.projectRoot ??
2281
+ resolveConfiguredEnvPath(resolveConfiguredSourcePathValue(params.appResults, params.envName)) ??
2282
+ path.join(appPath, 'source');
2283
+ const context = buildHookContext({
2284
+ phase: 'init',
2285
+ command: 'init',
2286
+ envName: params.envName,
2287
+ source: params.source,
2288
+ version: downloadResultsValue(params.downloadResults, 'version'),
2289
+ appPath,
2290
+ sourcePath,
2291
+ storagePath,
2292
+ hookScript: savedHookScript,
2293
+ envConfig: Install.buildSavedEnvConfig(params, {
2294
+ defaultApiHost: params.defaultApiHost,
2295
+ }),
2296
+ });
2297
+ if (!context) {
2298
+ return;
2299
+ }
2300
+ this.log(`Running hook ${params.hookName}: ${hookScriptPath}`);
2301
+ await runHookScriptHook({
2302
+ hookScriptPath,
2303
+ hookName: params.hookName,
2304
+ context,
2305
+ });
2306
+ }
2219
2307
  static buildSavedEnvConfig(params, options = {}) {
2220
2308
  const appPath = resolveConfiguredAppPathValue(params.appResults);
2221
2309
  const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
@@ -2247,6 +2335,7 @@ export default class Install extends Command {
2247
2335
  devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
2248
2336
  build: downloadResultsValue(params.downloadResults, 'build'),
2249
2337
  buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
2338
+ hookScript: params.appResults.hookScript,
2250
2339
  ...(appPath ? { appPath } : {}),
2251
2340
  ...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
2252
2341
  appPort,
@@ -2311,7 +2400,10 @@ export default class Install extends Command {
2311
2400
  yesInitialValues: { resume: parsed.resume },
2312
2401
  yes,
2313
2402
  });
2314
- const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2403
+ if (resumePreset?.appPreset?.hookScript !== undefined && appResults.hookScript === undefined) {
2404
+ appResults.hookScript = resumePreset.appPreset.hookScript;
2405
+ }
2406
+ const downloadOpts = await Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2315
2407
  downloadOpts.values = {
2316
2408
  ...(resumePreset?.downloadPreset ?? {}),
2317
2409
  ...downloadOpts.values,
@@ -2418,7 +2510,6 @@ export default class Install extends Command {
2418
2510
  const parsed = {
2419
2511
  ...flags,
2420
2512
  };
2421
- const defaultApiHost = await resolveDefaultApiHost();
2422
2513
  if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2423
2514
  this.error('--skip-auth cannot be used with --access-token or --token.');
2424
2515
  }
@@ -2440,6 +2531,12 @@ export default class Install extends Command {
2440
2531
  appResults,
2441
2532
  });
2442
2533
  appResults.setupState = 'prepared';
2534
+ await this.prepareHookScriptForInstall({
2535
+ envName,
2536
+ appResults,
2537
+ downloadResults,
2538
+ hookScript: parsed['hook-script'],
2539
+ });
2443
2540
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2444
2541
  const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2445
2542
  const dockerNetworkName = usesDockerResources
@@ -2492,8 +2589,7 @@ export default class Install extends Command {
2492
2589
  dbResults.dbUser = builtinDbPlan.dbUser;
2493
2590
  dbResults.dbPassword = builtinDbPlan.dbPassword;
2494
2591
  }
2495
- let dockerAppPlan;
2496
- let localAppPlan;
2592
+ let shouldStartApp = false;
2497
2593
  if (source === 'docker' || source === 'npm' || source === 'git') {
2498
2594
  this.logStage('Preparing application');
2499
2595
  if (source === 'docker') {
@@ -2505,76 +2601,28 @@ export default class Install extends Command {
2505
2601
  printInfo('Application image ready.');
2506
2602
  }
2507
2603
  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;
2604
+ shouldStartApp = true;
2522
2605
  }
2523
2606
  }
2524
2607
  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({
2608
+ if (!parsed['skip-download'] && !parsed['prepare-only']) {
2609
+ await this.downloadLocalApp({
2533
2610
  envName,
2534
2611
  appResults,
2535
2612
  downloadResults,
2536
2613
  verbose: parsed.verbose,
2537
2614
  });
2538
- if (!parsed['skip-download']) {
2539
2615
  printInfo('Application files ready.');
2540
2616
  }
2541
2617
  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;
2618
+ shouldStartApp = true;
2553
2619
  }
2554
2620
  }
2555
2621
  }
2556
2622
  else {
2557
2623
  this.logDetail('Skipped app download and install.');
2558
2624
  }
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) {
2625
+ if (shouldStartApp || builtinDbPlan) {
2578
2626
  await this.saveInstalledEnv({
2579
2627
  envName,
2580
2628
  appResults,
@@ -2584,10 +2632,14 @@ export default class Install extends Command {
2584
2632
  envAddResults,
2585
2633
  });
2586
2634
  }
2635
+ if (shouldStartApp) {
2636
+ this.logStage('Starting NocoBase');
2637
+ await this.config.runCommand('app:start', this.buildAppStartArgv({ envName, verbose: parsed.verbose }));
2638
+ }
2587
2639
  await this.syncInstalledEnvConnection({
2588
2640
  envName,
2589
2641
  envAddResults,
2590
- appReady: Boolean(dockerAppPlan || localAppPlan),
2642
+ appReady: shouldStartApp,
2591
2643
  skipAuth: Boolean(parsed['skip-auth']),
2592
2644
  });
2593
2645
  if (!parsed['prepare-only']) {
@@ -2596,7 +2648,7 @@ export default class Install extends Command {
2596
2648
  if (parsed['prepare-only']) {
2597
2649
  printInfo(`Preparation complete for "${envName}". Activate the license, then run \`nb app start --env ${envName}\`.`);
2598
2650
  }
2599
- else if (!dockerAppPlan && !localAppPlan) {
2651
+ else if (!shouldStartApp) {
2600
2652
  printInfo(`Install config for "${envName}" has been saved.`);
2601
2653
  }
2602
2654
  }
@@ -143,11 +143,14 @@ export default class LicenseActivate extends Command {
143
143
  const validation = await validateLicenseKey(runtime, resolvedKey);
144
144
  const ok = !validation.keyStatus && validation.envMatch && validation.domainMatch && validation.licenseStatus === 'active';
145
145
  const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
146
+ const shouldResolveInstanceId = Boolean(interactiveKeyFlowInstanceId || ok || (flags.json && !validation.keyStatus));
146
147
  const payload = {
147
148
  ok,
148
149
  env: runtime.envName,
149
150
  kind: runtime.kind,
150
- instanceId: interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime)),
151
+ instanceId: shouldResolveInstanceId
152
+ ? interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime))
153
+ : undefined,
151
154
  mode: 'key',
152
155
  key: redactLicenseKey(resolvedKey),
153
156
  keyFile: keyFile || undefined,