@nocobase/cli 2.2.0-alpha.5 → 2.2.0-alpha.6

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.
@@ -15,3 +15,4 @@ proxy_connect_timeout 600;
15
15
  proxy_send_timeout 600;
16
16
  proxy_read_timeout 600;
17
17
  send_timeout 600;
18
+ proxy_buffering off;
@@ -37,6 +37,9 @@ function buildLicenseSyncArgv(envName, options) {
37
37
  }
38
38
  function resolveHookCommand(value) {
39
39
  const text = String(value ?? '').trim();
40
+ if (text === 'init') {
41
+ return text;
42
+ }
40
43
  if (text === 'app:restart' || text === 'app:upgrade') {
41
44
  return text;
42
45
  }
@@ -225,7 +228,7 @@ export default class AppStart extends Command {
225
228
  }),
226
229
  'hook-command': Flags.string({
227
230
  hidden: true,
228
- options: ['app:start', 'app:restart', 'app:upgrade'],
231
+ options: ['init', 'app:start', 'app:restart', 'app:upgrade'],
229
232
  }),
230
233
  };
231
234
  async run() {
@@ -2253,6 +2253,13 @@ export default class Install extends Command {
2253
2253
  }
2254
2254
  await this.config.runCommand('env:update', [params.envName]);
2255
2255
  }
