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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -3
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +89 -24
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +142 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +9 -5
  27. package/dist/commands/config/delete.js +6 -1
  28. package/dist/commands/config/get.js +6 -1
  29. package/dist/commands/config/set.js +7 -2
  30. package/dist/commands/env/add.js +24 -44
  31. package/dist/commands/env/info.js +3 -2
  32. package/dist/commands/env/proxy/caddy.js +28 -0
  33. package/dist/commands/env/proxy/index.js +353 -0
  34. package/dist/commands/env/proxy/nginx.js +28 -0
  35. package/dist/commands/env/remove.js +112 -22
  36. package/dist/commands/env/shared.js +17 -9
  37. package/dist/commands/env/update.js +385 -21
  38. package/dist/commands/init.js +350 -98
  39. package/dist/commands/install.js +218 -75
  40. package/dist/commands/license/activate.js +63 -244
  41. package/dist/commands/license/plugins/shared.js +64 -13
  42. package/dist/commands/plugin/import.js +108 -0
  43. package/dist/commands/revision/create.js +89 -0
  44. package/dist/commands/source/test.js +13 -14
  45. package/dist/lib/app-health.js +5 -5
  46. package/dist/lib/app-managed-resources.js +24 -8
  47. package/dist/lib/app-public-path.js +80 -0
  48. package/dist/lib/app-runtime.js +10 -1
  49. package/dist/lib/auth-store.js +119 -4
  50. package/dist/lib/cli-config.js +191 -3
  51. package/dist/lib/cli-entry-error.js +8 -0
  52. package/dist/lib/command-log.js +284 -0
  53. package/dist/lib/docker-env-file.js +10 -2
  54. package/dist/lib/env-auth.js +4 -1
  55. package/dist/lib/env-command-config.js +45 -0
  56. package/dist/lib/env-config.js +7 -2
  57. package/dist/lib/env-guard.js +9 -10
  58. package/dist/lib/env-paths.js +101 -0
  59. package/dist/lib/env-proxy.js +1295 -0
  60. package/dist/lib/inquirer.js +4 -5
  61. package/dist/lib/managed-env-file.js +98 -0
  62. package/dist/lib/plugin-import.js +279 -0
  63. package/dist/lib/prompt-catalog-core.js +3 -2
  64. package/dist/lib/prompt-validators.js +14 -0
  65. package/dist/lib/prompt-web-ui.js +8 -1
  66. package/dist/lib/run-npm.js +56 -12
  67. package/dist/lib/skills-manager.js +3 -1
  68. package/dist/lib/source-publish.js +18 -1
  69. package/dist/lib/ui.js +2 -1
  70. package/dist/locale/en-US.json +116 -22
  71. package/dist/locale/zh-CN.json +113 -19
  72. package/package.json +5 -7
  73. package/scripts/build.mjs +34 -0
  74. package/scripts/clean.mjs +9 -0
  75. package/tsconfig.json +19 -0
  76. package/LICENSE.txt +0 -107
  77. package/README.zh-CN.md +0 -355
