@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
@@ -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,11 +83,19 @@ 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({
59
- description: 'Port (overrides appPort from env config when set)',
60
- char: 'p',
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',
61
96
  required: false,
97
+ default: true,
98
+ allowNo: true,
62
99
  }),
63
100
  daemon: Flags.boolean({
64
101
  description: 'Run the application as a daemon after stopping it (default: true; use --no-daemon to stay in the foreground)',
@@ -67,12 +104,6 @@ export default class AppRestart extends Command {
67
104
  default: true,
68
105
  allowNo: true,
69
106
  }),
70
- instances: Flags.integer({
71
- description: 'Number of instances to run after stopping it',
72
- char: 'i',
73
- required: false,
74
- }),
75
- 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
76
107
  verbose: Flags.boolean({
77
108
  description: 'Show raw shutdown/startup output from the underlying local or Docker command',
78
109
  default: false,
@@ -80,8 +111,10 @@ export default class AppRestart extends Command {
80
111
  };
81
112
  async run() {
82
113
  const { flags } = await this.parse(AppRestart);
114
+ const quickstart = flags.quickstart ?? true;
83
115
  const requestedEnv = flags.env?.trim() || undefined;
84
116
  const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
117
+ const shouldSyncLicensedPlugins = flags['sync-licensed-plugins'] ?? true;
85
118
  if (explicitEnvSelection) {
86
119
  const confirmed = await ensureCrossEnvConfirmed({
87
120
  command: this,
@@ -93,12 +126,41 @@ export default class AppRestart extends Command {
93
126
  }
94
127
  }
95
128
  const runtime = await resolveManagedAppRuntime(requestedEnv);
129
+ const runCommand = this.config.runCommand.bind(this.config);
96
130
  const commandStdio = flags.verbose ? 'inherit' : 'ignore';
97
131
  if (!runtime) {
98
132
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
99
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
+ }
100
163
  if (runtime.kind === 'docker') {
101
- announceTargetEnv(runtime.envName);
102
164
  startTask(`Stopping NocoBase for "${runtime.envName}" before restart...`);
103
165
  try {
104
166
  const state = await stopDockerContainer(runtime.containerName, {
@@ -129,10 +191,11 @@ export default class AppRestart extends Command {
129
191
  failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
130
192
  this.error(formatDockerRestartFailure(runtime.envName, message));
131
193
  }
132
- const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : 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);
133
196
  await waitForAppReady({
134
197
  envName: runtime.envName,
135
- apiBaseUrl: resolveManagedAppApiBaseUrl(runtime),
198
+ apiBaseUrl,
136
199
  containerName: runtime.containerName,
137
200
  logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
138
201
  ...(flags.verbose ? { verbose: true } : {}),
@@ -149,17 +212,19 @@ export default class AppRestart extends Command {
149
212
  if (flags.verbose) {
150
213
  stopArgv.push('--verbose');
151
214
  }
152
- await this.config.runCommand('app:stop', stopArgv);
215
+ await runWithSuppressedTargetEnvLog(async () => {
216
+ await runCommand('app:stop', stopArgv);
217
+ });
153
218
  const startArgv = [...stopArgv];
154
- if (flags.quickstart) {
219
+ if (quickstart) {
155
220
  startArgv.push('--quickstart');
156
221
  }
157
- pushFlag(startArgv, '--port', flags.port);
222
+ startArgv.push('--no-sync-licensed-plugins');
158
223
  if (daemonFlagWasProvided) {
159
224
  startArgv.push(flags.daemon === false ? '--no-daemon' : '--daemon');
160
225
  }
161
- pushFlag(startArgv, '--instances', flags.instances);
162
- pushFlag(startArgv, '--launch-mode', flags['launch-mode']);
163
- await this.config.runCommand('app:start', startArgv);
226
+ await runWithSuppressedTargetEnvLog(async () => {
227
+ await runCommand('app:start', startArgv);
228
+ });
164
229
  }
165
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], {
@@ -189,7 +267,6 @@ export default class AppStart extends Command {
189
267
  onFailTask: failTask,
190
268
  });
191
269
  if (runtime.source === 'npm' || runtime.source === 'git') {
192
- const runCommand = this.config.runCommand.bind(this.config);
193
270
  const downloadableRuntime = runtime;
194
271
  await ensureSavedLocalSource(downloadableRuntime, runCommand, {
195
272
  verbose: flags.verbose,
@@ -199,13 +276,10 @@ export default class AppStart extends Command {
199
276
  });
200
277
  }
201
278
  const npmArgs = ['start'];
202
- if (flags.quickstart) {
279
+ if (quickstart) {
203
280
  npmArgs.push('--quickstart');
204
281
  }
205
- if (flags.port) {
206
- npmArgs.push('--port', flags.port);
207
- }
208
- else if (runtime.env.appPort !== undefined &&
282
+ if (runtime.env.appPort !== undefined &&
209
283
  runtime.env.appPort !== null &&
210
284
  String(runtime.env.appPort).trim() !== '') {
211
285
  npmArgs.push('--port', String(runtime.env.appPort));
@@ -213,20 +287,14 @@ export default class AppStart extends Command {
213
287
  if (flags.daemon !== false) {
214
288
  npmArgs.push('--daemon');
215
289
  }
216
- if (flags.instances !== undefined) {
217
- npmArgs.push('--instances', flags.instances.toString());
218
- }
219
- if (flags['launch-mode']) {
220
- npmArgs.push('--launch-mode', flags['launch-mode']);
221
- }
222
- const effectivePort = flags.port ||
223
- (runtime.env.appPort !== undefined && runtime.env.appPort !== null
224
- ? String(runtime.env.appPort).trim()
225
- : undefined);
226
- const appUrl = formatAppUrl(effectivePort);
290
+ const effectivePort = runtime.env.appPort !== undefined && runtime.env.appPort !== null
291
+ ? String(runtime.env.appPort).trim()
292
+ : undefined;
227
293
  const apiBaseUrl = resolveManagedAppApiBaseUrl(runtime, {
228
294
  portOverride: effectivePort,
229
295
  });
296
+ const appUrl = resolveDisplayAppUrl(apiBaseUrl, effectivePort, runtime.env.config?.appPublicPath);
297
+ let defaultCdnBaseUrl;
230
298
  if (await isAppReady(apiBaseUrl, { requestTimeoutMs: 1_500 })) {
231
299
  if (flags.daemon === false) {
232
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.`);
@@ -238,8 +306,23 @@ export default class AppStart extends Command {
238
306
  }
239
307
  return;
240
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
+ }
241
323
  try {
242
324
  await ensureLocalPostinstall(runtime, {
325
+ env: managedAppLifecycleEnvVars(),
243
326
  verbose: flags.verbose,
244
327
  onStartTask: startTask,
245
328
  onSucceedTask: succeedTask,
@@ -249,6 +332,20 @@ export default class AppStart extends Command {
249
332
  catch (error) {
250
333
  this.error(error instanceof Error ? error.message : String(error));
251
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
+ }
252
349
  if (flags.daemon === false) {
253
350
  printInfo(`Starting NocoBase for "${runtime.envName}" in the foreground${appUrl ? ` at ${appUrl}` : ''}. Press Ctrl+C to stop.`);
254
351
  }
@@ -256,7 +353,14 @@ export default class AppStart extends Command {
256
353
  startTask(`Starting NocoBase for "${runtime.envName}" in the background...`);
257
354
  }
258
355
  try {
356
+ const startEnv = defaultCdnBaseUrl
357
+ ? {
358
+ ...managedAppLifecycleEnvVars(),
359
+ CDN_BASE_URL: defaultCdnBaseUrl,
360
+ }
361
+ : managedAppLifecycleEnvVars();
259
362
  await runLocalNocoBaseCommand(runtime, npmArgs, {
363
+ env: startEnv,
260
364
  stdio: commandStdio,
261
365
  });
262
366
  if (flags.daemon !== false) {