2256
+ buildAppStartArgv(params) {
2257
+ const argv = ['--env', params.envName, '--yes', '--no-sync-licensed-plugins', '--hook-command', 'init'];
2258
+ if (params.verbose) {
2259
+ argv.push('--verbose');
2260
+ }
2261
+ return argv;
2262
+ }
2256
2263
  async runInstallHookIfNeeded(params) {
2257
2264
  const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
2258
2265
  const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
@@ -2497,7 +2504,6 @@ export default class Install extends Command {
2497
2504
  const parsed = {
2498
2505
  ...flags,
2499
2506
  };
2500
- const defaultApiHost = await resolveDefaultApiHost();
2501
2507
  if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2502
2508
  this.error('--skip-auth cannot be used with --access-token or --token.');
2503
2509
  }
@@ -2577,8 +2583,7 @@ export default class Install extends Command {
2577
2583
  dbResults.dbUser = builtinDbPlan.dbUser;
2578
2584
  dbResults.dbPassword = builtinDbPlan.dbPassword;
2579
2585
  }
2580
- let dockerAppPlan;
2581
- let localAppPlan;
2586
+ let shouldStartApp = false;
2582
2587
  if (source === 'docker' || source === 'npm' || source === 'git') {
2583
2588
  this.logStage('Preparing application');
2584
2589
  if (source === 'docker') {
@@ -2590,111 +2595,28 @@ export default class Install extends Command {
2590
2595
  printInfo('Application image ready.');
2591
2596
  }
2592
2597
  if (!parsed['prepare-only']) {
2593
- await this.runInstallHookIfNeeded({
2594
- hookName: 'beforeAppInstall',
2595
- envName,
2596
- source,
2597
- appResults,
2598
- downloadResults,
2599
- dbResults,
2600
- rootResults,
2601
- envAddResults,
2602
- defaultApiHost,
2603
- });
2604
- dockerAppPlan = await this.installDockerApp({
2605
- envName,
2606
- dockerNetworkName,
2607
- dockerContainerPrefix,
2608
- appResults,
2609
- downloadResults,
2610
- dbResults,
2611
- rootResults,
2612
- builtinDbPlan,
2613
- force: parsed.force,
2614
- commandStdio,
2615
- });
2616
- appResults.appKey = dockerAppPlan.appKey;
2617
- appResults.timeZone = dockerAppPlan.timeZone;
2598
+ shouldStartApp = true;
2618
2599
  }
2619
2600
  }
2620
2601
  else if (source === 'npm' || source === 'git') {
2621
- const localSource = source === 'npm' ? 'npm' : 'git';
2622
- const projectRoot = parsed['skip-download'] || parsed['prepare-only']
2623
- ? Install.resolveLocalProjectRoot({
2624
- envName,
2625
- appResults,
2626
- downloadResults,
2627
- })
2628
- : await this.downloadLocalApp({
2602
+ if (!parsed['skip-download'] && !parsed['prepare-only']) {
2603
+ await this.downloadLocalApp({
2629
2604
  envName,
2630
2605
  appResults,
2631
2606
  downloadResults,
2632
2607
  verbose: parsed.verbose,
2633
2608
  });
2634
- if (!parsed['skip-download'] && !parsed['prepare-only']) {
2635
2609
  printInfo('Application files ready.');
2636
2610
  }
2637
2611
  if (!parsed['prepare-only']) {
2638
- await this.runInstallHookIfNeeded({
2639
- hookName: 'beforeAppInstall',
2640
- envName,
2641
- source,
2642
- appResults,
2643
- downloadResults,
2644
- dbResults,
2645
- rootResults,
2646
- envAddResults,
2647
- projectRoot,
2648
- defaultApiHost,
2649
- });
2650
- localAppPlan = await this.startLocalApp({
2651
- envName,
2652
- source: localSource,
2653
- projectRoot,
2654
- appResults,
2655
- dbResults,
2656
- rootResults,
2657
- commandStdio,
2658
- });
2659
- appResults.appKey = localAppPlan.appKey;
2660
- appResults.timeZone = localAppPlan.timeZone;
2612
+ shouldStartApp = true;
2661
2613
  }
2662
2614
  }
2663
2615
  }
2664
2616
  else {
2665
2617
  this.logDetail('Skipped app download and install.');
2666
2618
  }
2667
- if (dockerAppPlan || localAppPlan) {
2668
- this.logStage('Starting NocoBase');
2669
- await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2670
- appResults,
2671
- envAddResults,
2672
- defaultApiHost,
2673
- }), {
2674
- containerName: dockerAppPlan?.containerName,
2675
- verbose: parsed.verbose,
2676
- });
2677
- const displayApiBaseUrl = Install.resolveApiBaseUrl({
2678
- appResults,
2679
- envAddResults,
2680
- defaultApiHost,
2681
- });
2682
- printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
2683
- appResults.setupState = 'installed';
2684
- await this.runInstallHookIfNeeded({
2685
- hookName: 'afterAppStart',
2686
- envName,
2687
- source,
2688
- appResults,
2689
- downloadResults,
2690
- dbResults,
2691
- rootResults,
2692
- envAddResults,
2693
- projectRoot: localAppPlan?.projectRoot,
2694
- defaultApiHost,
2695
- });
2696
- }
2697
- if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2619
+ if (shouldStartApp || builtinDbPlan) {
2698
2620
  await this.saveInstalledEnv({
2699
2621
  envName,
2700
2622
  appResults,
@@ -2704,10 +2626,14 @@ export default class Install extends Command {
2704
2626
  envAddResults,
2705
2627
  });
2706
2628
  }
2629
+ if (shouldStartApp) {
2630
+ this.logStage('Starting NocoBase');
2631
+ await this.config.runCommand('app:start', this.buildAppStartArgv({ envName, verbose: parsed.verbose }));
2632
+ }
2707
2633
  await this.syncInstalledEnvConnection({
2708
2634
  envName,
2709
2635
  envAddResults,
2710
- appReady: Boolean(dockerAppPlan || localAppPlan),
2636
+ appReady: shouldStartApp,
2711
2637
  skipAuth: Boolean(parsed['skip-auth']),
2712
2638
  });
2713
2639
  if (!parsed['prepare-only']) {
@@ -2716,7 +2642,7 @@ export default class Install extends Command {
2716
2642
  if (parsed['prepare-only']) {
2717
2643
  printInfo(`Preparation complete for "${envName}". Activate the license, then run \`nb app start --env ${envName}\`.`);
2718
2644
  }
2719
- else if (!dockerAppPlan && !localAppPlan) {
2645
+ else if (!shouldStartApp) {
2720
2646
  printInfo(`Install config for "${envName}" has been saved.`);
2721
2647
  }
2722
2648
  }
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../../lib/app-runtime.js';
11
+ import { resolveEnvProxyEntry, setEnvProxyEntry } from '../../../lib/auth-store.js';
12
+ import { resolveDefaultConfigScope } from '../../../lib/cli-home.js';
11
13
  import { getCaddyProxyDriver, writeManualCaddyProxyBundle, writeCaddyProxyBundle, resolveCaddyProxyRuntimeContext, } from '../../../lib/proxy-caddy.js';
12
14
  import { normalizeProxyListenPort } from '../../../lib/proxy-nginx.js';
13
15
  import { announceTargetEnv, failTask, startTask, succeedTask } from '../../../lib/ui.js';
@@ -129,12 +131,18 @@ export default class ProxyCaddyGenerate extends Command {
129
131
  announceTargetEnv(runtime.envName);
130
132
  startTask(`Generating caddy proxy config for env "${runtime.envName}" with the ${driver} driver...`);
131
133
  try {
132
- const { bundle, status } = await writeCaddyProxyBundle(runtime, {
133
- host: flags.host?.trim() || undefined,
134
- port: normalizedPort,
135
- }, runtimeContext, {
134
+ const savedAppEntryOptions = resolveEnvProxyEntry(runtime.env.config, 'caddy');
135
+ const appEntryOptions = {
136
+ host: flags.host?.trim() || savedAppEntryOptions?.host,
137
+ port: normalizedPort ?? (savedAppEntryOptions?.port !== undefined ? String(savedAppEntryOptions.port) : undefined),
138
+ };
139
+ const { bundle, status } = await writeCaddyProxyBundle(runtime, appEntryOptions, runtimeContext, {
136
140
  cdnBaseUrl: flags['cdn-base-url']?.trim() || undefined,
137
141
  });
142
+ await setEnvProxyEntry(runtime.envName, 'caddy', {
143
+ host: appEntryOptions.host,
144
+ port: appEntryOptions.port ? Number(appEntryOptions.port) : undefined,
145
+ }, { scope: resolveDefaultConfigScope() });
138
146
  succeedTask(status === 'created'
139
147
  ? `Saved caddy proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and created app.caddy at ${bundle.appConfigPath}.`
140
148
  : `Saved caddy proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and refreshed app.caddy at ${bundle.appConfigPath}.`);
@@ -8,7 +8,9 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../../lib/app-runtime.js';
11
+ import { resolveDefaultConfigScope } from '../../../lib/cli-home.js';
11
12
  import { getNginxProxyDriver, normalizeProxyListenPort, resolveNginxProxyRuntimeContext, writeManualNginxProxyBundle, writeNginxProxyBundle, } from '../../../lib/proxy-nginx.js';
13
+ import { resolveEnvProxyEntry, setEnvProxyEntry } from '../../../lib/auth-store.js';
12
14
  import { announceTargetEnv, failTask, startTask, succeedTask } from '../../../lib/ui.js';
13
15
  export default class ProxyNginxGenerate extends Command {
14
16
  static summary = 'Generate nginx proxy files for one managed env';
@@ -132,13 +134,19 @@ export default class ProxyNginxGenerate extends Command {
132
134
  announceTargetEnv(runtime.envName);
133
135
  startTask(`Generating nginx proxy config for env "${runtime.envName}" with the ${driver} driver...`);
134
136
  try {
135
- const { bundle, status } = await writeNginxProxyBundle(runtime, {
136
- host: flags.host?.trim() || undefined,
137
- port: normalizedPort,
138
- }, runtimeContext, {
137
+ const savedAppEntryOptions = resolveEnvProxyEntry(runtime.env.config, 'nginx');
138
+ const appEntryOptions = {
139
+ host: flags.host?.trim() || savedAppEntryOptions?.host,
140
+ port: normalizedPort ?? (savedAppEntryOptions?.port !== undefined ? String(savedAppEntryOptions.port) : undefined),
141
+ };
142
+ const { bundle, status } = await writeNginxProxyBundle(runtime, appEntryOptions, runtimeContext, {
139
143
  cdnBaseUrl: flags['cdn-base-url']?.trim() || undefined,
140
144
  force: flags.force,
141
145
  });
146
+ await setEnvProxyEntry(runtime.envName, 'nginx', {
147
+ host: appEntryOptions.host,
148
+ port: appEntryOptions.port ? Number(appEntryOptions.port) : undefined,
149
+ }, { scope: resolveDefaultConfigScope() });
142
150
  succeedTask(status === 'created'
143
151
  ? `Saved nginx proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and created editable app entry config at ${bundle.appConfigPath}.`
144
152
  : `Saved nginx proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and refreshed editable app entry config at ${bundle.appConfigPath}.`);
@@ -11,6 +11,7 @@ import path from 'node:path';
11
11
  import { resolveAppPublicPath } from './app-public-path.js';
12
12
  import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
13
13
  import { normalizeCliLocale } from './cli-locale.js';
14
+ import { normalizeEnvProxyConfig, normalizeEnvProxyProviderConfig, } from './env-proxy-config.js';
14
15
  import { inferConfiguredAppPathFromLegacyConfig, resolveConfiguredAppPath, resolveConfiguredSourcePath, resolveConfiguredStoragePath, } from './env-paths.js';
15
16
  import { ENV_CONFIG_SCHEMA_VERSION, normalizeEnvConfigSchemaVersion } from './env-config.js';
16
17
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
@@ -81,12 +82,14 @@ function normalizeEnvConfigEntry(entry) {
81
82
  const normalizedKind = resolveEnvKind(entry);
82
83
  const apiBaseUrl = readEnvApiBaseUrl(entry);
83
84
  const schemaVersion = normalizeEnvConfigSchemaVersion(entry.schemaVersion);
85
+ const proxy = normalizeEnvProxyConfig(entry.proxy);
84
86
  return {
85
87
  ...rest,
86
88
  ...(schemaVersion ? { schemaVersion } : {}),
87
89
  ...(normalizedKind ? { kind: normalizedKind } : {}),
88
90
  ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
89
91
  ...(normalizeOptionalString(entry.appPublicPath) ? { appPublicPath: resolveAppPublicPath(entry.appPublicPath) } : {}),
92
+ ...(proxy ? { proxy } : {}),
90
93
  };
91
94
  }
92
95
  function normalizeAuthConfig(config) {
@@ -516,6 +519,64 @@ export async function setEnvRuntime(envName, runtime, options = {}) {
516
519
  };
517
520
  await saveAuthConfig(config, options);
518
521
  }
522
+ export function resolveEnvProxyEntry(config, provider) {
523
+ const proxy = normalizeEnvProxyConfig(config?.proxy);
524
+ const resolved = {
525
+ ...(proxy?.host ? { host: proxy.host } : {}),
526
+ ...(proxy?.port !== undefined ? { port: proxy.port } : {}),
527
+ ...((provider === 'nginx' ? proxy?.nginx : proxy?.caddy) ?? {}),
528
+ };
529
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
530
+ }
531
+ export async function setEnvProxyEntry(envName, provider, entry, options = {}) {
532
+ await writeEnv(envName, (previous) => {
533
+ const currentProxy = normalizeEnvProxyConfig(previous?.proxy) ?? {};
534
+ const nextEntry = normalizeEnvProxyProviderConfig(entry);
535
+ const nextProxy = { ...currentProxy };
536
+ if (nextEntry && 'host' in nextEntry) {
537
+ const host = normalizeOptionalString(nextEntry.host);
538
+ if (host) {
539
+ nextProxy.host = host;
540
+ }
541
+ else {
542
+ delete nextProxy.host;
543
+ }
544
+ }
545
+ if (nextEntry && 'port' in nextEntry) {
546
+ const portValue = nextEntry.port;
547
+ const port = typeof portValue === 'number' && Number.isInteger(portValue) && portValue >= 1 && portValue <= 65535
548
+ ? portValue
549
+ : undefined;
550
+ if (port !== undefined) {
551
+ nextProxy.port = port;
552
+ }
553
+ else {
554
+ delete nextProxy.port;
555
+ }
556
+ }
557
+ const providerConfig = nextEntry && Object.keys(nextEntry).some((key) => key !== 'host' && key !== 'port')
558
+ ? Object.fromEntries(Object.entries(nextEntry).filter(([key]) => key !== 'host' && key !== 'port'))
559
+ : undefined;
560
+ if (provider === 'nginx') {
561
+ if (providerConfig && Object.keys(providerConfig).length > 0) {
562
+ nextProxy.nginx = providerConfig;
563
+ }
564
+ else {
565
+ delete nextProxy.nginx;
566
+ }
567
+ }
568
+ else if (providerConfig && Object.keys(providerConfig).length > 0) {
569
+ nextProxy.caddy = providerConfig;
570
+ }
571
+ else {
572
+ delete nextProxy.caddy;
573
+ }
574
+ return {
575
+ ...previous,
576
+ proxy: nextProxy,
577
+ };
578
+ }, options);
579
+ }
519
580
  export async function clearEnvRootSetup(envName, options = {}) {
520
581
  const config = await loadExactAuthConfig(options);
521
582
  const current = config.envs[envName];
@@ -6,6 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { normalizeEnvProxyConfig } from './env-proxy-config.js';
9
10
  import { resolveAppPublicPath } from './app-public-path.js';
10
11
  const STRING_ENV_CONFIG_KEYS = [
11
12
  'source',
@@ -110,5 +111,9 @@ export function buildStoredEnvConfig(input) {
110
111
  if ((authType === 'basic' || authType === 'token') && accessToken) {
111
112
  envConfig.accessToken = accessToken;
112
113
  }
114
+ const proxy = normalizeEnvProxyConfig(input.proxy);
115
+ if (proxy) {
116
+ envConfig.proxy = proxy;
117
+ }
113
118
  return envConfig;
114
119
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ function normalizeOptionalString(value) {
10
+ const normalized = String(value ?? '').trim();
11
+ return normalized || undefined;
12
+ }
13
+ function normalizeOptionalPort(value) {
14
+ const normalized = normalizeOptionalString(value);
15
+ if (!normalized || !/^\d+$/.test(normalized)) {
16
+ return undefined;
17
+ }
18
+ const port = Number(normalized);
19
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
20
+ return undefined;
21
+ }
22
+ return port;
23
+ }
24
+ export function normalizeEnvProxyProviderConfig(value) {
25
+ if (!value || typeof value !== 'object') {
26
+ return undefined;
27
+ }
28
+ return { ...value };
29
+ }
30
+ export function normalizeEnvProxyConfig(value) {
31
+ if (!value || typeof value !== 'object') {
32
+ return undefined;
33
+ }
34
+ const proxy = value;
35
+ const host = normalizeOptionalString(proxy.host);
36
+ const port = normalizeOptionalPort(proxy.port);
37
+ const nginx = normalizeEnvProxyProviderConfig(proxy.nginx);
38
+ const caddy = normalizeEnvProxyProviderConfig(proxy.caddy);
39
+ if (!host && port === undefined && !nginx && !caddy) {
40
+ return undefined;
41
+ }
42
+ return {
43
+ ...(host ? { host } : {}),
44
+ ...(port !== undefined ? { port } : {}),
45
+ ...(nginx ? { nginx } : {}),
46
+ ...(caddy ? { caddy } : {}),
47
+ };
48
+ }
@@ -487,6 +487,7 @@ function rewriteHtmlAssetPublicPath(html, currentPublicPath, nextPublicPath) {
487
487
  }
488
488
  function buildNginxManagedConfigBlock(context) {
489
489
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
490
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
490
491
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
491
492
  const isRootMounted = context.appPublicPath === '/';
492
493
  const appPublicPathRedirectBlock = isRootMounted
@@ -525,6 +526,10 @@ function buildNginxManagedConfigBlock(context) {
525
526
  ` include ${context.snippetsDir}/proxy-location.conf;`,
526
527
  ' }',
527
528
  '',
529
+ ` location = ${apiBasePathNoTrailingSlash} {`,
530
+ ` return 308 ${context.apiBasePath}$is_args$args;`,
531
+ ' }',
532
+ '',
528
533
  ` location ^~ ${context.apiBasePath} {`,
529
534
  ` proxy_pass ${context.backendUrl};`,
530
535
  ` include ${context.snippetsDir}/proxy-location.conf;`,
@@ -989,6 +994,7 @@ function buildNginxOtherLocation(appPublicPath, v2PublicPath, modernClientPrefix
989
994
  function renderNginxLocationTemplate(context) {
990
995
  const proxyPassBlock = buildNginxProxyPassBlock(context.proxyHost, context.apiPort);
991
996
  const wsProxyPassTarget = `http://${context.proxyHost}:${context.apiPort}${context.wsPath}`;
997
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
992
998
  return ` location ~* ^${context.appPublicPath}storage/uploads/(.*\\.md)$ {
993
999
  alias ${context.uploadsPath}/$1;
994
1000
  default_type text/markdown;
@@ -1034,6 +1040,10 @@ function renderNginxLocationTemplate(context) {
1034
1040
  ${proxyPassBlock}
1035
1041
  }${context.otherLocation}
1036
1042
 
1043
+ location = ${apiBasePathNoTrailingSlash} {
1044
+ return 308 ${context.apiBasePath}$is_args$args;
1045
+ }
1046
+
1037
1047
  location ^~ ${context.apiBasePath} {
1038
1048
  ${proxyPassBlock}
1039
1049
  }
@@ -9,14 +9,17 @@
9
9
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
12
+ import { loadAuthConfig } from './auth-store.js';
12
13
  import { CADDY_PROXY_DRIVER_OPTIONS, DEFAULT_CADDY_PROXY_DRIVER, getCliConfigValue, normalizeCaddyProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
13
14
  import { resolveCliHomeRoot } from './cli-home.js';
14
15
  import { applyEnvProxyAppEntryOptions, buildManualEnvProxyCaddyBundle, buildEnvProxyCaddyBundle, buildEnvProxyMainConfig, mapProxyPathFromCliRoot, resolveEnvProxyMainOutputPath, } from './env-proxy.js';
15
- import { run } from './run-npm.js';
16
+ import { normalizeEnvProxyConfig } from './env-proxy-config.js';
17
+ import { commandOutput, run } from './run-npm.js';
16
18
  const DOCKER_CADDY_PROXY_CONTAINER_SUFFIX = 'caddy-proxy';
17
19
  const DOCKER_CADDY_PROXY_IMAGE = 'caddy:latest';
18
20
  const DOCKER_CADDY_PROXY_RUNTIME_ROOT = '/apps';
19
21
  const DOCKER_CADDY_PROXY_CONF_DESTINATION = '/etc/caddy/Caddyfile';
22
+ const DEFAULT_DOCKER_CADDY_PROXY_PUBLISHED_PORTS = [80, 443];
20
23
  async function readOptionalTextFile(filePath) {
21
24
  try {
22
25
  return await readFile(filePath, 'utf8');
@@ -171,11 +174,16 @@ async function reloadLocalCaddyProxy(runtimeContext) {
171
174
  }
172
175
  async function ensureDockerCaddyProxyContainer(runtimeContext) {
173
176
  const containerName = await resolveCaddyProxyContainerName();
177
+ const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
178
+ const publishedPorts = await resolveDockerCaddyPublishedPorts();
174
179
  if (await dockerContainerExists(containerName)) {
175
- return;
180
+ if (await dockerCaddyProxyContainerMatchesPublishedPorts(containerName, publishedPorts)) {
181
+ return;
182
+ }
183
+ await removeDockerCaddyProxyContainer(containerName);
176
184
  }
177
185
  const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
178
- const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
186
+ const dockerPortArgs = publishedPorts.flatMap((port) => ['-p', `${port}:${port}`]);
179
187
  await run('docker', [
180
188
  'run',
181
189
  '-d',
@@ -183,8 +191,7 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
183
191
  containerName,
184
192
  '--add-host',
185
193
  'host.docker.internal:host-gateway',
186
- '-p',
187
- '80:80',
194
+ ...dockerPortArgs,
188
195
  '-v',
189
196
  `${hostCliRoot}:${DOCKER_CADDY_PROXY_RUNTIME_ROOT}`,
190
197
  '-v',
@@ -195,14 +202,53 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
195
202
  stdio: 'ignore',
196
203
  });
197
204
  }
205
+ async function resolveDockerCaddyPublishedPorts() {
206
+ const config = await loadAuthConfig();
207
+ const ports = new Set(DEFAULT_DOCKER_CADDY_PROXY_PUBLISHED_PORTS);
208
+ for (const envConfig of Object.values(config.envs)) {
209
+ const port = normalizeEnvProxyConfig(envConfig.proxy)?.port;
210
+ if (port !== undefined) {
211
+ ports.add(port);
212
+ }
213
+ }
214
+ return Array.from(ports).sort((left, right) => left - right);
215
+ }
216
+ async function readDockerCaddyPublishedPorts(containerName) {
217
+ const output = await commandOutput('docker', ['inspect', '--format', '{{json .HostConfig.PortBindings}}', containerName], { errorName: 'docker inspect' });
218
+ const parsed = JSON.parse(output.trim() || '{}');
219
+ const ports = new Set();
220
+ for (const bindings of Object.values(parsed)) {
221
+ if (!Array.isArray(bindings)) {
222
+ continue;
223
+ }
224
+ for (const binding of bindings) {
225
+ const port = Number.parseInt(String(binding?.HostPort ?? '').trim(), 10);
226
+ if (Number.isInteger(port) && port > 0) {
227
+ ports.add(port);
228
+ }
229
+ }
230
+ }
231
+ return Array.from(ports).sort((left, right) => left - right);
232
+ }
233
+ async function dockerCaddyProxyContainerMatchesPublishedPorts(containerName, expectedPorts) {
234
+ const currentPorts = await readDockerCaddyPublishedPorts(containerName);
235
+ return currentPorts.length === expectedPorts.length && currentPorts.every((port, index) => port === expectedPorts[index]);
236
+ }
237
+ async function removeDockerCaddyProxyContainer(containerName) {
238
+ await run('docker', ['rm', '-f', containerName], {
239
+ errorName: 'docker rm',
240
+ stdio: 'ignore',
241
+ });
242
+ }
198
243
  async function startDockerCaddyProxy(runtimeContext) {
199
244
  const containerName = await resolveCaddyProxyContainerName();
200
245
  await ensureCaddyProxyMainConfig(runtimeContext);
201
- if (await dockerContainerExists(containerName)) {
246
+ const existedBeforeEnsure = await dockerContainerExists(containerName);
247
+ await ensureDockerCaddyProxyContainer(runtimeContext);
248
+ if (existedBeforeEnsure && (await dockerContainerExists(containerName))) {
202
249
  const state = await startDockerContainer(containerName, { stdio: 'ignore' });
203
250
  return state === 'already-running' ? 'already-running' : 'started';
204
251
  }
205
- await ensureDockerCaddyProxyContainer(runtimeContext);
206
252
  return 'started';
207
253
  }
208
254
  async function stopDockerCaddyProxy() {
@@ -9,14 +9,17 @@
9
9
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
12
+ import { loadAuthConfig } from './auth-store.js';
12
13
  import { DEFAULT_NGINX_PROXY_DRIVER, getCliConfigValue, NGINX_PROXY_DRIVER_OPTIONS, normalizeNginxProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
13
14
  import { resolveCliHomeRoot } from './cli-home.js';
14
15
  import { applyEnvProxyAppEntryOptions, appConfigHasManagedNginxBlock, buildManualEnvProxyNginxBundle, buildEnvProxyMainConfig, buildEnvProxyNginxBundle, extractManagedNginxConfigBlock, installEnvProxyProvider, mapProxyPathFromCliRoot, reloadEnvProxyProvider, resolveEnvProxyMainOutputPath, replaceManagedNginxConfigBlock, syncEnvProxyNginxSnippets, } from './env-proxy.js';
15
- import { run } from './run-npm.js';
16
+ import { normalizeEnvProxyConfig } from './env-proxy-config.js';
17
+ import { commandOutput, run } from './run-npm.js';
16
18
  const DOCKER_NGINX_PROXY_CONTAINER_SUFFIX = 'nginx-proxy';
17
19
  const DOCKER_NGINX_PROXY_IMAGE = 'nginx:latest';
18
20
  const DOCKER_NGINX_PROXY_RUNTIME_ROOT = '/apps';
19
21
  const DOCKER_NGINX_PROXY_CONF_DESTINATION = '/etc/nginx/conf.d/default.conf';
22
+ const DEFAULT_DOCKER_NGINX_PROXY_PUBLISHED_PORTS = [80, 443];
20
23
  async function readOptionalTextFile(filePath) {
21
24
  try {
22
25
  return await readFile(filePath, 'utf8');
@@ -231,11 +234,16 @@ async function reloadLocalNginxProxy(runtimeContext) {
231
234
  }
232
235
  async function ensureDockerNginxProxyContainer(runtimeContext) {
233
236
  const containerName = await resolveNginxProxyContainerName();
237
+ const mainConfigPath = await ensureNginxProxyMainConfig(runtimeContext);
238
+ const publishedPorts = await resolveDockerNginxPublishedPorts();
234
239
  if (await dockerContainerExists(containerName)) {
235
- return;
240
+ if (await dockerNginxProxyContainerMatchesPublishedPorts(containerName, publishedPorts)) {
241
+ return;
242
+ }
243
+ await removeDockerNginxProxyContainer(containerName);
236
244
  }
237
245
  const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
238
- const mainConfigPath = await ensureNginxProxyMainConfig(runtimeContext);
246
+ const dockerPortArgs = publishedPorts.flatMap((port) => ['-p', `${port}:${port}`]);
239
247
  await run('docker', [
240
248
  'run',
241
249
  '-d',
@@ -243,8 +251,7 @@ async function ensureDockerNginxProxyContainer(runtimeContext) {
243
251
  containerName,
244
252
  '--add-host',
245
253
  'host.docker.internal:host-gateway',
246
- '-p',
247
- '80:80',
254
+ ...dockerPortArgs,
248
255
  '-v',
249
256
  `${hostCliRoot}:${DOCKER_NGINX_PROXY_RUNTIME_ROOT}`,
250
257
  '-v',
@@ -255,14 +262,53 @@ async function ensureDockerNginxProxyContainer(runtimeContext) {
255
262
  stdio: 'ignore',
256
263
  });
257
264
  }
265
+ async function resolveDockerNginxPublishedPorts() {
266
+ const config = await loadAuthConfig();
267
+ const ports = new Set(DEFAULT_DOCKER_NGINX_PROXY_PUBLISHED_PORTS);
268
+ for (const envConfig of Object.values(config.envs)) {
269
+ const port = normalizeEnvProxyConfig(envConfig.proxy)?.port;
270
+ if (port !== undefined) {
271
+ ports.add(port);
272
+ }
273
+ }
274
+ return Array.from(ports).sort((left, right) => left - right);
275
+ }
276
+ async function readDockerNginxPublishedPorts(containerName) {
277
+ const output = await commandOutput('docker', ['inspect', '--format', '{{json .HostConfig.PortBindings}}', containerName], { errorName: 'docker inspect' });
278
+ const parsed = JSON.parse(output.trim() || '{}');
279
+ const ports = new Set();
280
+ for (const bindings of Object.values(parsed)) {
281
+ if (!Array.isArray(bindings)) {
282
+ continue;
283
+ }
284
+ for (const binding of bindings) {
285
+ const port = Number.parseInt(String(binding?.HostPort ?? '').trim(), 10);
286
+ if (Number.isInteger(port) && port > 0) {
287
+ ports.add(port);
288
+ }
289
+ }
290
+ }
291
+ return Array.from(ports).sort((left, right) => left - right);
292
+ }
293
+ async function dockerNginxProxyContainerMatchesPublishedPorts(containerName, expectedPorts) {
294
+ const currentPorts = await readDockerNginxPublishedPorts(containerName);
295
+ return currentPorts.length === expectedPorts.length && currentPorts.every((port, index) => port === expectedPorts[index]);
296
+ }
297
+ async function removeDockerNginxProxyContainer(containerName) {
298
+ await run('docker', ['rm', '-f', containerName], {
299
+ errorName: 'docker rm',
300
+ stdio: 'ignore',
301
+ });
302
+ }
258
303
  async function startDockerNginxProxy(runtimeContext) {
259
304
  const containerName = await resolveNginxProxyContainerName();
260
305
  await ensureNginxProxyMainConfig(runtimeContext);
261
- if (await dockerContainerExists(containerName)) {
306
+ const existedBeforeEnsure = await dockerContainerExists(containerName);
307
+ await ensureDockerNginxProxyContainer(runtimeContext);
308
+ if (existedBeforeEnsure && (await dockerContainerExists(containerName))) {
262
309
  const state = await startDockerContainer(containerName, { stdio: 'ignore' });
263
310
  return state === 'already-running' ? 'already-running' : 'started';
264
311
  }
265
- await ensureDockerNginxProxyContainer(runtimeContext);
266
312
  return 'started';
267
313
  }
268
314
  async function stopDockerNginxProxy() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.2.0-alpha.5",
3
+ "version": "2.2.0-alpha.6",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -144,5 +144,5 @@
144
144
  "type": "git",
145
145
  "url": "git+https://github.com/nocobase/nocobase.git"
146
146
  },
147
- "gitHead": "814ff497dd1233ca7218c9f7f2a016c2ead6516d"
147
+ "gitHead": "f55b252811f6af01d08cd3814171867b70c8ef7a"
148
148
  }