@@ -0,0 +1,89 @@
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
+ import { Args, Command, Flags } from '@oclif/core';
10
+ import { executeRawApiRequest } from '../../lib/api-client.js';
11
+ import { ensureCrossEnvConfirmed } from '../../lib/env-guard.js';
12
+ export default class RevisionCreate extends Command {
13
+ static summary = 'Save the current NocoBase build as a restorable revision';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %> "description here"',
16
+ '<%= config.bin %> <%= command.id %> --env app1 "description here"',
17
+ ];
18
+ static args = {
19
+ description: Args.string({
20
+ required: true,
21
+ description: 'Revision description, up to 2000 characters.',
22
+ }),
23
+ };
24
+ static flags = {
25
+ 'api-base-url': Flags.string({
26
+ description: 'NocoBase API base URL, for example http://localhost:13000/api',
27
+ }),
28
+ env: Flags.string({
29
+ char: 'e',
30
+ description: 'Environment name',
31
+ }),
32
+ role: Flags.string({
33
+ description: 'Role override, sent as X-Role',
34
+ }),
35
+ token: Flags.string({
36
+ char: 't',
37
+ description: 'API key override',
38
+ }),
39
+ yes: Flags.boolean({
40
+ char: 'y',
41
+ description: 'Confirm using --env when it targets a different env than the current env',
42
+ default: false,
43
+ }),
44
+ 'json-output': Flags.boolean({
45
+ char: 'j',
46
+ description: 'Print raw JSON response',
47
+ default: true,
48
+ allowNo: true,
49
+ }),
50
+ };
51
+ async run() {
52
+ const { args, flags } = await this.parse(RevisionCreate);
53
+ const description = args.description;
54
+ if (description.length > 2000) {
55
+ this.error('Description length should not be over 2000');
56
+ }
57
+ const confirmed = await ensureCrossEnvConfirmed({
58
+ command: this,
59
+ requestedEnv: flags.env,
60
+ yes: flags.yes,
61
+ });
62
+ if (!confirmed) {
63
+ return;
64
+ }
65
+ const response = await executeRawApiRequest({
66
+ envName: flags.env,
67
+ baseUrl: flags['api-base-url'],
68
+ role: flags.role,
69
+ token: flags.token,
70
+ method: 'POST',
71
+ path: '/app:publishEvent',
72
+ body: {
73
+ plugin: 'plugin-version-control',
74
+ command: 'revision:create',
75
+ payload: {
76
+ description,
77
+ },
78
+ },
79
+ });
80
+ if (!response.ok) {
81
+ this.error(`Request failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`);
82
+ }
83
+ if (flags['json-output']) {
84
+ this.log(JSON.stringify(response.data, null, 2));
85
+ return;
86
+ }
87
+ this.log(`HTTP ${response.status}`);
88
+ }
89
+ }
@@ -14,7 +14,7 @@ import Install from '../install.js';
14
14
  import { defaultWorkspaceName } from '../../lib/app-runtime.js';
15
15
  import { resolveCliLocale } from '../../lib/cli-locale.js';
16
16
  import { findAvailableTcpPort, validateAvailableTcpPort } from '../../lib/prompt-validators.js';
17
- import { commandSucceeds, resolveProjectCwd, run, runNocoBaseCommand } from '../../lib/run-npm.js';
17
+ import { commandSucceeds, ensureDockerDaemonRunning, resolveProjectCwd, run, runNocoBaseCommand, } from '../../lib/run-npm.js';
18
18
  import { failTask, printInfo, setVerboseMode, startTask, succeedTask } from '../../lib/ui.js';
19
19
  const DEFAULT_DB_HOST = '127.0.0.1';
20
20
  const DEFAULT_DB_DATABASE = 'nocobase-test';
@@ -48,11 +48,11 @@ const DEFAULT_DB_PORTS = {
48
48
  };
49
49
  const TCP_PORT_READY_SCRIPT = [
50
50
  "const net = require('node:net');",
51
- "const port = Number(process.argv.at(-1));",
51
+ 'const port = Number(process.argv.at(-1));',
52
52
  "const socket = net.createConnection({ host: '127.0.0.1', port });",
53
53
  "socket.once('connect', () => { socket.end(); process.exit(0); });",
54
54
  "socket.once('error', () => process.exit(1));",
55
- "setTimeout(() => { socket.destroy(); process.exit(1); }, 200).unref();",
55
+ 'setTimeout(() => { socket.destroy(); process.exit(1); }, 200).unref();',
56
56
  ].join('\n');
