@nocobase/cli 2.1.0-alpha.45 → 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 (87) 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 -32
  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 +92 -20
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +143 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +11 -7
  27. package/dist/commands/config/delete.js +10 -2
  28. package/dist/commands/config/get.js +7 -1
  29. package/dist/commands/config/set.js +12 -2
  30. package/dist/commands/db/ps.js +5 -18
  31. package/dist/commands/env/add.js +24 -44
  32. package/dist/commands/env/auth.js +1 -1
  33. package/dist/commands/env/info.js +3 -2
  34. package/dist/commands/env/list.js +4 -5
  35. package/dist/commands/env/proxy/caddy.js +28 -0
  36. package/dist/commands/env/proxy/index.js +353 -0
  37. package/dist/commands/env/proxy/nginx.js +28 -0
  38. package/dist/commands/env/remove.js +112 -22
  39. package/dist/commands/env/shared.js +17 -9
  40. package/dist/commands/env/status.js +10 -7
  41. package/dist/commands/env/update.js +385 -21
  42. package/dist/commands/init.js +368 -98
  43. package/dist/commands/install.js +271 -104
  44. package/dist/commands/license/activate.js +63 -244
  45. package/dist/commands/license/plugins/shared.js +64 -13
  46. package/dist/commands/plugin/import.js +108 -0
  47. package/dist/commands/revision/create.js +89 -0
  48. package/dist/commands/self/update.js +58 -1
  49. package/dist/commands/source/dev.js +5 -5
  50. package/dist/commands/source/test.js +13 -14
  51. package/dist/lib/app-health.js +39 -32
  52. package/dist/lib/app-managed-resources.js +25 -9
  53. package/dist/lib/app-public-path.js +80 -0
  54. package/dist/lib/app-runtime.js +12 -3
  55. package/dist/lib/auth-store.js +128 -4
  56. package/dist/lib/backup.js +3 -3
  57. package/dist/lib/bootstrap.js +14 -8
  58. package/dist/lib/cli-config.js +229 -3
  59. package/dist/lib/cli-entry-error.js +52 -0
  60. package/dist/lib/command-log.js +284 -0
  61. package/dist/lib/docker-env-file.js +10 -2
  62. package/dist/lib/docker-log-stream.js +45 -0
  63. package/dist/lib/env-auth.js +25 -14
  64. package/dist/lib/env-command-config.js +45 -0
  65. package/dist/lib/env-config.js +7 -2
  66. package/dist/lib/env-guard.js +9 -10
  67. package/dist/lib/env-paths.js +101 -0
  68. package/dist/lib/env-proxy.js +1295 -0
  69. package/dist/lib/inquirer.js +4 -5
  70. package/dist/lib/managed-env-file.js +98 -0
  71. package/dist/lib/plugin-import.js +279 -0
  72. package/dist/lib/prompt-catalog-core.js +3 -2
  73. package/dist/lib/prompt-validators.js +14 -0
  74. package/dist/lib/prompt-web-ui.js +8 -1
  75. package/dist/lib/run-npm.js +56 -12
  76. package/dist/lib/skills-manager.js +3 -1
  77. package/dist/lib/source-publish.js +22 -2
  78. package/dist/lib/startup-update.js +74 -50
  79. package/dist/lib/ui.js +2 -1
  80. package/dist/locale/en-US.json +122 -22
  81. package/dist/locale/zh-CN.json +119 -19
  82. package/package.json +5 -7
  83. package/scripts/build.mjs +34 -0
  84. package/scripts/clean.mjs +9 -0
  85. package/tsconfig.json +19 -0
  86. package/LICENSE.txt +0 -107
  87. 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
+ }
@@ -8,14 +8,29 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { confirm } from "../../lib/inquirer.js";
11
+ import { updateNocoBaseSkills } from '../../lib/skills-manager.js';
11
12
  import { setVerboseMode } from '../../lib/ui.js';
12
13
  import { formatSelfUpdateUnavailableMessage, formatUnsupportedSelfUpdateMessage, inspectSelfStatus, updateSelf, } from '../../lib/self-manager.js';
