@nocobase/cli 2.1.0-beta.24 → 2.1.0-beta.25

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 (58) hide show
  1. package/README.md +19 -0
  2. package/dist/commands/app/down.js +12 -6
  3. package/dist/commands/app/logs.js +2 -2
  4. package/dist/commands/app/start.js +2 -1
  5. package/dist/commands/app/stop.js +2 -1
  6. package/dist/commands/app/upgrade.js +116 -129
  7. package/dist/commands/config/delete.js +30 -0
  8. package/dist/commands/config/get.js +29 -0
  9. package/dist/commands/config/index.js +20 -0
  10. package/dist/commands/config/list.js +29 -0
  11. package/dist/commands/config/set.js +35 -0
  12. package/dist/commands/db/check.js +171 -65
  13. package/dist/commands/db/logs.js +2 -2
  14. package/dist/commands/db/shared.js +6 -5
  15. package/dist/commands/db/start.js +2 -1
  16. package/dist/commands/db/stop.js +2 -1
  17. package/dist/commands/env/info.js +6 -2
  18. package/dist/commands/env/shared.js +1 -1
  19. package/dist/commands/install.js +50 -35
  20. package/dist/commands/license/activate.js +360 -0
  21. package/dist/commands/license/env.js +94 -0
  22. package/dist/commands/license/generate-id.js +108 -0
  23. package/dist/commands/license/id.js +56 -0
  24. package/dist/commands/license/index.js +20 -0
  25. package/dist/commands/license/plugins/clean.js +101 -0
  26. package/dist/commands/license/plugins/index.js +20 -0
  27. package/dist/commands/license/plugins/list.js +50 -0
  28. package/dist/commands/license/plugins/shared.js +325 -0
  29. package/dist/commands/license/plugins/sync.js +269 -0
  30. package/dist/commands/license/shared.js +414 -0
  31. package/dist/commands/license/status.js +50 -0
  32. package/dist/commands/plugin/disable.js +2 -0
  33. package/dist/commands/plugin/enable.js +2 -0
  34. package/dist/commands/source/dev.js +2 -1
  35. package/dist/lib/api-client.js +74 -3
  36. package/dist/lib/app-managed-resources.js +10 -6
  37. package/dist/lib/app-runtime.js +29 -11
  38. package/dist/lib/auth-store.js +36 -68
  39. package/dist/lib/bootstrap.js +0 -4
  40. package/dist/lib/build-config.js +8 -0
  41. package/dist/lib/builtin-db.js +86 -0
  42. package/dist/lib/cli-config.js +176 -0
  43. package/dist/lib/cli-home.js +6 -21
  44. package/dist/lib/env-config.js +7 -0
  45. package/dist/lib/generated-command.js +24 -3
  46. package/dist/lib/plugin-storage.js +127 -0
  47. package/dist/lib/prompt-validators.js +4 -4
  48. package/dist/lib/run-npm.js +53 -0
  49. package/dist/lib/runtime-env-vars.js +32 -0
  50. package/dist/lib/runtime-generator.js +89 -10
  51. package/dist/lib/self-manager.js +57 -2
  52. package/dist/lib/skills-manager.js +2 -2
  53. package/dist/lib/startup-update.js +81 -6
  54. package/dist/lib/ui.js +3 -0
  55. package/dist/locale/en-US.json +0 -4
  56. package/dist/locale/zh-CN.json +0 -4
  57. package/nocobase-ctl.config.json +82 -0
  58. package/package.json +13 -4
package/README.md CHANGED
@@ -336,6 +336,23 @@ nb api resource get --resource users --filter-by-tk 1 -e app1
336
336
  nb api resource create --resource users --values '{"nickname":"Ada"}' -e app1