57
57
  function inferTestEnv(paths) {
58
58
  const first = String(paths[0] ?? '').trim();
@@ -60,9 +60,7 @@ function inferTestEnv(paths) {
60
60
  return undefined;
61
61
  }
62
62
  const normalized = first.split('\\').join('/');
63
- if (normalized.includes('/client/')
64
- || normalized.includes('/client-v2/')
65
- || normalized.includes('/flow-engine/')) {
63
+ if (normalized.includes('/client/') || normalized.includes('/client-v2/') || normalized.includes('/flow-engine/')) {
66
64
  return 'client-side';
67
65
  }
68
66
  return 'server-side';
@@ -77,9 +75,7 @@ function defaultTestDbPort(dbDialect) {
77
75
  return String(DEFAULT_DB_PORTS[dbDialect] ?? DEFAULT_DB_PORTS.postgres);
78
76
  }
79
77
  function defaultTestDbImage(dbDialect) {
80
- const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
81
- ? DEFAULT_TEST_DB_IMAGES_ZH_CN
82
- : DEFAULT_TEST_DB_IMAGES;
78
+ const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN' ? DEFAULT_TEST_DB_IMAGES_ZH_CN : DEFAULT_TEST_DB_IMAGES;
83
79
  return defaults[dbDialect] ?? defaults.postgres;
84
80
  }
85
81
  function delay(ms) {
@@ -174,9 +170,11 @@ async function startTestDbDistributor(params) {
174
170
  if (code === 0) {
175
171
  return;
176
172
  }
177
- childError = childError ?? new Error(signal
178
- ? `test DB distributor exited due to signal ${signal}`
179
- : `test DB distributor exited with code ${code ?? 'unknown'}`);
173
+ childError =
174
+ childError ??
175
+ new Error(signal
176
+ ? `test DB distributor exited due to signal ${signal}`
177
+ : `test DB distributor exited with code ${code ?? 'unknown'}`);
180
178
  });
181
179
  try {
182
180
  await waitForTcpPortReady(port);
@@ -194,6 +192,7 @@ async function startTestDbDistributor(params) {
194
192
  };
195
193
  }
196
194
  async function ensureDockerNetwork(networkName, options) {
195
+ await ensureDockerDaemonRunning('prepare Docker resources for the built-in test database');
197
196
  if (await commandSucceeds('docker', ['network', 'inspect', networkName])) {
198
197
  return;
199
198
  }
@@ -440,8 +439,8 @@ export default class SourceTest extends Command {
440
439
  server: flags.server,
441
440
  client: flags.client,
442
441
  paths: args.paths ?? [],
443
- })
444
- && supportsTestDbDistributor(testDbConfig.env.DB_DIALECT)) {
442
+ }) &&
443
+ supportsTestDbDistributor(testDbConfig.env.DB_DIALECT)) {
445
444
  testDbDistributor = await startTestDbDistributor({
446
445
  cwd,
447
446
  env: testDbConfig.env,
@@ -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 { buildLocalApiBaseUrl, buildLocalAppUrl } from './app-public-path.js';
9
10
  import { startDockerLogFollower } from './docker-log-stream.js';
10
11
  import { printInfo } from './ui.js';
11
12
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
@@ -56,14 +57,13 @@ async function requestAppHealthCheck(params) {
56
57
  }
57
58
  export class AppHealthCheckError extends Error {
58
59
  }
59
- export function formatAppUrl(port) {
60
- const value = trimValue(port);
61
- return value ? `http://127.0.0.1:${value}` : undefined;
60
+ export function formatAppUrl(port, appPublicPath) {
61
+ return buildLocalAppUrl(port, appPublicPath);
62
62
  }
63
63
  export function resolveManagedAppApiBaseUrl(runtime, options) {
64
64
  const override = trimValue(options?.portOverride);
65
65
  if (override) {
66
- return `http://127.0.0.1:${override}/api`;
66
+ return buildLocalApiBaseUrl(override, runtime.env.config?.appPublicPath);
67
67
  }
68
68
  const baseUrl = trimValue(runtime.env.baseUrl);
69
69
  if (baseUrl) {
@@ -72,7 +72,7 @@ export function resolveManagedAppApiBaseUrl(runtime, options) {
72
72
  const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
73
73
  ? ''
74
74
  : trimValue(runtime.env.appPort);
75
- return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
75
+ return buildLocalApiBaseUrl(appPort, runtime.env.config?.appPublicPath);
76
76
  }
77
77
  export async function isAppReady(apiBaseUrl, options) {
78
78
  const baseUrl = trimValue(apiBaseUrl);
@@ -7,18 +7,20 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { mkdir, readdir } from 'node:fs/promises';
10
- import { dockerContainerExists, runLocalNocoBaseCommand, startDockerContainer } from './app-runtime.js';
10
+ import { resolveAppPublicPath } from './app-public-path.js';
11
+ import { dockerContainerExists, managedAppLifecycleEnvVars, runLocalNocoBaseCommand, startDockerContainer, } from './app-runtime.js';
11
12
  import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
12
- import { resolveConfiguredEnvPath } from './cli-home.js';
13
+ import { resolveConfiguredStoragePath } from './env-paths.js';
13
14
  import { resolveDockerEnvFileArg } from "./docker-env-file.js";
14
15
  import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "./docker-image.js";
15
- import { commandSucceeds, run } from './run-npm.js';
16
+ import { commandSucceeds, ensureDockerDaemonRunning, run } from './run-npm.js';
16
17
  import Install from '../commands/install.js';
17
18
  const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
18
19
  function commandStdio(verbose) {
19
20
  return verbose ? 'inherit' : 'ignore';
20
21
  }
21
22
  async function ensureDockerNetwork(networkName) {
23
+ await ensureDockerDaemonRunning('prepare Docker resources for this environment');
22
24
  if (await commandSucceeds('docker', ['network', 'inspect', networkName])) {
23
25
  return;
24
26
  }
@@ -44,6 +46,16 @@ function pushOptionalEnvArg(args, key, value) {
44
46
  args.push('-e', `${key}=${String(value)}`);
45
47
  }
46
48
  }
49
+ function resolveDockerClientAssetsExtractEnabled(envValue) {
50
+ const text = trimValue(envValue).toLowerCase();
51
+ if (!text) {
52
+ return true;
53
+ }
54
+ if (['0', 'false', 'no', 'off'].includes(text)) {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
47
59
  function normalizeDockerPlatform(value) {
48
60
  const text = trimValue(value);
49
61
  if (!text || text === 'auto') {
@@ -104,15 +116,13 @@ async function localProjectHasFiles(projectRoot) {
104
116
  }
105
117
  export async function buildSavedDockerRunArgs(runtime) {
106
118
  const config = runtime.env.config ?? {};
107
- const configuredStoragePath = trimValue(config.storagePath);
108
- const storagePath = configuredStoragePath
109
- ? trimValue(resolveConfiguredEnvPath(configuredStoragePath))
110
- : '';
119
+ const storagePath = trimValue(resolveConfiguredStoragePath(config));
111
120
  const envFile = await resolveDockerEnvFileArg(runtime.envName, config);
112
121
  const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
113
122
  ? ''
114
123
  : trimValue(runtime.env.appPort);
115
124
  const appKey = trimValue(config.appKey);
125
+ const appPublicPath = trimValue(config.appPublicPath);
116
126
  const timeZone = trimValue(config.timezone) || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
117
127
  const builtinDbConnection = config.builtinDb ? deriveBuiltinDbConnection(runtime) : undefined;
118
128
  const dbDialect = builtinDbConnection?.dbDialect || trimValue(config.dbDialect);
@@ -124,6 +134,7 @@ export async function buildSavedDockerRunArgs(runtime) {
124
134
  const dbSchema = trimValue(config.dbSchema);
125
135
  const dbTablePrefix = trimValue(config.dbTablePrefix);
126
136
  const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
137
+ const extractClientAssets = resolveDockerClientAssetsExtractEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
127
138
  const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
128
139
  const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
129
140
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
@@ -178,10 +189,13 @@ export async function buildSavedDockerRunArgs(runtime) {
178
189
  if (envFile) {
179
190
  args.push('--env-file', envFile);
180
191
  }
181
- args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
192
+ const lifecycleEnvVars = managedAppLifecycleEnvVars();
193
+ args.push('-e', `APP_ENV=${lifecycleEnvVars.APP_ENV}`, '-e', `NODE_ENV=${lifecycleEnvVars.NODE_ENV}`, '-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
194
+ pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath ? resolveAppPublicPath(appPublicPath) : undefined);
182
195
  pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
183
196
  pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
184
197
  pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
198
+ pushOptionalEnvArg(args, 'NOCOBASE_EXTRACT_CLIENT_ASSETS', extractClientAssets);
185
199
  args.push(imageRef);
186
200
  return {
187
201
  appPort: appPort || undefined,
@@ -220,6 +234,7 @@ export async function ensureBuiltinDbReady(runtime, options) {
220
234
  envName: runtime.envName,
221
235
  workspaceName: runtime.workspaceName,
222
236
  dockerContainerPrefix: runtime.dockerContainerPrefix,
237
+ appPath: config.appPath,
223
238
  storagePath: config.storagePath,
224
239
  source: runtime.source,
225
240
  dbDialect: builtinDbConnection.dbDialect,
@@ -305,6 +320,7 @@ export async function ensureLocalPostinstall(runtime, options) {
305
320
  options?.onStartTask?.(`Running local postinstall for "${runtime.envName}"...`);
306
321
  try {
307
322
  await runLocalNocoBaseCommand(runtime, ['postinstall'], {
323
+ env: options?.env,
308
324
  stdio: commandStdio(options?.verbose),
309
325
  });
310
326
  options?.onSucceedTask?.(`Local postinstall finished for "${runtime.envName}".`);
@@ -0,0 +1,80 @@
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
+ import { readFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ const DEFAULT_APP_PUBLIC_PATH = '/';
12
+ const DEFAULT_DIST_SEGMENT = 'dist';
13
+ const DIST_CLIENT_DIR = 'dist-client';
14
+ const DIST_CLIENT_ACTIVE_VERSION_FILE = 'active-version';
15
+ function trimValue(value) {
16
+ const text = String(value ?? '').trim();
17
+ return text || undefined;
18
+ }
19
+ export function resolveAppPublicPath(appPublicPath = DEFAULT_APP_PUBLIC_PATH) {
20
+ const normalized = String(appPublicPath || DEFAULT_APP_PUBLIC_PATH)
21
+ .trim()
22
+ .replace(/\/+/g, '/')
23
+ .replace(/\/?$/, '/');
24
+ const withLeadingSlash = normalized.startsWith('/') ? normalized : `/${normalized}`;
25
+ return withLeadingSlash || DEFAULT_APP_PUBLIC_PATH;
26
+ }
27
+ export function appendAppPublicPath(appPublicPath, segment, options) {
28
+ const publicPath = resolveAppPublicPath(appPublicPath).replace(/\/+$/, '');
29
+ const normalizedSegment = String(segment || '')
30
+ .trim()
31
+ .replace(/^\/+/, '')
32
+ .replace(options?.trailingSlash ? /\/+$/ : /\/*$/, '');
33
+ const joinedPath = normalizedSegment ? `${publicPath}/${normalizedSegment}` : publicPath || '/';
34
+ if (options?.trailingSlash) {
35
+ return joinedPath.endsWith('/') ? joinedPath : `${joinedPath}/`;
36
+ }
37
+ return joinedPath || '/';
38
+ }
39
+ export function resolveDistPublicPath(appPublicPath) {
40
+ return appendAppPublicPath(appPublicPath, DEFAULT_DIST_SEGMENT, { trailingSlash: true });
41
+ }
42
+ export function buildDefaultCdnBaseUrl(appPublicPath, version) {
43
+ const normalizedVersion = String(version || '')
44
+ .trim()
45
+ .replace(/^\/+|\/+$/g, '');
46
+ return `${resolveDistPublicPath(appPublicPath)}${normalizedVersion}/`;
47
+ }
48
+ export function buildLocalAppUrl(port, appPublicPath) {
49
+ const value = trimValue(port);
50
+ if (!value) {
51
+ return undefined;
52
+ }
53
+ const publicPath = resolveAppPublicPath(appPublicPath);
54
+ return publicPath === '/' ? `http://127.0.0.1:${value}` : `http://127.0.0.1:${value}${publicPath}`;
55
+ }
56
+ export function buildLocalApiBaseUrl(port, appPublicPath) {
57
+ const value = trimValue(port);
58
+ if (!value) {
59
+ return undefined;
60
+ }
61
+ return `http://127.0.0.1:${value}${appendAppPublicPath(appPublicPath, 'api', { trailingSlash: false })}`;
62
+ }
63
+ export function resolveDistClientRoot(storagePath) {
64
+ return path.join(storagePath, DIST_CLIENT_DIR);
65
+ }
66
+ export function resolveDistClientVersionRoot(storagePath, version) {
67
+ return path.join(resolveDistClientRoot(storagePath), version);
68
+ }
69
+ export function resolveDistClientActiveVersionFile(storagePath) {
70
+ return path.join(resolveDistClientRoot(storagePath), DIST_CLIENT_ACTIVE_VERSION_FILE);
71
+ }
72
+ export async function readDistClientActiveVersion(storagePath) {
73
+ try {
74
+ const content = await readFile(resolveDistClientActiveVersionFile(storagePath), 'utf8');
75
+ return trimValue(content);
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import { resolveEnvKind } from './auth-store.js';
11
11
  import { getEnv, loadAuthConfig } from './auth-store.js';
12
12
  import { DEFAULT_DOCKER_CONTAINER_PREFIX, DEFAULT_DOCKER_NETWORK, getEffectiveCliConfigValue, } from './cli-config.js';
13
+ import { resolveManagedLocalEnvFilePath } from './managed-env-file.js';
13
14
  import { commandOutput, commandSucceeds, run, runNocoBaseCommand } from './run-npm.js';
14
15
  import { buildRuntimeEnvVars } from './runtime-env-vars.js';
15
16
  const DOCKER_APP_WORKDIR = '/app/nocobase';
@@ -84,7 +85,7 @@ export async function resolveManagedAppRuntime(envName) {
84
85
  env,
85
86
  envName: resolvedName,
86
87
  source: source === 'git' ? 'git' : source === 'npm' ? 'npm' : 'local',
87
- projectRoot: env.appRootPath,
88
+ projectRoot: env.sourcePath,
88
89
  dockerNetworkName,
89
90
  dockerContainerPrefix,
90
91
  workspaceName: dockerNetworkName,
@@ -115,12 +116,20 @@ export function formatMissingManagedAppEnvMessage(envName) {
115
116
  }
116
117
  return 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.';
117
118
  }
119
+ export function managedAppLifecycleEnvVars() {
120
+ return {
121
+ APP_ENV: 'production',
122
+ NODE_ENV: 'production',
123
+ };
124
+ }
118
125
  export async function runLocalNocoBaseCommand(runtime, args, options) {
119
126
  const envVars = await buildRuntimeEnvVars(runtime);
127
+ const appEnvPath = resolveManagedLocalEnvFilePath(runtime);
120
128
  await runNocoBaseCommand(args, {
121
129
  cwd: runtime.projectRoot,
122
130
  env: {
123
131
  ...envVars,
132
+ APP_ENV_PATH: appEnvPath,
124
133
  ...options?.env,
125
134
  },
126
135
  stdio: options?.stdio,
@@ -8,8 +8,10 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
+ import { resolveAppPublicPath } from './app-public-path.js';
11
12
  import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
12
13
  import { normalizeCliLocale } from './cli-locale.js';
14
+ import { inferConfiguredAppPathFromLegacyConfig, resolveConfiguredAppPath, resolveConfiguredSourcePath, resolveConfiguredStoragePath, } from './env-paths.js';
13
15
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
14
16
  function normalizeStoredEnvKind(value) {
15
17
  const kind = String(value ?? '').trim();
@@ -62,7 +64,7 @@ export function resolveEnvKind(config) {
62
64
  if (source === 'npm' || source === 'git' || source === 'local') {
63
65
  return 'local';
64
66
  }
65
- if (String(config.appRootPath ?? '').trim()) {
67
+ if (String(config.appPath ?? '').trim() || String(config.appRootPath ?? '').trim()) {
66
68
  return 'local';
67
69
  }
68
70
  if (readEnvApiBaseUrl(config) || config.auth) {
@@ -81,16 +83,31 @@ function normalizeEnvConfigEntry(entry) {
81
83
  ...rest,
82
84
  ...(normalizedKind ? { kind: normalizedKind } : {}),
83
85
  ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
86
+ ...(normalizeOptionalString(entry.appPublicPath) ? { appPublicPath: resolveAppPublicPath(entry.appPublicPath) } : {}),
84
87
  };
85
88
  }
86
89
  function normalizeAuthConfig(config) {
87
90
  const settings = config.settings ?? {};
88
91
  const locale = normalizeOptionalCliLocale(settings.locale);
92
+ const defaultUiHost = normalizeOptionalString(settings.init?.defaultUiHost);
93
+ const defaultApiHost = normalizeOptionalString(settings.init?.defaultApiHost);
89
94
  const updatePolicy = normalizeOptionalCliUpdatePolicy(settings.update?.policy);
95
+ const logRetentionDays = typeof settings.log?.retentionDays === 'number' && Number.isInteger(settings.log.retentionDays)
96
+ ? settings.log.retentionDays
97
+ : undefined;
98
+ const logEnabled = typeof settings.log?.enabled === 'boolean' ? settings.log.enabled : undefined;
90
99
  return {
91
100
  name: config.name || config.dockerResourcePrefix,
92
101
  settings: {
93
102
  ...(locale ? { locale } : {}),
103
+ ...(defaultUiHost || defaultApiHost
104
+ ? {
105
+ init: {
106
+ ...(defaultUiHost ? { defaultUiHost } : {}),
107
+ ...(defaultApiHost ? { defaultApiHost } : {}),
108
+ },
109
+ }
110
+ : {}),
94
111
  ...(updatePolicy ? { update: { policy: updatePolicy } } : {}),
95
112
  ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
96
113
  ...(settings.docker?.network || settings.docker?.containerPrefix
@@ -103,15 +120,39 @@ function normalizeAuthConfig(config) {
103
120
  },
104
121
  }
105
122
  : {}),
106
- ...(settings.bin?.docker || settings.bin?.git || settings.bin?.yarn
123
+ ...(settings.bin?.docker || settings.bin?.caddy || settings.bin?.git || settings.bin?.nginx || settings.bin?.yarn
107
124
  ? {
108
125
  bin: {
109
126
  ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
127
+ ...(settings.bin?.caddy ? { caddy: normalizeOptionalString(settings.bin.caddy) } : {}),
110
128
  ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
129
+ ...(settings.bin?.nginx ? { nginx: normalizeOptionalString(settings.bin.nginx) } : {}),
111
130
  ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
112
131
  },
113
132
  }
114
133
  : {}),
134
+ ...(settings.proxy?.nbCliRoot ||
135
+ settings.proxy?.upstreamHost ||
136
+ settings.proxy?.host
137
+ ? {
138
+ proxy: {
139
+ ...(settings.proxy?.nbCliRoot ? { nbCliRoot: normalizeOptionalString(settings.proxy.nbCliRoot) } : {}),
140
+ ...(settings.proxy?.upstreamHost || settings.proxy?.host
141
+ ? {
142
+ upstreamHost: normalizeOptionalString(settings.proxy?.upstreamHost ?? settings.proxy.host),
143
+ }
144
+ : {}),
145
+ },
146
+ }
147
+ : {}),
148
+ ...(logEnabled !== undefined || logRetentionDays !== undefined
149
+ ? {
150
+ log: {
151
+ ...(logEnabled !== undefined ? { enabled: logEnabled } : {}),
152
+ ...(logRetentionDays !== undefined ? { retentionDays: logRetentionDays } : {}),
153
+ },
154
+ }
155
+ : {}),
115
156
  },
116
157
  lastEnv: config.lastEnv ||
117
158
  config.currentEnv ||
@@ -205,7 +246,29 @@ export class Env {
205
246
  return configuredPath;
206
247
  }
207
248
  }
208
- return resolveConfiguredEnvPath(this.config.appRootPath) ?? resolveEnvRelativePath('.');
249
+ const legacyPath = resolveConfiguredEnvPath(this.config.appRootPath);
250
+ if (legacyPath) {
251
+ return legacyPath;
252
+ }
253
+ return this.kind === 'local' ? this.sourcePath : resolveEnvRelativePath('.');
254
+ }
255
+ get appPath() {
256
+ if (this.kind === 'ssh') {
257
+ const configuredPath = String(this.config.appPath ?? inferConfiguredAppPathFromLegacyConfig(this.config) ?? '').trim();
258
+ if (configuredPath) {
259
+ return configuredPath;
260
+ }
261
+ }
262
+ return resolveConfiguredAppPath(this.config) ?? resolveEnvRelativePath('.');
263
+ }
264
+ get sourcePath() {
265
+ if (this.kind === 'ssh') {
266
+ const configuredPath = String(this.config.appRootPath ?? '').trim();
267
+ if (configuredPath) {
268
+ return configuredPath;
269
+ }
270
+ }
271
+ return resolveConfiguredSourcePath(this.config) ?? path.join(this.appPath, 'source');
209
272
  }
210
273
  get storagePath() {
211
274
  if (this.kind === 'ssh') {
@@ -214,7 +277,13 @@ export class Env {
214
277
  return configuredPath;
215
278
  }
216
279
  }
217
- return resolveConfiguredEnvPath(this.config.storagePath) ?? resolveEnvRelativePath('.');
280
+ const resolvedStoragePath = resolveConfiguredStoragePath(this.config);
281
+ if (resolvedStoragePath) {
282
+ return resolvedStoragePath;
283
+ }
284
+ return this.kind === 'local' || this.kind === 'docker'
285
+ ? path.join(this.appPath, 'storage')
286
+ : resolveEnvRelativePath('.');
218
287
  }
219
288
  get appPort() {
220
289
  return this.config.appPort;
@@ -230,6 +299,8 @@ export class Env {
230
299
  out[key] = String(value);
231
300
  };
232
301
  put('APP_PORT', this.appPort);
302
+ put('APP_PUBLIC_PATH', this.config.appPublicPath ? resolveAppPublicPath(this.config.appPublicPath) : undefined);
303
+ put('CDN_BASE_URL', this.config.cdnBaseUrl);
233
304
  put('APP_KEY', this.config.appKey);
234
305
  put('TZ', this.config.timezone);
235
306
  put('DB_DIALECT', this.config.dbDialect);
@@ -358,6 +429,39 @@ export async function updateEnvConnection(envName, updates, options = {}) {
358
429
  };
359
430
  }, options);
360
431
  }
432
+ export async function replaceEnvConfig(envName, config, options = {}) {
433
+ await writeEnv(envName, (previous) => {
434
+ if (!previous) {
435
+ throw new Error(`Env "${envName}" is not configured`);
436
+ }
437
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, ...rest } = config;
438
+ const nextApiBaseUrl = readEnvApiBaseUrl(config);
439
+ const previousApiBaseUrl = readEnvApiBaseUrl(previous);
440
+ const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
441
+ const previousAuthType = resolveConfiguredAuthType(previous);
442
+ const nextAuthType = normalizeConfiguredAuthType(authType) ?? (accessToken ? 'token' : undefined);
443
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(authUsername) : undefined;
444
+ const nextAuth = accessToken
445
+ ? {
446
+ type: 'token',
447
+ accessToken,
448
+ }
449
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
450
+ ? previous.auth
451
+ : undefined;
452
+ const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
453
+ const authTypeChanged = previousAuthType !== nextAuthType;
454
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
455
+ return {
456
+ ...rest,
457
+ ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
458
+ ...(nextAuthType ? { authType: nextAuthType } : {}),
459
+ ...(nextAuthUsername ? { authUsername: nextAuthUsername } : {}),
460
+ ...(nextAuth ? { auth: nextAuth } : {}),
461
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
462
+ };
463
+ }, options);
464
+ }
361
465
  export async function setEnvOauthSession(envName, auth, options = {}) {
362
466
  await writeEnv(envName, (previous) => ({
363
467
  ...previous,
@@ -375,6 +479,17 @@ export async function setEnvRuntime(envName, runtime, options = {}) {
375
479
  };
376
480
  await saveAuthConfig(config, options);
377
481
  }
482
+ export async function clearEnvRootSetup(envName, options = {}) {
483
+ const config = await loadExactAuthConfig(options);
484
+ const current = config.envs[envName];
485
+ if (!current) {
486
+ return false;
487
+ }
488
+ const { rootUsername: _rootUsername, rootEmail: _rootEmail, rootPassword: _rootPassword, rootNickname: _rootNickname, ...rest } = current;
489
+ config.envs[envName] = rest;
490
+ await saveAuthConfig(config, options);
491
+ return true;
492
+ }
378
493
  export async function removeEnv(envName, options = {}) {
379
494
  const config = await loadExactAuthConfig(options);
380
495
  if (!config.envs[envName]) {