@nocobase/cli 2.1.0-alpha.26 → 2.1.0-alpha.28

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 (52) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +2 -3
  4. package/dist/commands/app/logs.js +2 -2
  5. package/dist/commands/app/upgrade.js +114 -128
  6. package/dist/commands/config/delete.js +30 -0
  7. package/dist/commands/config/get.js +29 -0
  8. package/dist/commands/config/index.js +20 -0
  9. package/dist/commands/config/list.js +29 -0
  10. package/dist/commands/config/set.js +35 -0
  11. package/dist/commands/db/check.js +238 -0
  12. package/dist/commands/db/logs.js +2 -2
  13. package/dist/commands/db/shared.js +6 -5
  14. package/dist/commands/env/info.js +6 -2
  15. package/dist/commands/env/shared.js +1 -1
  16. package/dist/commands/init.js +0 -1
  17. package/dist/commands/install.js +87 -35
  18. package/dist/commands/license/activate.js +357 -0
  19. package/dist/commands/license/env.js +94 -0
  20. package/dist/commands/license/generate-id.js +107 -0
  21. package/dist/commands/license/id.js +52 -0
  22. package/dist/commands/license/index.js +20 -0
  23. package/dist/commands/license/plugins/clean.js +98 -0
  24. package/dist/commands/license/plugins/index.js +20 -0
  25. package/dist/commands/license/plugins/list.js +50 -0
  26. package/dist/commands/license/plugins/shared.js +325 -0
  27. package/dist/commands/license/plugins/sync.js +267 -0
  28. package/dist/commands/license/shared.js +414 -0
  29. package/dist/commands/license/status.js +50 -0
  30. package/dist/lib/api-client.js +74 -3
  31. package/dist/lib/app-managed-resources.js +10 -6
  32. package/dist/lib/app-runtime.js +29 -11
  33. package/dist/lib/auth-store.js +36 -68
  34. package/dist/lib/build-config.js +8 -0
  35. package/dist/lib/builtin-db.js +86 -0
  36. package/dist/lib/cli-config.js +176 -0
  37. package/dist/lib/cli-home.js +6 -21
  38. package/dist/lib/db-connection-check.js +178 -0
  39. package/dist/lib/env-config.js +7 -0
  40. package/dist/lib/generated-command.js +23 -3
  41. package/dist/lib/plugin-storage.js +127 -0
  42. package/dist/lib/prompt-validators.js +4 -4
  43. package/dist/lib/run-npm.js +53 -0
  44. package/dist/lib/runtime-env-vars.js +32 -0
  45. package/dist/lib/runtime-generator.js +89 -10
  46. package/dist/lib/self-manager.js +57 -2
  47. package/dist/lib/skills-manager.js +2 -2
  48. package/dist/lib/startup-update.js +85 -7
  49. package/dist/locale/en-US.json +16 -13
  50. package/dist/locale/zh-CN.json +16 -13
  51. package/nocobase-ctl.config.json +82 -0
  52. package/package.json +16 -4
package/README.md CHANGED
@@ -60,12 +60,16 @@ nb init --ui
60
60
  When creating a new app, it can also install NocoBase AI coding skills
61
61
  (`nocobase/skills`) globally.
62
62
 
63
+ Use `--skip-skills` if the skills are managed separately, or when running in CI
64
+ or offline environments where `nb init` should not install or update them.
65
+
63
66
  ### Non-Interactive Setup
64
67
 
65
68
  When prompts are skipped, an app/env name is required:
66
69
 
67
70
  ```bash
68
71
  nb init --env app1 --yes
72
+ nb init --env app1 --yes --skip-skills
69
73
  ```
70
74
 
71
75
  Install with Docker:
@@ -111,6 +115,7 @@ If `nb init` was interrupted after the env config had already been saved, you ca
111
115
 
112
116
  ```bash
113
117
  nb init --env app1 --resume
118
+ nb init --env app1 --resume --skip-skills
114
119
  ```
115
120
 
116
121
  `--resume` reuses the saved workspace env config for app, source, database, and env connection settings. In interactive mode, it only asks for any missing setup-only values.
@@ -331,6 +336,23 @@ nb api resource get --resource users --filter-by-tk 1 -e app1
331
336
  nb api resource create --resource users --values '{"nickname":"Ada"}' -e app1