337
337
  ```
338
338
 
339
+ Create and download a backup:
340
+
341
+ ```bash
342
+ nb api backup create -e app1
343
+ nb api backup status --name backup_20260430_120000_1234.nbdata -e app1
344
+ nb api backup download --name backup_20260430_120000_1234.nbdata --output ./backup.nbdata -e app1
345
+ ```
346
+
347
+ Restore or run a migration package:
348
+
349
+ ```bash
350
+ nb api backup restore-upload --file ./backup.nbdata -e app1
351
+ nb api migration rules create --name default --user-defined-rule schema-only --system-defined-rule overwrite-first -e app1
352
+ nb api migration create --rule-id 1 --title release-20260430 -e app1
353
+ nb api migration execute --file ./migration.nbdata -e app1
354
+ ```
355
+
339
356
  Use `-j, --json-output` to print raw JSON when available:
340
357
 
341
358
  ```bash
@@ -350,9 +367,11 @@ Available API command topics:
350
367
  | `nb api api-keys` | Manage API keys for HTTP API access. |
351
368
  | `nb api app` | Manage application resources. |
352
369
  | `nb api authenticators` | Manage user authentication, including password auth, SMS auth, SSO protocols, and extensible providers. |
370
+ | `nb api backup` | Create, download, remove, and restore backups. |
353
371
  | `nb api data-modeling` | Manage data sources, collections, and database modeling resources. |
354
372
  | `nb api file-manager` | Manage file storage services, file collections, and attachment fields. |
355
373
  | `nb api flow-surfaces` | Compose and mutate page, tab, block, field, and action surfaces. |
374
+ | `nb api migration` | Create, check, execute, and inspect migration packages. |
356
375
  | `nb api pm` | Manage plugins through API commands. |
357
376
  | `nb api resource` | Work with generic collection resources. |
358
377
  | `nb api system-settings` | Adjust system title, logo, language, and other global settings. |
@@ -92,21 +92,26 @@ function builtinDbContainerName(runtime) {
92
92
  return undefined;
93
93
  }
94
94
  const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
95
- const workspaceName = runtime.workspaceName;
96
- return buildDockerDbContainerName(runtime.envName, dbDialect, workspaceName);
95
+ return buildDockerDbContainerName(runtime.envName, dbDialect, runtime.dockerContainerPrefix || runtime.workspaceName);
97
96
  }
98
97
  function managedDockerNetworkName(runtime) {
99
- return runtime.workspaceName?.trim() || undefined;
98
+ return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
100
99
  }