14
+ function formatSkillsUpdateMessage(result, verbose) {
15
+ if (result.action === 'noop') {
16
+ if (result.reason === 'not-installed') {
17
+ return verbose
18
+ ? 'NocoBase AI coding skills are not installed globally. Run `nb skills install` first.'
19
+ : 'Skipped skills update because NocoBase AI coding skills are not installed.';
20
+ }
21
+ return verbose
22
+ ? 'NocoBase AI coding skills are already up to date globally.'
23
+ : 'NocoBase AI coding skills are up to date.';
24
+ }
25
+ return verbose ? 'Updated the global NocoBase AI coding skills.' : 'Updated NocoBase AI coding skills globally.';
26
+ }
13
27
  export default class SelfUpdate extends Command {
14
28
  static summary = 'Update the globally installed NocoBase CLI';
15
29
  static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm install.';
16
30
  static examples = [
17
31
  '<%= config.bin %> <%= command.id %>',
18
32
  '<%= config.bin %> <%= command.id %> --yes',
33
+ '<%= config.bin %> <%= command.id %> --skills',
19
34
  '<%= config.bin %> <%= command.id %> --channel alpha --json',
20
35
  ];
21
36
  static flags = {
@@ -33,6 +48,10 @@ export default class SelfUpdate extends Command {
33
48
  description: 'Output the result as JSON',
34
49
  default: false,
35
50
  }),
51
+ skills: Flags.boolean({
52
+ description: 'Also update the globally installed NocoBase AI coding skills',
53
+ default: false,
54
+ }),
36
55
  verbose: Flags.boolean({
37
56
  description: 'Show detailed update output',
38
57
  default: false,
@@ -50,11 +69,14 @@ export default class SelfUpdate extends Command {
50
69
  if (!status.latestVersion && status.registryError) {
51
70
  this.error(formatSelfUpdateUnavailableMessage(status));
52
71
  }
72
+ let shouldUpdateSkills = flags.skills;
53
73
  if (!flags.yes && status.updateAvailable) {
54
74
  let confirmed = false;
55
75
  try {
56
76
  confirmed = await confirm({
57
- message: `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`,
77
+ message: flags.skills
78
+ ? `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion} and refresh the globally installed NocoBase AI coding skills?`
79
+ : `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`,
58
80
  default: false,
59
81
  });
60
82
  }
@@ -69,6 +91,24 @@ export default class SelfUpdate extends Command {
69
91
  channel: flags.channel,
70
92
  verbose: flags.verbose,
71
93
  });
94
+ if (flags.skills && !flags.yes && !status.updateAvailable) {
95
+ let confirmed = false;
96
+ try {
97
+ confirmed = await confirm({
98
+ message: 'Update the globally installed NocoBase AI coding skills?',
99
+ default: true,
100
+ });
101
+ }
102
+ catch {
103
+ return;
104
+ }
105
+ shouldUpdateSkills = confirmed;
106
+ }
107
+ const skillsResult = shouldUpdateSkills
108
+ ? await updateNocoBaseSkills({
109
+ verbose: flags.verbose,
110
+ })
111
+ : undefined;
72
112
  if (flags.json) {
73
113
  this.log(JSON.stringify({
74
114
  ok: true,
@@ -79,6 +119,17 @@ export default class SelfUpdate extends Command {
79
119
  channel: result.status.channel,
80
120
  fromVersion: result.status.currentVersion,
81
121
  toVersion: result.targetVersion,
122
+ skills: skillsResult
123
+ ? {
124
+ action: skillsResult.action,
125
+ reason: skillsResult.action === 'noop' ? skillsResult.reason : undefined,
126
+ globalRoot: skillsResult.status.globalRoot,
127
+ workspaceRoot: skillsResult.status.workspaceRoot,
128
+ installedSkillNames: skillsResult.status.installedSkillNames,
129
+ installedVersion: skillsResult.status.installedVersion,
130
+ installedRef: skillsResult.status.installedRef,
131
+ }
132
+ : undefined,
82
133
  }, null, 2));
83
134
  return;
84
135
  }
@@ -86,10 +137,16 @@ export default class SelfUpdate extends Command {
86
137
  this.log(flags.verbose
87
138
  ? `NocoBase CLI is already up to date at ${result.status.currentVersion}.`
88
139
  : `NocoBase CLI is up to date: ${result.status.currentVersion}.`);
140
+ if (skillsResult) {
141
+ this.log(formatSkillsUpdateMessage(skillsResult, flags.verbose));
142
+ }
89
143
  return;
90
144
  }
91
145
  this.log(flags.verbose
92
146
  ? `Updated NocoBase CLI from ${result.status.currentVersion} using ${result.packageSpec}${result.targetVersion ? ` (latest ${result.status.channel} resolves to ${result.targetVersion})` : ''}.`
93
147
  : `Updated NocoBase CLI: ${result.status.currentVersion} -> ${result.targetVersion}.`);
148
+ if (skillsResult) {
149
+ this.log(formatSkillsUpdateMessage(skillsResult, flags.verbose));
150
+ }
94
151
  }
95
152
  }
@@ -16,20 +16,20 @@ function formatUnsupportedRuntimeMessage(kind, envName) {
16
16
  return [
17
17
  `Can't run dev mode for "${envName}".`,
18
18
  'This env is managed by Docker, but `nb source dev` requires a local npm or Git source directory.',
19
- `Use \`nb app logs --env ${envName}\` to inspect the Docker app, or create a source-based env with \`nb init --env ${envName} --source git\`.`,
19
+ `Use \`nb app logs --env ${envName}\` to inspect the Docker app, or create a source-based env with \`nb init --ui --env ${envName} --source git\`.`,
20
20
  ].join('\n');
21
21
  }
22
22
  if (kind === 'ssh') {
23
23
  return [
24
24
  `Can't run dev mode for "${envName}" yet.`,
25
25
  'SSH env support is reserved but not implemented yet.',
26
- `Create a source-based env with \`nb init --env ${envName} --source git\` if you want local development mode right now.`,
26
+ `Create a source-based env with \`nb init --ui --env ${envName} --source git\` if you want local development mode right now.`,
27
27
  ].join('\n');
28
28
  }
29
29
  return [
30
30
  `Can't run dev mode for "${envName}".`,
31
31
  'This env only has an API connection, but `nb source dev` requires a local npm or Git source directory.',
32
- `Create a source-based env with \`nb init --env ${envName} --source git\` if you want local development mode.`,
32
+ `Create a source-based env with \`nb init --ui --env ${envName} --source git\` if you want local development mode.`,
33
33
  ].join('\n');
34
34
  }
35
35
  function appUrlForPort(port) {
@@ -129,8 +129,8 @@ export default class SourceDev extends Command {
129
129
  this.error(formatUnsupportedRuntimeMessage(runtime.kind, runtime.envName));
130
130
  }
131
131
  announceTargetEnv(runtime.envName);
132
- const devPort = flags.port
133
- || (runtime.env.appPort !== undefined && runtime.env.appPort !== null
132
+ const devPort = flags.port ||
133
+ (runtime.env.appPort !== undefined && runtime.env.appPort !== null
134
134
  ? String(runtime.env.appPort).trim()
135
135
  : undefined);
136
136
  const appUrl = appUrlForPort(devPort);
@@ -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,8 @@
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';
10
+ import { startDockerLogFollower } from './docker-log-stream.js';
9
11
  import { printInfo } from './ui.js';
10
12
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
11
13
  const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
@@ -55,14 +57,13 @@ async function requestAppHealthCheck(params) {
55
57
  }
56
58
  export class AppHealthCheckError extends Error {
57
59
  }
58
- export function formatAppUrl(port) {
59
- const value = trimValue(port);
60
- return value ? `http://127.0.0.1:${value}` : undefined;
60
+ export function formatAppUrl(port, appPublicPath) {
61
+ return buildLocalAppUrl(port, appPublicPath);
61
62
  }
62
63
  export function resolveManagedAppApiBaseUrl(runtime, options) {
63
64
  const override = trimValue(options?.portOverride);
64
65
  if (override) {
65
- return `http://127.0.0.1:${override}/api`;
66
+ return buildLocalApiBaseUrl(override, runtime.env.config?.appPublicPath);
66
67
  }
67
68
  const baseUrl = trimValue(runtime.env.baseUrl);
68
69
  if (baseUrl) {
@@ -71,7 +72,7 @@ export function resolveManagedAppApiBaseUrl(runtime, options) {
71
72
  const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
72
73
  ? ''
73
74
  : trimValue(runtime.env.appPort);
74
- return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
75
+ return buildLocalApiBaseUrl(appPort, runtime.env.config?.appPublicPath);
75
76
  }
76
77
  export async function isAppReady(apiBaseUrl, options) {
77
78
  const baseUrl = trimValue(apiBaseUrl);
@@ -99,34 +100,40 @@ export async function waitForAppReady(params) {
99
100
  let lastMessage = 'No response yet';
100
101
  let nextProgressLogAt = startedAt + progressLogIntervalMs;
101
102
  printInfo(`Waiting for NocoBase to become ready for "${params.envName}"...`);
102
- while (Date.now() - startedAt < timeoutMs) {
103
- const result = await requestAppHealthCheck({
104
- healthCheckUrl,
105
- fetchImpl: params.fetchImpl,
106
- requestTimeoutMs: params.requestTimeoutMs,
107
- });
108
- if (result.ok) {
109
- return;
110
- }
111
- lastMessage = result.message;
112
- const now = Date.now();
113
- if (now >= nextProgressLogAt) {
114
- const elapsedSeconds = Math.max(1, Math.floor((now - startedAt) / 1000));
115
- printInfo(`Still waiting for "${params.envName}"... (${elapsedSeconds}s elapsed)`);
116
- while (nextProgressLogAt <= now) {
117
- nextProgressLogAt += progressLogIntervalMs;
103
+ const dockerLogFollower = params.verbose && params.containerName ? startDockerLogFollower(params.containerName) : undefined;
104
+ try {
105
+ while (Date.now() - startedAt < timeoutMs) {
106
+ const result = await requestAppHealthCheck({
107
+ healthCheckUrl,
108
+ fetchImpl: params.fetchImpl,
109
+ requestTimeoutMs: params.requestTimeoutMs,
110
+ });
111
+ if (result.ok) {
112
+ return;
118
113
  }
114
+ lastMessage = result.message;
115
+ const now = Date.now();
116
+ if (now >= nextProgressLogAt) {
117
+ const elapsedSeconds = Math.max(1, Math.floor((now - startedAt) / 1000));
118
+ printInfo(`Still waiting for "${params.envName}"... (${elapsedSeconds}s elapsed)`);
119
+ while (nextProgressLogAt <= now) {
120
+ nextProgressLogAt += progressLogIntervalMs;
121
+ }
122
+ }
123
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
124
+ if (remainingMs <= 0) {
125
+ break;
126
+ }
127
+ await sleep(Math.min(intervalMs, remainingMs));
119
128
  }
120
- const remainingMs = timeoutMs - (Date.now() - startedAt);
121
- if (remainingMs <= 0) {
122
- break;
123
- }
124
- await sleep(Math.min(intervalMs, remainingMs));
129
+ const hints = [
130
+ params.logHint,
131
+ params.containerName ? `docker logs ${params.containerName}` : undefined,
132
+ ].filter(Boolean);
133
+ const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
134
+ throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
135
+ }
136
+ finally {
137
+ await dockerLogFollower?.stop();
125
138
  }
126
- const hints = [
127
- params.logHint,
128
- params.containerName ? `docker logs ${params.containerName}` : undefined,
129
- ].filter(Boolean);
130
- const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
131
- throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
132
139
  }
@@ -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') {
@@ -82,7 +94,7 @@ function formatSavedDockerSettingsIncomplete(envName, missing) {
82
94
  return [
83
95
  `Can't start NocoBase for "${envName}" yet.`,
84
96
  `The saved Docker settings for this env are incomplete. Missing: ${missing.join(', ')}.`,
85
- 'Re-run `nb init` or `nb env add` to refresh this env config, then try again.',
97
+ `Re-run \`nb init --ui --env ${envName}\` to refresh this env config, then try again.`,
86
98
  ].join('\n');
87
99
  }
88
100
  function formatDockerAppRecreateFailure(envName, message) {
@@ -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,
@@ -110,17 +111,25 @@ export function formatMissingManagedAppEnvMessage(envName) {
110
111
  if (requested) {
111
112
  return [
112
113
  `Env "${requested}" is not configured in this workspace.`,
113
- `If you want to create a new NocoBase AI environment, run \`nb init --env ${requested}\` first.`,
114
+ `If you want to create a new NocoBase AI environment, run \`nb init --ui --env ${requested}\` first.`,
114
115
  ].join('\n');
115
116
  }
116
- return 'No NocoBase env is configured yet. Run `nb init` to create one first.';
117
+ return 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.';
118
+ }
119
+ export function managedAppLifecycleEnvVars() {
120
+ return {
121
+ APP_ENV: 'production',
122
+ NODE_ENV: 'production',
123
+ };
117
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,