@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
@@ -8,11 +8,14 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { removeEnv } from '../../lib/auth-store.js';
11
- import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { formatMissingManagedAppEnvMessage, managedAppLifecycleEnvVars, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
12
12
  import { hasExplicitEnvSelection } from '../../lib/env-guard.js';
13
+ import { resolveEnvProxyEntryDir, resolveLegacyNginxEnvProxyAppOutputPath } from "../../lib/env-proxy.js";
14
+ import { resolveConfiguredStoragePath } from '../../lib/env-paths.js';
13
15
  import { input } from "../../lib/inquirer.js";
14
16
  import { announceTargetEnv, failTask, isInteractiveTerminal, printInfo, startTask, succeedTask } from '../../lib/ui.js';
15
- import { builtinDbContainerName, managedDockerNetworkName, removeDockerContainerIfExists, removeDockerNetworkIfUnused, removePathIfExists, resolveConfiguredPath, resolveManagedLocalAppPath, shouldRemoveManagedLocalAppFiles, } from './shared.js';
17
+ import path from 'node:path';
18
+ import { builtinDbContainerName, managedDockerNetworkName, removeDockerContainerIfExists, removeDockerNetworkIfUnused, removePathIfExists, resolveManagedLocalAppPath, shouldRemoveManagedLocalAppFiles, } from './shared.js';
16
19
  function formatDestroyFailure(envName, message) {
17
20
  return [
18
21
  `Couldn't destroy env "${envName}".`,
@@ -50,9 +53,7 @@ function buildDestroyPrompt(runtime, options) {
50
53
  if (options.removesManagedLocalAppFiles) {
51
54
  lines.push('CLI-managed local app files will also be removed.');
52
55
  }
53
- else if (runtime.kind === 'local') {
54
- lines.push('Custom local app source files will be kept.');
55
- }
56
+ lines.push('Env-specific proxy entry files generated by `nb env proxy` will also be removed when present.');
56
57
  if (options.removesStorageData) {
57
58
  lines.push('Storage data will be removed.');
58
59
  }
@@ -91,8 +92,8 @@ async function confirmDestroy(runtime, options) {
91
92
  }
92
93
  }
93
94
  export default class AppDestroy extends Command {
94
- static hidden = false;
95
- static description = 'Destroy the selected env by removing managed runtime resources, storage data, and the saved CLI env config.';
95
+ static hidden = true;
96
+ static description = 'Deprecated compatibility alias for `nb env remove <name> --purge`.';
96
97
  static examples = [
97
98
  '<%= config.bin %> <%= command.id %> --env app1',
98
99
  '<%= config.bin %> <%= command.id %> --env app1 --force',
@@ -124,7 +125,7 @@ export default class AppDestroy extends Command {
124
125
  Boolean(resolveManagedLocalAppPath(runtime)) &&
125
126
  shouldRemoveManagedLocalAppFiles(runtime);
126
127
  const removesStorageData = (runtime.kind === 'local' || runtime.kind === 'docker') &&
127
- Boolean(resolveConfiguredPath(runtime.env.config.storagePath));
128
+ Boolean(resolveConfiguredStoragePath(runtime.env.config));
128
129
  let confirmed = false;
129
130
  try {
130
131
  confirmed = await confirmDestroy(runtime, {
@@ -154,6 +155,7 @@ export default class AppDestroy extends Command {
154
155
  else if (runtime.kind === 'local') {
155
156
  startTask(`Stopping local NocoBase app for "${runtime.envName}"...`);
156
157
  await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
158
+ env: managedAppLifecycleEnvVars(),
157
159
  stdio: flags.verbose ? 'inherit' : 'ignore',
158
160
  });
159
161
  succeedTask(`Local NocoBase app stopped for "${runtime.envName}".`);
@@ -193,14 +195,21 @@ export default class AppDestroy extends Command {
193
195
  await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`);
194
196
  succeedTask(`Managed local app files removed for "${runtime.envName}".`);
195
197
  }
196
- else if (localAppPath) {
197
- printInfo(`Keeping custom local app files for "${runtime.envName}" at "${localAppPath}".`);
198
- }
199
198
  else {
200
199
  printInfo(`No saved local app path found for "${runtime.envName}".`);
201
200
  }
202
201
  }
203
- const configuredStoragePath = resolveConfiguredPath(runtime.env.config.storagePath);
202
+ const proxyEntryDirs = [
203
+ resolveEnvProxyEntryDir(runtime.envName, { provider: 'nginx' }),
204
+ resolveEnvProxyEntryDir(runtime.envName, { provider: 'caddy' }),
205
+ path.dirname(resolveLegacyNginxEnvProxyAppOutputPath(runtime.envName)),
206
+ ];
207
+ startTask(`Removing env proxy entry files for "${runtime.envName}"...`);
208
+ for (const proxyEntryDir of proxyEntryDirs) {
209
+ await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`);
210
+ }
211
+ succeedTask(`Env proxy entry files removed for "${runtime.envName}".`);
212
+ const configuredStoragePath = resolveConfiguredStoragePath(runtime.env.config);
204
213
  if (configuredStoragePath) {
205
214
  startTask(`Removing storage data for "${runtime.envName}"...`);
206
215
  await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`);
@@ -10,7 +10,7 @@ import { Command, Flags } from '@oclif/core';
10
10
  import { printWarning } from '../../lib/ui.js';
11
11
  export default class AppDown extends Command {
12
12
  static hidden = true;
13
- static description = 'Deprecated compatibility alias for `nb app stop --with-db` and `nb app destroy`.';
13
+ static description = 'Deprecated compatibility alias for `nb app stop --with-db` and `nb env remove <name> --purge`.';
14
14
  static examples = [
15
15
  '<%= config.bin %> <%= command.id %> --env app1',
16
16
  '<%= config.bin %> <%= command.id %> --env app1 --all --force',
@@ -51,7 +51,7 @@ export default class AppDown extends Command {
51
51
  argv.push('--verbose');
52
52
  }
53
53
  if (flags.all) {
54
- printWarning('`nb app down --all` is deprecated. Use `nb app destroy` instead.');
54
+ printWarning('`nb app down --all` is deprecated. Use `nb env remove <name> --purge` instead.');
55
55
  if (flags.force || flags.yes) {
56
56
  argv.push('--force');
57
57
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
- import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { formatMissingManagedAppEnvMessage, managedAppLifecycleEnvVars, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
12
12
  import { run } from '../../lib/run-npm.js';
13
13
  import { printInfo } from '../../lib/ui.js';
14
14
  function formatLogsFailure(envName, message) {
@@ -103,6 +103,7 @@ export default class AppLogs extends Command {
103
103
  localArgs.push('--nostream');
104
104
  }
105
105
  await runLocalNocoBaseCommand(runtime, localArgs, {
106
+ env: managedAppLifecycleEnvVars(),
106
107
  stdio: 'inherit',
107
108
  });
108
109
  }
@@ -11,6 +11,7 @@ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-
11
11
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, stopDockerContainer, } from '../../lib/app-runtime.js';
12
12
  import { formatAppUrl, resolveManagedAppApiBaseUrl, waitForAppReady } from '../../lib/app-health.js';
13
13
  import { recreateSavedDockerApp } from '../../lib/app-managed-resources.js';
14
+ import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
14
15
  import { run } from '../../lib/run-npm.js';
15
16
  import { announceTargetEnv, failTask, startTask, succeedTask } from '../../lib/ui.js';
16
17
  function argvHasToken(argv, tokens) {
@@ -21,6 +22,31 @@ function pushFlag(argv, flag, value) {
21
22
  argv.push(flag, String(value));
22
23
  }
23
24
  }
25
+ function buildLicenseSyncArgv(envName, options) {
26
+ const argv = ['--env', envName, '--skip-if-no-license'];
27
+ if (options.yes || options.explicitEnvSelection) {
28
+ argv.push('--yes');
29
+ }
30
+ if (options.verbose) {
31
+ argv.push('--verbose');
32
+ }
33
+ return argv;
34
+ }
35
+ async function runWithSuppressedTargetEnvLog(task) {
36
+ const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
37
+ process.env.NB_SKIP_TARGET_ENV_LOG = '1';
38
+ try {
39
+ return await task();
40
+ }
41
+ finally {
42
+ if (previousTargetEnv === undefined) {
43
+ delete process.env.NB_SKIP_TARGET_ENV_LOG;
44
+ }
45
+ else {
46
+ process.env.NB_SKIP_TARGET_ENV_LOG = previousTargetEnv;
47
+ }
48
+ }
49
+ }
24
50
  function formatDockerRestartFailure(envName, message) {
25
51
  return [
26
52
  `Couldn't restart NocoBase for "${envName}".`,
@@ -29,18 +55,21 @@ function formatDockerRestartFailure(envName, message) {
29
55
  `Details: ${message}`,
30
56
  ].join('\n');
31
57
  }
58
+ function resolveDisplayAppUrl(apiBaseUrl, port, appPublicPath) {
59
+ const resolvedFromApiBaseUrl = resolveAppUrlFromApiBaseUrl(apiBaseUrl);
60
+ if (resolvedFromApiBaseUrl) {
61
+ return resolvedFromApiBaseUrl;
62
+ }
63
+ return formatAppUrl(port, appPublicPath);
64
+ }
32
65
  export default class AppRestart extends Command {
33
66
  static hidden = false;
34
- static description = 'Restart NocoBase for the selected env. Local npm/git installs stop and start the app again, and Docker installs recreate the saved app container so saved env changes can take effect.';
67
+ static description = 'Restart NocoBase for the selected env. When applicable, the CLI synchronizes licensed commercial plugins first, then restarts the local app or recreates the saved Docker container.';
35
68
  static examples = [
36
69
  '<%= config.bin %> <%= command.id %>',
37
70
  '<%= config.bin %> <%= command.id %> --env local',
38
- '<%= config.bin %> <%= command.id %> --env local --quickstart',
39
- '<%= config.bin %> <%= command.id %> --env local --port 12000',
40
71
  '<%= config.bin %> <%= command.id %> --env local --daemon',
41
72
  '<%= config.bin %> <%= command.id %> --env local --no-daemon',
42
- '<%= config.bin %> <%= command.id %> --env local --instances 2',
43
- '<%= config.bin %> <%= command.id %> --env local --launch-mode pm2',
44
73
  '<%= config.bin %> <%= command.id %> --env local --verbose',
45
74
  '<%= config.bin %> <%= command.id %> --env local-docker',
46
75
  ];
@@ -54,8 +83,20 @@ export default class AppRestart extends Command {
54
83
  description: 'Confirm using --env when it targets a different env than the current env',
55
84
  default: false,
56
85
  }),
57
- quickstart: Flags.boolean({ description: 'Quickstart the application after stopping it', required: false }),
58
- port: Flags.string({ description: 'Port (overrides appPort from env config when set)', char: 'p', required: false }),
86
+ quickstart: Flags.boolean({
87
+ hidden: true,
88
+ description: 'Quickstart the application after stopping it',
89
+ required: false,
90
+ default: true,
91
+ allowNo: true,
92
+ }),
93
+ 'sync-licensed-plugins': Flags.boolean({
94
+ hidden: true,
95
+ description: 'Synchronize licensed commercial plugins before restarting the application',
96
+ required: false,
97
+ default: true,
98
+ allowNo: true,
99
+ }),
59
100
  daemon: Flags.boolean({
60
101
  description: 'Run the application as a daemon after stopping it (default: true; use --no-daemon to stay in the foreground)',
61
102
  char: 'd',
@@ -63,8 +104,6 @@ export default class AppRestart extends Command {
63
104
  default: true,
64
105
  allowNo: true,
65
106
  }),
66
- instances: Flags.integer({ description: 'Number of instances to run after stopping it', char: 'i', required: false }),
67
- 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
68
107
  verbose: Flags.boolean({
69
108
  description: 'Show raw shutdown/startup output from the underlying local or Docker command',
70
109
  default: false,
@@ -72,8 +111,10 @@ export default class AppRestart extends Command {
72
111
  };
73
112
  async run() {
74
113
  const { flags } = await this.parse(AppRestart);
114
+ const quickstart = flags.quickstart ?? true;
75
115
  const requestedEnv = flags.env?.trim() || undefined;
76
116
  const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
117
+ const shouldSyncLicensedPlugins = flags['sync-licensed-plugins'] ?? true;
77
118
  if (explicitEnvSelection) {
78
119
  const confirmed = await ensureCrossEnvConfirmed({
79
120
  command: this,
@@ -85,12 +126,41 @@ export default class AppRestart extends Command {
85
126
  }
86
127
  }
87
128
  const runtime = await resolveManagedAppRuntime(requestedEnv);
129
+ const runCommand = this.config.runCommand.bind(this.config);
88
130
  const commandStdio = flags.verbose ? 'inherit' : 'ignore';
89
131
  if (!runtime) {
90
132
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
91
133
  }
134
+ if (runtime.kind === 'http') {
135
+ this.error([
136
+ `Can't restart "${runtime.envName}" from this machine.`,
137
+ 'This env only has an API connection, so there is no saved local app or Docker runtime to restart here.',
138
+ 'Connect it to a local checkout or reinstall it with npm, git, or Docker if you want CLI-managed restart.',
139
+ ].join('\n'));
140
+ }
141
+ if (runtime.kind === 'ssh') {
142
+ this.error([
143
+ `Can't restart "${runtime.envName}" yet.`,
144
+ 'SSH env support is reserved but not implemented yet.',
145
+ 'Use a local or Docker env if you need CLI-managed restart right now.',
146
+ ].join('\n'));
147
+ }
148
+ announceTargetEnv(runtime.envName);
149
+ if (shouldSyncLicensedPlugins) {
150
+ try {
151
+ await runWithSuppressedTargetEnvLog(async () => {
152
+ await runCommand('license:plugins:sync', buildLicenseSyncArgv(runtime.envName, {
153
+ explicitEnvSelection,
154
+ yes: flags.yes,
155
+ verbose: flags.verbose,
156
+ }));
157
+ });
158
+ }
159
+ catch (error) {
160
+ this.error(error instanceof Error ? error.message : String(error));
161
+ }
162
+ }
92
163
  if (runtime.kind === 'docker') {
93
- announceTargetEnv(runtime.envName);
94
164
  startTask(`Stopping NocoBase for "${runtime.envName}" before restart...`);
95
165
  try {
96
166
  const state = await stopDockerContainer(runtime.containerName, {
@@ -121,14 +191,14 @@ export default class AppRestart extends Command {
121
191
  failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
122
192
  this.error(formatDockerRestartFailure(runtime.envName, message));
123
193
  }
124
- const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null
125
- ? undefined
126
- : String(runtime.env.appPort));
194
+ const apiBaseUrl = resolveManagedAppApiBaseUrl(runtime);
195
+ const appUrl = resolveDisplayAppUrl(apiBaseUrl, runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort), runtime.env.config?.appPublicPath);
127
196
  await waitForAppReady({
128
197
  envName: runtime.envName,
129
- apiBaseUrl: resolveManagedAppApiBaseUrl(runtime),
198
+ apiBaseUrl,
130
199
  containerName: runtime.containerName,
131
200
  logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
201
+ ...(flags.verbose ? { verbose: true } : {}),
132
202
  });
133
203
  succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
134
204
  return;
@@ -142,17 +212,19 @@ export default class AppRestart extends Command {
142
212
  if (flags.verbose) {
143
213
  stopArgv.push('--verbose');
144
214
  }
145
- await this.config.runCommand('app:stop', stopArgv);
215
+ await runWithSuppressedTargetEnvLog(async () => {
216
+ await runCommand('app:stop', stopArgv);
217
+ });
146
218
  const startArgv = [...stopArgv];
147
- if (flags.quickstart) {
219
+ if (quickstart) {
148
220
  startArgv.push('--quickstart');
149
221
  }
150
- pushFlag(startArgv, '--port', flags.port);
222
+ startArgv.push('--no-sync-licensed-plugins');
151
223
  if (daemonFlagWasProvided) {
152
224
  startArgv.push(flags.daemon === false ? '--no-daemon' : '--daemon');
153
225
  }
154
- pushFlag(startArgv, '--instances', flags.instances);
155
- pushFlag(startArgv, '--launch-mode', flags['launch-mode']);
156
- await this.config.runCommand('app:start', startArgv);
226
+ await runWithSuppressedTargetEnvLog(async () => {
227
+ await runCommand('app:start', startArgv);
228
+ });
157
229
  }
158
230
  }
@@ -11,6 +11,7 @@ import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { buildDockerDbContainerName } from '../../lib/app-runtime.js';
13
13
  import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
14
+ import { resolveConfiguredAppPath } from '../../lib/env-paths.js';
14
15
  import { commandOutput, run } from '../../lib/run-npm.js';
15
16
  export function resolveConfiguredPath(value) {
16
17
  return resolveConfiguredEnvPath(value);
@@ -115,8 +116,8 @@ export function managedDockerNetworkName(runtime) {
115
116
  return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
116
117
  }
117
118
  export function resolveManagedLocalAppPath(runtime) {
118
- return resolveConfiguredPath(runtime.env.config.appRootPath) || runtime.projectRoot;
119
+ return resolveConfiguredAppPath(runtime.env.config) || runtime.projectRoot || resolveConfiguredPath(runtime.env.config.appRootPath);
119
120
  }
120
121
  export function shouldRemoveManagedLocalAppFiles(runtime) {
121
- return runtime.source === 'npm' || runtime.source === 'git';
122
+ return runtime.source === 'npm' || runtime.source === 'git' || runtime.source === 'local';
122
123
  }
@@ -7,18 +7,46 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
+ import { buildDefaultCdnBaseUrl, readDistClientActiveVersion } from '../../lib/app-public-path.js';
10
11
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
- import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
12
+ import { formatMissingManagedAppEnvMessage, managedAppLifecycleEnvVars, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
12
13
  import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBaseUrl, waitForAppReady, } from '../../lib/app-health.js';
13
14
  import { ensureBuiltinDbReady, ensureLocalPostinstall, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
15
+ import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
16
+ import { readManagedRuntimeEnvValues } from '../../lib/managed-env-file.js';
14
17
  import { run } from '../../lib/run-npm.js';
15
- import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
18
+ import { announceTargetEnv, failTask, printInfo, printWarning, startTask, succeedTask } from '../../lib/ui.js';
16
19
  function shouldPrintStartSuccess() {
17
20
  return process.env.NB_SKIP_APP_START_SUCCESS_LOG !== '1';
18
21
  }
19
22
  function argvHasToken(argv, tokens) {
20
23
  return tokens.some((token) => argv.includes(token));
21
24
  }
25
+ function buildLicenseSyncArgv(envName, options) {
26
+ const argv = ['--env', envName, '--skip-if-no-license'];
27
+ if (options.yes || options.explicitEnvSelection) {
28
+ argv.push('--yes');
29
+ }
30
+ if (options.verbose) {
31
+ argv.push('--verbose');
32
+ }
33
+ return argv;
34
+ }
35
+ async function runWithSuppressedTargetEnvLog(task) {
36
+ const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
37
+ process.env.NB_SKIP_TARGET_ENV_LOG = '1';
38
+ try {
39
+ return await task();
40
+ }
41
+ finally {
42
+ if (previousTargetEnv === undefined) {
43
+ delete process.env.NB_SKIP_TARGET_ENV_LOG;
44
+ }
45
+ else {
46
+ process.env.NB_SKIP_TARGET_ENV_LOG = previousTargetEnv;
47
+ }
48
+ }
49
+ }
22
50
  function formatDockerStartFailure(envName, message) {
23
51
  return [
24
52
  `Couldn't start NocoBase for "${envName}".`,
@@ -53,18 +81,49 @@ function formatLocalReadyFailure(envName, message, options) {
53
81
  `Details: ${message}`,
54
82
  ].join('\n');
55
83
  }
84
+ function formatLocalClientExtractWarning(envName, message) {
85
+ return [
86
+ `Client assets were not extracted for "${envName}".`,
87
+ 'NocoBase will keep starting, but versioned client files for CDN or external distribution may be stale or missing.',
88
+ `Details: ${message}`,
89
+ ].join('\n');
90
+ }
91
+ function resolveDisplayAppUrl(apiBaseUrl, port, appPublicPath) {
92
+ const resolvedFromApiBaseUrl = resolveAppUrlFromApiBaseUrl(apiBaseUrl);
93
+ if (resolvedFromApiBaseUrl) {
94
+ return resolvedFromApiBaseUrl;
95
+ }
96
+ return formatAppUrl(port, appPublicPath);
97
+ }
98
+ async function resolveDefaultLocalCdnBaseUrl(runtime) {
99
+ let envValues = {};
100
+ if (runtime.env.config && typeof runtime.env.config === 'object') {
101
+ ({ envValues } = await readManagedRuntimeEnvValues(runtime));
102
+ }
103
+ const explicitProcessCdnBaseUrl = String(process.env.CDN_BASE_URL ?? '').trim();
104
+ const explicitSavedCdnBaseUrl = String(runtime.env.envVars.CDN_BASE_URL ?? '').trim();
105
+ const explicitEnvFileCdnBaseUrl = String(envValues.CDN_BASE_URL ?? '').trim();
106
+ if (explicitProcessCdnBaseUrl || explicitSavedCdnBaseUrl || explicitEnvFileCdnBaseUrl) {
107
+ return undefined;
108
+ }
109
+ const storagePath = String(runtime.env.storagePath ?? runtime.env.config?.storagePath ?? '').trim();
110
+ if (!storagePath) {
111
+ return undefined;
112
+ }
113
+ const activeVersion = await readDistClientActiveVersion(storagePath);
114
+ if (!activeVersion) {
115
+ return undefined;
116
+ }
117
+ return buildDefaultCdnBaseUrl(runtime.env.config?.appPublicPath || envValues.APP_PUBLIC_PATH, activeVersion);
118
+ }
56
119
  export default class AppStart extends Command {
57
120
  static hidden = false;
58
- static description = 'Start NocoBase for the selected env. Local npm/git installs run the app command, and Docker installs recreate the saved app container.';
121
+ static description = 'Start NocoBase for the selected env. When applicable, the CLI synchronizes licensed commercial plugins first, then prepares and starts the app or recreates the saved Docker container.';
59
122
  static examples = [
60
123
  '<%= config.bin %> <%= command.id %>',
61
124
  '<%= config.bin %> <%= command.id %> --env local',
62
- '<%= config.bin %> <%= command.id %> --env local --quickstart',
63
- '<%= config.bin %> <%= command.id %> --env local --port 12000',
64
125
  '<%= config.bin %> <%= command.id %> --env local --daemon',
65
126
  '<%= config.bin %> <%= command.id %> --env local --no-daemon',
66
- '<%= config.bin %> <%= command.id %> --env local --instances 2',
67
- '<%= config.bin %> <%= command.id %> --env local --launch-mode pm2',
68
127
  '<%= config.bin %> <%= command.id %> --env local --verbose',
69
128
  '<%= config.bin %> <%= command.id %> --env local-docker',
70
129
  ];
@@ -78,11 +137,19 @@ export default class AppStart extends Command {
78
137
  description: 'Confirm using --env when it targets a different env than the current env',
79
138
  default: false,
80
139
  }),
81
- quickstart: Flags.boolean({ description: 'Quickstart the application', required: false }),
82
- port: Flags.string({
83
- description: 'Port (overrides appPort from env config when set)',
84
- char: 'p',
140
+ quickstart: Flags.boolean({
141
+ hidden: true,
142
+ description: 'Quickstart the application',
85
143
  required: false,
144
+ default: true,
145
+ allowNo: true,
146
+ }),
147
+ 'sync-licensed-plugins': Flags.boolean({
148
+ hidden: true,
149
+ description: 'Synchronize licensed commercial plugins before starting the application',
150
+ required: false,
151
+ default: true,
152
+ allowNo: true,
86
153
  }),
87
154
  daemon: Flags.boolean({
88
155
  description: 'Run the application as a daemon (default: true; use --no-daemon to stay in the foreground)',
@@ -91,8 +158,6 @@ export default class AppStart extends Command {
91
158
  default: true,
92
159
  allowNo: true,
93
160
  }),
94
- instances: Flags.integer({ description: 'Number of instances to run', char: 'i', required: false }),
95
- 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
96
161
  verbose: Flags.boolean({
97
162
  description: 'Show raw startup output from the underlying local or Docker command',
98
163
  default: false,
@@ -100,8 +165,11 @@ export default class AppStart extends Command {
100
165
  };
101
166
  async run() {
102
167
  const { flags } = await this.parse(AppStart);
168
+ const quickstart = flags.quickstart ?? true;
103
169
  const requestedEnv = flags.env?.trim() || undefined;
104
- if (requestedEnv && hasExplicitEnvSelection(this.argv)) {
170
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
171
+ const shouldSyncLicensedPlugins = flags['sync-licensed-plugins'] ?? true;
172
+ if (explicitEnvSelection) {
105
173
  const confirmed = await ensureCrossEnvConfirmed({
106
174
  command: this,
107
175
  requestedEnv,
@@ -113,6 +181,7 @@ export default class AppStart extends Command {
113
181
  }
114
182
  const daemonFlagWasProvided = argvHasToken(this.argv, ['--daemon', '--no-daemon']);
115
183
  const runtime = await resolveManagedAppRuntime(requestedEnv);
184
+ const runCommand = this.config.runCommand.bind(this.config);
116
185
  const commandStdio = flags.verbose ? 'inherit' : 'ignore';
117
186
  if (!runtime) {
118
187
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
@@ -133,12 +202,7 @@ export default class AppStart extends Command {
133
202
  }
134
203
  announceTargetEnv(runtime.envName);
135
204
  if (runtime.kind === 'docker') {
136
- const unsupportedFlags = [
137
- flags.port ? '--port' : undefined,
138
- daemonFlagWasProvided ? (flags.daemon ? '--daemon' : '--no-daemon') : undefined,
139
- flags.instances !== undefined ? '--instances' : undefined,
140
- flags['launch-mode'] ? '--launch-mode' : undefined,
141
- ].filter(Boolean);
205
+ const unsupportedFlags = [daemonFlagWasProvided ? (flags.daemon ? '--daemon' : '--no-daemon') : undefined].filter(Boolean);
142
206
  if (unsupportedFlags.length > 0) {
143
207
  this.error([
144
208
  `Can't apply ${unsupportedFlags.join(', ')} to "${runtime.envName}".`,
@@ -146,14 +210,28 @@ export default class AppStart extends Command {
146
210
  `Run \`nb app start --env ${runtime.envName}\` to recreate the saved container, or recreate the env if you need different runtime settings.`,
147
211
  ].join('\n'));
148
212
  }
213
+ if (shouldSyncLicensedPlugins) {
214
+ try {
215
+ await runWithSuppressedTargetEnvLog(async () => {
216
+ await runCommand('license:plugins:sync', buildLicenseSyncArgv(runtime.envName, {
217
+ explicitEnvSelection,
218
+ yes: flags.yes,
219
+ verbose: flags.verbose,
220
+ }));
221
+ });
222
+ }
223
+ catch (error) {
224
+ this.error(error instanceof Error ? error.message : String(error));
225
+ }
226
+ }
149
227
  await ensureBuiltinDbReady(runtime, {
150
228
  verbose: flags.verbose,
151
229
  onStartTask: startTask,
152
230
  onSucceedTask: succeedTask,
153
231
  onFailTask: failTask,
154
232
  });
155
- const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort));
156
233
  const apiBaseUrl = resolveManagedAppApiBaseUrl(runtime);
234
+ const appUrl = resolveDisplayAppUrl(apiBaseUrl, runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort), runtime.env.config?.appPublicPath);
157
235
  startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
158
236
  try {
159
237
  await run('docker', ['rm', '-f', runtime.containerName], {
@@ -175,6 +253,7 @@ export default class AppStart extends Command {
175
253
  apiBaseUrl,
176
254
  containerName: runtime.containerName,
177
255
  logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
256
+ ...(flags.verbose ? { verbose: true } : {}),
178
257
  });
179
258
  if (shouldPrintStartSuccess()) {
180
259
  succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
@@ -188,7 +267,6 @@ export default class AppStart extends Command {
188
267
  onFailTask: failTask,
189
268
  });
190
269
  if (runtime.source === 'npm' || runtime.source === 'git') {
191
- const runCommand = this.config.runCommand.bind(this.config);
192
270
  const downloadableRuntime = runtime;
193
271
  await ensureSavedLocalSource(downloadableRuntime, runCommand, {
194
272
  verbose: flags.verbose,
@@ -198,13 +276,10 @@ export default class AppStart extends Command {
198
276
  });
199
277
  }
200
278
  const npmArgs = ['start'];
201
- if (flags.quickstart) {
279
+ if (quickstart) {
202
280
  npmArgs.push('--quickstart');
203
281
  }
204
- if (flags.port) {
205
- npmArgs.push('--port', flags.port);
206
- }
207
- else if (runtime.env.appPort !== undefined &&
282
+ if (runtime.env.appPort !== undefined &&
208
283
  runtime.env.appPort !== null &&
209
284
  String(runtime.env.appPort).trim() !== '') {
210
285
  npmArgs.push('--port', String(runtime.env.appPort));
@@ -212,20 +287,14 @@ export default class AppStart extends Command {
212
287
  if (flags.daemon !== false) {
213
288
  npmArgs.push('--daemon');
214
289
  }
215
- if (flags.instances !== undefined) {
216
- npmArgs.push('--instances', flags.instances.toString());
217
- }
218
- if (flags['launch-mode']) {
219
- npmArgs.push('--launch-mode', flags['launch-mode']);
220
- }
221
- const effectivePort = flags.port ||
222
- (runtime.env.appPort !== undefined && runtime.env.appPort !== null
223
- ? String(runtime.env.appPort).trim()
224
- : undefined);
225
- const appUrl = formatAppUrl(effectivePort);
290
+ const effectivePort = runtime.env.appPort !== undefined && runtime.env.appPort !== null
291
+ ? String(runtime.env.appPort).trim()
292
+ : undefined;
226
293
  const apiBaseUrl = resolveManagedAppApiBaseUrl(runtime, {
227
294
  portOverride: effectivePort,
228
295
  });
296
+ const appUrl = resolveDisplayAppUrl(apiBaseUrl, effectivePort, runtime.env.config?.appPublicPath);
297
+ let defaultCdnBaseUrl;
229
298
  if (await isAppReady(apiBaseUrl, { requestTimeoutMs: 1_500 })) {
230
299
  if (flags.daemon === false) {
231
300
  printInfo(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}. Use \`nb app stop --env ${runtime.envName}\` before starting it again in the foreground.`);
@@ -237,8 +306,23 @@ export default class AppStart extends Command {
237
306
  }
238
307
  return;
239
308
  }
309
+ if (shouldSyncLicensedPlugins) {
310
+ try {
311
+ await runWithSuppressedTargetEnvLog(async () => {
312
+ await runCommand('license:plugins:sync', buildLicenseSyncArgv(runtime.envName, {
313
+ explicitEnvSelection,
314
+ yes: flags.yes,
315
+ verbose: flags.verbose,
316
+ }));
317
+ });
318
+ }
319
+ catch (error) {
320
+ this.error(error instanceof Error ? error.message : String(error));
321
+ }
322
+ }
240
323
  try {
241
324
  await ensureLocalPostinstall(runtime, {
325
+ env: managedAppLifecycleEnvVars(),
242
326
  verbose: flags.verbose,
243
327
  onStartTask: startTask,
244
328
  onSucceedTask: succeedTask,
@@ -248,6 +332,20 @@ export default class AppStart extends Command {
248
332
  catch (error) {
249
333
  this.error(error instanceof Error ? error.message : String(error));
250
334
  }
335
+ try {
336
+ startTask(`Extracting client assets for "${runtime.envName}"...`);
337
+ await runLocalNocoBaseCommand(runtime, ['client:extract'], {
338
+ env: managedAppLifecycleEnvVars(),
339
+ stdio: commandStdio,
340
+ });
341
+ succeedTask(`Client assets are ready for "${runtime.envName}".`);
342
+ defaultCdnBaseUrl = await resolveDefaultLocalCdnBaseUrl(runtime);
343
+ }
344
+ catch (error) {
345
+ const message = error instanceof Error ? error.message : String(error);
346
+ failTask(`Failed to extract client assets for "${runtime.envName}".`);
347
+ printWarning(formatLocalClientExtractWarning(runtime.envName, message));
348
+ }
251
349
  if (flags.daemon === false) {
252
350
  printInfo(`Starting NocoBase for "${runtime.envName}" in the foreground${appUrl ? ` at ${appUrl}` : ''}. Press Ctrl+C to stop.`);
253
351
  }
@@ -255,7 +353,14 @@ export default class AppStart extends Command {
255
353
  startTask(`Starting NocoBase for "${runtime.envName}" in the background...`);
256
354
  }
257
355
  try {
356
+ const startEnv = defaultCdnBaseUrl
357
+ ? {
358
+ ...managedAppLifecycleEnvVars(),
359
+ CDN_BASE_URL: defaultCdnBaseUrl,
360
+ }
361
+ : managedAppLifecycleEnvVars();
258
362
  await runLocalNocoBaseCommand(runtime, npmArgs, {
363
+ env: startEnv,
259
364
  stdio: commandStdio,
260
365
  });
261
366
  if (flags.daemon !== false) {