101
- async function confirmDownAll(envName, yes) {
100
+ async function confirmDownAll(envName, yes, options) {
102
101
  if (yes) {
103
102
  return true;
104
103
  }
104
+ const usedCurrentEnv = options?.explicitEnv === false;
105
105
  if (!isInteractiveTerminal()) {
106
+ if (usedCurrentEnv) {
107
+ throw new Error(`\`nb app down --all\` is using the current env "${envName}". Re-run with --env ${envName} --yes to delete everything for that env in non-interactive mode.`);
108
+ }
106
109
  throw new Error(`\`nb app down --all\` needs confirmation. Re-run with --yes to delete everything for "${envName}" in non-interactive mode.`);
107
110
  }
108
111
  const answer = await p.confirm({
109
- message: `Delete everything for "${envName}"? This removes the app, managed containers, storage data, and the saved CLI env config.`,
112
+ message: usedCurrentEnv
113
+ ? `Delete everything for current env "${envName}"? This removes the app, managed containers, storage data, and the saved CLI env config.`
114
+ : `Delete everything for "${envName}"? This removes the app, managed containers, storage data, and the saved CLI env config.`,
110
115
  active: 'yes',
111
116
  inactive: 'no',
112
117
  initialValue: false,
@@ -153,6 +158,7 @@ export default class AppDown extends Command {
153
158
  async run() {
154
159
  const { flags } = await this.parse(AppDown);
155
160
  const requestedEnv = flags.env?.trim() || undefined;
161
+ const explicitEnv = Boolean(requestedEnv);
156
162
  const removeData = Boolean(flags.all);
157
163
  const removeEnvConfig = Boolean(flags.all);
158
164
  const runtime = await resolveManagedAppRuntime(requestedEnv);
@@ -176,7 +182,7 @@ export default class AppDown extends Command {
176
182
  if (flags.all) {
177
183
  let confirmed = false;
178
184
  try {
179
- confirmed = await confirmDownAll(runtime.envName, flags.yes);
185
+ confirmed = await confirmDownAll(runtime.envName, flags.yes, { explicitEnv });
180
186
  }
181
187
  catch (error) {
182
188
  this.error(error instanceof Error ? error.message : String(error));
@@ -39,7 +39,7 @@ export default class AppLogs extends Command {
39
39
  follow: Flags.boolean({
40
40
  char: 'f',
41
41
  description: 'Keep streaming new log lines',
42
- default: true,
42
+ default: false,
43
43
  allowNo: true,
44
44
  }),
45
45
  };
@@ -65,7 +65,7 @@ export default class AppLogs extends Command {
65
65
  ].join('\n'));
66
66
  }
67
67
  const tail = String(flags.tail ?? 100);
68
- const follow = flags.follow !== false;
68
+ const follow = flags.follow === true;
69
69
  printInfo(follow
70
70
  ? `Showing logs for "${runtime.envName}" (press Ctrl+C to stop).`
71
71
  : `Showing recent logs for "${runtime.envName}".`);
@@ -10,7 +10,7 @@ import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, } from '../../lib/app-runtime.js';
11
11
  import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBaseUrl, waitForAppReady, } from '../../lib/app-health.js';
12
12
  import { ensureBuiltinDbReady, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
13
- import { failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
13
+ import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
14
14
  function argvHasToken(argv, tokens) {
15
15
  return tokens.some((token) => argv.includes(token));
16
16
  }
@@ -107,6 +107,7 @@ export default class AppStart extends Command {
107
107
  'Use a local or Docker env if you need CLI-managed start and stop right now.',
108
108
  ].join('\n'));
109
109
  }
110
+ announceTargetEnv(runtime.envName);
110
111
  if (runtime.kind === 'docker') {
111
112
  const unsupportedFlags = [
112
113
  flags.quickstart ? '--quickstart' : undefined,
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, stopDockerContainer, } from '../../lib/app-runtime.js';
11
- import { failTask, startTask, succeedTask } from '../../lib/ui.js';
11
+ import { announceTargetEnv, failTask, startTask, succeedTask } from '../../lib/ui.js';
12
12
  function formatStopFailure(envName, message) {
13
13
  if (/does not exist/i.test(message)) {
14
14
  return [
@@ -65,6 +65,7 @@ export default class AppStop extends Command {
65
65
  'Use a local or Docker env if you need CLI-managed stop right now.',
66
66
  ].join('\n'));
67
67
  }
68
+ announceTargetEnv(runtime.envName);
68
69
  if (runtime.kind === 'docker') {
69
70
  startTask(`Stopping NocoBase for "${runtime.envName}"...`);
70
71
  try {
@@ -7,12 +7,13 @@
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 { upsertEnv } from '../../lib/auth-store.js';
10
11
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
11
12
  import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
12
- import { commandOutput, commandSucceeds, run } from '../../lib/run-npm.js';
13
- import { failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../../lib/ui.js';
13
+ import { deriveBuiltinDbConnection } from '../../lib/builtin-db.js';
14
+ import { commandSucceeds, run } from '../../lib/run-npm.js';
15
+ import { announceTargetEnv, failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../../lib/ui.js';
14
16
  const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
15
- const DEFAULT_DOWNLOAD_VERSION = 'alpha';
16
17
  const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
17
18
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
18
19
  const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
@@ -92,27 +93,6 @@ function formatDockerStartFailure(envName, message) {
92
93
  `Details: ${message}`,
93
94
  ].join('\n');
94
95
  }
95
- function parseDockerImageRef(imageRef) {
96
- const cleaned = trimValue(imageRef).replace(/@.+$/, '');
97
- if (!cleaned) {
98
- return {
99
- dockerRegistry: DEFAULT_DOCKER_REGISTRY,
100
- version: DEFAULT_DOWNLOAD_VERSION,
101
- };
102
- }
103
- const lastSlash = cleaned.lastIndexOf('/');
104
- const lastColon = cleaned.lastIndexOf(':');
105
- if (lastColon > lastSlash) {
106
- return {
107
- dockerRegistry: cleaned.slice(0, lastColon),
108
- version: cleaned.slice(lastColon + 1) || DEFAULT_DOWNLOAD_VERSION,
109
- };
110
- }
111
- return {
112
- dockerRegistry: cleaned,
113
- version: 'latest',
114
- };
115
- }
116
96
  function normalizeDockerPlatform(value) {
117
97
  const text = String(value ?? '').trim();
118
98
  if (!text || text === 'auto') {
@@ -215,52 +195,14 @@ async function ensureDockerNetwork(name) {
215
195
  stdio: 'ignore',
216
196
  });
217
197
  }
218
- async function inspectDockerContainerEnv(name) {
219
- const output = await commandOutput('docker', [
220
- 'inspect',
221
- '--format',
222
- '{{range .Config.Env}}{{println .}}{{end}}',
223
- name,
224
- ], {
225
- errorName: 'docker inspect',
226
- });
227
- const env = {};
228
- for (const line of output.split(/\r?\n/)) {
229
- const index = line.indexOf('=');
230
- if (index <= 0) {
231
- continue;
232
- }
233
- env[line.slice(0, index)] = line.slice(index + 1);
234
- }
235
- return env;
236
- }
237
- async function inspectDockerContainerImage(name) {
238
- return await commandOutput('docker', [
239
- 'inspect',
240
- '--format',
241
- '{{.Config.Image}}',
242
- name,
243
- ], {
244
- errorName: 'docker inspect',
245
- });
246
- }
247
- async function inspectDockerStoragePath(name) {
248
- return await commandOutput('docker', [
249
- 'inspect',
250
- '--format',
251
- `{{range .Mounts}}{{if eq .Destination "${DOCKER_APP_STORAGE_DESTINATION}"}}{{println .Source}}{{end}}{{end}}`,
252
- name,
253
- ], {
254
- errorName: 'docker inspect',
255
- });
256
- }
257
198
  export default class AppUpgrade extends Command {
258
199
  static hidden = false;
259
- static description = 'Upgrade the selected NocoBase app. Local npm/git installs refresh the saved source and restart with quickstart; Docker installs refresh the saved image and recreate the app container.';
200
+ static description = 'Upgrade the selected NocoBase app. Local npm/git installs refresh the saved source and restart with quickstart; Docker installs refresh the saved image and recreate the app container. Use --version to upgrade to a specific saved source version or image tag.';
260
201
  static examples = [
261
202
  '<%= config.bin %> <%= command.id %>',
262
203
  '<%= config.bin %> <%= command.id %> --env local',
263
204
  '<%= config.bin %> <%= command.id %> --env local -s',
205
+ '<%= config.bin %> <%= command.id %> --env local --version beta',
264
206
  '<%= config.bin %> <%= command.id %> --env local --verbose',
265
207
  '<%= config.bin %> <%= command.id %> --env local-docker -s',
266
208
  ];
@@ -274,23 +216,49 @@ export default class AppUpgrade extends Command {
274
216
  description: 'Restart with the saved local code or Docker image without downloading updates first',
275
217
  required: false,
276
218
  }),
219
+ version: Flags.string({
220
+ description: 'Override the saved downloadVersion for this upgrade. When the upgrade succeeds, the new version is saved back to the env config.',
221
+ required: false,
222
+ }),
277
223
  verbose: Flags.boolean({
278
224
  description: 'Show raw output from the underlying local or Docker commands',
279
225
  default: false,
280
226
  }),
281
227
  };
282
- static buildLocalDownloadArgv(runtime) {
228
+ static resolveUpgradeVersion(runtime, flags) {
229
+ const requestedVersion = trimValue(flags.version);
230
+ if (requestedVersion && flags['skip-code-update']) {
231
+ throw new Error('`--version` and `--skip-code-update` cannot be used together. Use `--version` to download a specific upgrade target, or `--skip-code-update` to restart the saved code/image as-is.');
232
+ }
233
+ if (runtime.kind === 'local' && runtime.source === 'local') {
234
+ if (requestedVersion) {
235
+ throw new Error([
236
+ `Env "${runtime.envName}" is managed from an existing local app path.`,
237
+ 'This source does not support `nb app upgrade --version` because the CLI does not manage that code checkout.',
238
+ 'Update the local app path yourself, then run `nb app upgrade` to restart it.',
239
+ ].join('\n'));
240
+ }
241
+ return {};
242
+ }
243
+ const savedVersion = readEnvValue(runtime.env, 'downloadVersion');
244
+ const downloadVersion = requestedVersion || savedVersion;
245
+ if (!downloadVersion) {
246
+ throw new Error([
247
+ `Env "${runtime.envName}" does not have a saved \`downloadVersion\`.`,
248
+ 'This env cannot be upgraded until a source version is explicit.',
249
+ 'Re-run `nb init` or `nb env add` for this env, or pass `--version` to `nb app upgrade`.',
250
+ ].join('\n'));
251
+ }
252
+ return {
253
+ downloadVersion,
254
+ persistDownloadVersion: requestedVersion || undefined,
255
+ };
256
+ }
257
+ static buildLocalDownloadArgv(runtime, downloadVersion) {
283
258
  const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
284
- const version = readEnvValue(runtime.env, 'downloadVersion');
285
- const outputDir = readEnvValue(runtime.env, 'appRootPath');
286
259
  const gitUrl = readEnvValue(runtime.env, 'gitUrl');
287
260
  const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
288
- if (version) {
289
- argv.push('--version', version);
290
- }
291
- if (outputDir) {
292
- argv.push('--output-dir', outputDir);
293
- }
261
+ argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
294
262
  if (gitUrl) {
295
263
  argv.push('--git-url', gitUrl);
296
264
  }
@@ -308,6 +276,24 @@ export default class AppUpgrade extends Command {
308
276
  }
309
277
  return argv;
310
278
  }
279
+ static buildDockerDownloadArgv(runtime, plan) {
280
+ const argv = [
281
+ '-y',
282
+ '--no-intro',
283
+ '--source',
284
+ 'docker',
285
+ '--replace',
286
+ '--docker-registry',
287
+ plan.dockerRegistry,
288
+ '--version',
289
+ plan.downloadVersion,
290
+ ];
291
+ const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
292
+ if (dockerPlatform) {
293
+ argv.push('--docker-platform', dockerPlatform);
294
+ }
295
+ return argv;
296
+ }
311
297
  static buildLocalStartArgv(runtime) {
312
298
  const argv = ['start', '--quickstart'];
313
299
  const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
@@ -319,43 +305,26 @@ export default class AppUpgrade extends Command {
319
305
  argv.push('--daemon');
320
306
  return argv;
321
307
  }
322
- static async buildDockerUpgradePlan(runtime) {
323
- const containerExists = await dockerContainerExists(runtime.containerName);
324
- let inspectedEnv;
325
- const readContainerEnv = async () => {
326
- if (!containerExists) {
327
- return {};
328
- }
329
- if (!inspectedEnv) {
330
- inspectedEnv = await inspectDockerContainerEnv(runtime.containerName);
331
- }
332
- return inspectedEnv;
333
- };
334
- let dockerRegistry = readEnvValue(runtime.env, 'dockerRegistry');
335
- let version = readEnvValue(runtime.env, 'downloadVersion');
336
- if ((!dockerRegistry || !version) && containerExists) {
337
- const imageRef = await inspectDockerContainerImage(runtime.containerName);
338
- const parsed = parseDockerImageRef(imageRef);
339
- dockerRegistry ||= parsed.dockerRegistry;
340
- version ||= parsed.version;
341
- }
342
- const envVars = await readContainerEnv();
308
+ static buildDockerUpgradePlan(runtime, downloadVersion) {
309
+ const dockerRegistry = readEnvValue(runtime.env, 'dockerRegistry') || DEFAULT_DOCKER_REGISTRY;
343
310
  const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
344
311
  ? ''
345
312
  : trimValue(runtime.env.appPort);
346
- let storagePath = readEnvValue(runtime.env, 'storagePath');
347
- if (!storagePath && containerExists) {
348
- storagePath = trimValue(await inspectDockerStoragePath(runtime.containerName));
349
- }
350
- const appKey = readEnvValue(runtime.env, 'appKey') || trimValue(envVars.APP_KEY);
351
- const timeZone = readEnvValue(runtime.env, 'timezone') || trimValue(envVars.TZ);
352
- const dbDialect = readEnvValue(runtime.env, 'dbDialect') || trimValue(envVars.DB_DIALECT);
353
- const dbHost = readEnvValue(runtime.env, 'dbHost') || trimValue(envVars.DB_HOST);
354
- const dbPort = readEnvValue(runtime.env, 'dbPort') || trimValue(envVars.DB_PORT);
355
- const dbDatabase = readEnvValue(runtime.env, 'dbDatabase') || trimValue(envVars.DB_DATABASE);
356
- const dbUser = readEnvValue(runtime.env, 'dbUser') || trimValue(envVars.DB_USER);
357
- const dbPassword = readEnvValue(runtime.env, 'dbPassword') || trimValue(envVars.DB_PASSWORD);
313
+ const storagePath = readEnvValue(runtime.env, 'storagePath');
314
+ const appKey = readEnvValue(runtime.env, 'appKey');
315
+ const timeZone = readEnvValue(runtime.env, 'timezone');
316
+ const builtinDbConnection = runtime.env.config.builtinDb ? deriveBuiltinDbConnection(runtime) : undefined;
317
+ const dbDialect = builtinDbConnection?.dbDialect || readEnvValue(runtime.env, 'dbDialect');
318
+ const dbHost = builtinDbConnection?.dbHost || readEnvValue(runtime.env, 'dbHost');
319
+ const dbPort = builtinDbConnection?.dbPort || readEnvValue(runtime.env, 'dbPort');
320
+ const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
321
+ const dbUser = readEnvValue(runtime.env, 'dbUser');
322
+ const dbPassword = readEnvValue(runtime.env, 'dbPassword');
323
+ const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
358
324
  const missing = [];
325
+ if (!networkName) {
326
+ missing.push('docker.network');
327
+ }
359
328
  if (!storagePath) {
360
329
  missing.push('storagePath');
361
330
  }
@@ -386,9 +355,7 @@ export default class AppUpgrade extends Command {
386
355
  if (missing.length > 0) {
387
356
  throw new Error(`The saved Docker settings for "${runtime.envName}" are incomplete. Missing: ${missing.join(', ')}. Re-run \`nb init\` or \`nb env add\` to refresh this env config.`);
388
357
  }
389
- const resolvedRegistry = dockerRegistry || DEFAULT_DOCKER_REGISTRY;
390
- const resolvedVersion = version || DEFAULT_DOWNLOAD_VERSION;
391
- const imageRef = `${resolvedRegistry}:${resolvedVersion}`;
358
+ const imageRef = `${dockerRegistry}:${downloadVersion}`;
392
359
  const args = [
393
360
  'run',
394
361
  '-d',
@@ -397,7 +364,7 @@ export default class AppUpgrade extends Command {
397
364
  '--restart',
398
365
  'always',
399
366
  '--network',
400
- runtime.workspaceName,
367
+ networkName,
401
368
  ];
402
369
  if (appPort) {
403
370
  args.push('-p', `${appPort}:80`);
@@ -405,7 +372,9 @@ export default class AppUpgrade extends Command {
405
372
  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}`, imageRef);
406
373
  return {
407
374
  containerName: runtime.containerName,
408
- networkName: runtime.workspaceName,
375
+ networkName,
376
+ dockerRegistry,
377
+ downloadVersion,
409
378
  imageRef,
410
379
  appPort: appPort || undefined,
411
380
  storagePath,
@@ -420,7 +389,7 @@ export default class AppUpgrade extends Command {
420
389
  args,
421
390
  };
422
391
  }
423
- static async upgradeLocal(runCommand, runtime, flags, commandStdio) {
392
+ static async upgradeLocal(runCommand, runtime, downloadVersion, flags, commandStdio) {
424
393
  const displayUrl = formatDisplayUrl(resolveApiBaseUrl(runtime), trimValue(runtime.env.appPort));
425
394
  startTask(`Stopping NocoBase for "${runtime.envName}" before upgrade...`);
426
395
  try {
@@ -437,7 +406,7 @@ export default class AppUpgrade extends Command {
437
406
  if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
438
407
  startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
439
408
  try {
440
- await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime));
409
+ await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion));
441
410
  succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
442
411
  }
443
412
  catch (error) {
@@ -468,22 +437,16 @@ export default class AppUpgrade extends Command {
468
437
  envName: runtime.envName,
469
438
  apiBaseUrl: resolveApiBaseUrl(runtime),
470
439
  });
471
- succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
440
+ return displayUrl;
472
441
  }
473
- static async upgradeDocker(runCommand, runtime, flags, commandStdio) {
474
- const plan = await AppUpgrade.buildDockerUpgradePlan(runtime);
442
+ static async upgradeDocker(runCommand, runtime, downloadVersion, flags, commandStdio) {
475
443
  const apiBaseUrl = resolveApiBaseUrl(runtime);
476
- const displayUrl = formatDisplayUrl(apiBaseUrl, plan.appPort);
477
444
  const containerExists = await dockerContainerExists(runtime.containerName);
478
445
  if (!flags['skip-code-update']) {
479
- const argv = ['-y', '--no-intro', '--source', 'docker', '--replace', '--docker-registry', parseDockerImageRef(plan.imageRef).dockerRegistry, '--version', parseDockerImageRef(plan.imageRef).version];
480
- const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
481
- if (dockerPlatform) {
482
- argv.push('--docker-platform', dockerPlatform);
483
- }
446
+ const plan = AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
484
447
  startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
485
448
  try {
486
- await runCommand('source:download', argv);
449
+ await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan));
487
450
  succeedTask(`Docker image is ready for "${runtime.envName}".`);
488
451
  }
489
452
  catch (error) {
@@ -528,6 +491,8 @@ export default class AppUpgrade extends Command {
528
491
  }
529
492
  }
530
493
  else {
494
+ const plan = AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
495
+ const displayUrl = formatDisplayUrl(apiBaseUrl, plan.appPort);
531
496
  startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
532
497
  try {
533
498
  if (containerExists) {
@@ -548,13 +513,32 @@ export default class AppUpgrade extends Command {
548
513
  failTask(`Failed to recreate the Docker app for "${runtime.envName}".`);
549
514
  throw new Error(formatDockerStartFailure(runtime.envName, message));
550
515
  }
516
+ await waitForAppHealthCheck({
517
+ envName: runtime.envName,
518
+ apiBaseUrl,
519
+ containerName: runtime.containerName,
520
+ });
521
+ return displayUrl;
551
522
  }
552
523
  await waitForAppHealthCheck({
553
524
  envName: runtime.envName,
554
525
  apiBaseUrl,
555
526
  containerName: runtime.containerName,
556
527
  });
557
- succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
528
+ return formatDisplayUrl(apiBaseUrl, trimValue(runtime.env.appPort));
529
+ }
530
+ static async persistDownloadVersion(runtime, downloadVersion) {
531
+ const { name: _name, ...envConfig } = runtime.env.config;
532
+ try {
533
+ await upsertEnv(runtime.envName, {
534
+ ...envConfig,
535
+ downloadVersion,
536
+ });
537
+ }
538
+ catch (error) {
539
+ const message = error instanceof Error ? error.message : String(error);
540
+ throw new Error(`NocoBase was upgraded for "${runtime.envName}", but the CLI could not save \`downloadVersion=${downloadVersion}\`. Details: ${message}`);
541
+ }
558
542
  }
559
543
  async run() {
560
544
  const { flags } = await this.parse(AppUpgrade);
@@ -579,14 +563,17 @@ export default class AppUpgrade extends Command {
579
563
  'Use a local or Docker env if you need CLI-managed upgrades right now.',
580
564
  ].join('\n'));
581
565
  }
566
+ announceTargetEnv(runtime.envName);
582
567
  try {
568
+ const resolvedVersion = AppUpgrade.resolveUpgradeVersion(runtime, parsed);
583
569
  const runCommand = this.config.runCommand.bind(this.config);
584
- if (runtime.kind === 'docker') {
585
- await AppUpgrade.upgradeDocker(runCommand, runtime, parsed, commandStdio);
586
- }
587
- else {
588
- await AppUpgrade.upgradeLocal(runCommand, runtime, parsed, commandStdio);
570
+ const displayUrl = runtime.kind === 'docker'
571
+ ? await AppUpgrade.upgradeDocker(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio)
572
+ : await AppUpgrade.upgradeLocal(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio);
573
+ if (resolvedVersion.persistDownloadVersion) {
574
+ await AppUpgrade.persistDownloadVersion(runtime, resolvedVersion.persistDownloadVersion);
589
575
  }
576
+ succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
590
577
  }
591
578
  catch (error) {
592
579
  this.error(error instanceof Error ? error.message : String(error));
@@ -0,0 +1,30 @@
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 } from '@oclif/core';
10
+ import { assertSupportedCliConfigKey, deleteCliConfigValue } from '../../lib/cli-config.js';
11
+ export default class ConfigDelete extends Command {
12
+ static summary = 'Delete an explicitly configured CLI setting';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %> license.pkg-url',
15
+ '<%= config.bin %> <%= command.id %> docker.network',
16
+ '<%= config.bin %> <%= command.id %> docker.container-prefix',
17
+ ];
18
+ static args = {
19
+ key: Args.string({
20
+ description: 'Configuration key',
21
+ required: true,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args } = await this.parse(ConfigDelete);
26
+ const key = assertSupportedCliConfigKey(args.key);
27
+ const removed = await deleteCliConfigValue(key);
28
+ this.log(removed ? `Deleted ${key}` : `${key} was not set`);
29
+ }
30
+ }
@@ -0,0 +1,29 @@
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 } from '@oclif/core';
10
+ import { assertSupportedCliConfigKey, getCliConfigValue } from '../../lib/cli-config.js';
11
+ export default class ConfigGet extends Command {
12
+ static summary = 'Get the effective CLI configuration value for a key';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %> license.pkg-url',
15
+ '<%= config.bin %> <%= command.id %> docker.network',
16
+ '<%= config.bin %> <%= command.id %> docker.container-prefix',
17
+ ];
18
+ static args = {
19
+ key: Args.string({
20
+ description: 'Configuration key',
21
+ required: true,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args } = await this.parse(ConfigGet);
26
+ const key = assertSupportedCliConfigKey(args.key);
27
+ this.log(await getCliConfigValue(key));
28
+ }
29
+ }
@@ -0,0 +1,20 @@
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 { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Config extends Command {
11
+ static summary = 'Manage CLI configuration defaults';
12
+ async run() {
13
+ await this.parse(Config);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'config',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,29 @@
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 { Command } from '@oclif/core';
10
+ import { listExplicitCliConfigValues, SUPPORTED_CLI_CONFIG_KEYS } from '../../lib/cli-config.js';
11
+ import { renderTable } from '../../lib/ui.js';
12
+ export default class ConfigList extends Command {
13
+ static summary = 'List explicitly configured CLI settings';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %>',
16
+ ];
17
+ async run() {
18
+ await this.parse(ConfigList);
19
+ const values = await listExplicitCliConfigValues();
20
+ const rows = SUPPORTED_CLI_CONFIG_KEYS
21
+ .filter((key) => Boolean(values[key]))
22
+ .map((key) => [key, values[key] ?? '']);
23
+ if (!rows.length) {
24
+ this.log('No CLI config values are set.');
25
+ return;
26
+ }
27
+ this.log(renderTable(['Key', 'Value'], rows));
28
+ }
29
+ }