332
337
  ```
333
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
+
334
356
  Use `-j, --json-output` to print raw JSON when available:
335
357
 
336
358
  ```bash
@@ -345,9 +367,11 @@ Available API command topics:
345
367
  | `nb api api-keys` | Manage API keys for HTTP API access. |
346
368
  | `nb api app` | Manage application resources. |
347
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. |
348
371
  | `nb api data-modeling` | Manage data sources, collections, and database modeling resources. |
349
372
  | `nb api file-manager` | Manage file storage services, file collections, and attachment fields. |
350
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. |
351
375
  | `nb api pm` | Manage plugins through API commands. |
352
376
  | `nb api resource` | Work with generic collection resources. |
353
377
  | `nb api system-settings` | Adjust system title, logo, language, and other global settings. |
package/README.zh-CN.md CHANGED
@@ -55,12 +55,15 @@ nb init --ui
55
55
 
56
56
  `nb init` 可以连接已有的 NocoBase 应用,也可以安装一个新的 NocoBase 应用。创建新应用时,还可以全局安装 NocoBase AI coding skills (`nocobase/skills`)。
57
57
 
58
+ 如果已经自行管理 skills,或在 CI、离线环境中运行,不希望 `nb init` 安装或更新 skills,可以传入 `--skip-skills`。
59
+
58
60
  ### 非交互式初始化
59
61
 
60
62
  跳过交互提示时,必须提供 app/env name:
61
63
 
62
64
  ```bash
63
65
  nb init --env app1 --yes
66
+ nb init --env app1 --yes --skip-skills
64
67
  ```
65
68
 
66
69
  使用 Docker 安装:
@@ -106,6 +109,7 @@ nb init --env app1 --yes --source git --version fix/cli-v2
106
109
 
107
110
  ```bash
108
111
  nb init --env app1 --resume
112
+ nb init --env app1 --resume --skip-skills
109
113
  ```
110
114
 
111
115
  `--resume` 会复用工作区里已保存的 env config,包括应用、source、数据库和 env 连接相关配置。在交互模式下,只会继续补齐缺失的初始化参数。
@@ -92,11 +92,10 @@ 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
100
  async function confirmDownAll(envName, yes) {
102
101
  if (yes) {
@@ -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}".`);
@@ -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 { deriveBuiltinDbConnection } from '../../lib/builtin-db.js';
14
+ import { commandSucceeds, run } from '../../lib/run-npm.js';
13
15
  import { 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);
@@ -580,13 +564,15 @@ export default class AppUpgrade extends Command {
580
564
  ].join('\n'));
581
565
  }
582
566
  try {
567
+ const resolvedVersion = AppUpgrade.resolveUpgradeVersion(runtime, parsed);
583
568
  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);
569
+ const displayUrl = runtime.kind === 'docker'
570
+ ? await AppUpgrade.upgradeDocker(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio)
571
+ : await AppUpgrade.upgradeLocal(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio);
572
+ if (resolvedVersion.persistDownloadVersion) {
573
+ await AppUpgrade.persistDownloadVersion(runtime, resolvedVersion.persistDownloadVersion);
589
574
  }
575
+ succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
590
576
  }
591
577
  catch (error) {
592
578
  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
+ }
@@ -0,0 +1,35 @@
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, setCliConfigValue } from '../../lib/cli-config.js';
11
+ export default class ConfigSet extends Command {
12
+ static summary = 'Set a CLI configuration value';
13
+ static description = 'Set a supported CLI configuration key. Supported keys: license.pkg-url, docker.network, docker.container-prefix.';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %> license.pkg-url https://pkg.nocobase.com/',
16
+ '<%= config.bin %> <%= command.id %> docker.network nocobase',
17
+ '<%= config.bin %> <%= command.id %> docker.container-prefix nb',
18
+ ];
19
+ static args = {
20
+ key: Args.string({
21
+ description: 'Configuration key',
22
+ required: true,
23
+ }),
24
+ value: Args.string({
25
+ description: 'Configuration value',
26
+ required: true,
27
+ }),
28
+ };
29
+ async run() {
30
+ const { args } = await this.parse(ConfigSet);
31
+ const key = assertSupportedCliConfigKey(args.key);
32
+ const value = await setCliConfigValue(key, args.value);
33
+ this.log(`${key}=${value}`);
34
+ }
35
+ }