@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.37

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 (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/upgrade.js +38 -16
  5. package/dist/commands/backup/create.js +147 -0
  6. package/dist/commands/backup/index.js +20 -0
  7. package/dist/commands/backup/restore.js +105 -0
  8. package/dist/commands/config/delete.js +4 -0
  9. package/dist/commands/config/get.js +4 -0
  10. package/dist/commands/config/set.js +5 -1
  11. package/dist/commands/env/add.js +129 -15
  12. package/dist/commands/env/auth.js +145 -12
  13. package/dist/commands/env/info.js +52 -8
  14. package/dist/commands/env/list.js +2 -2
  15. package/dist/commands/env/shared.js +41 -3
  16. package/dist/commands/init.js +254 -136
  17. package/dist/commands/install.js +447 -272
  18. package/dist/commands/license/activate.js +6 -4
  19. package/dist/commands/source/publish.js +17 -0
  20. package/dist/commands/v1.js +210 -0
  21. package/dist/lib/app-managed-resources.js +20 -1
  22. package/dist/lib/app-runtime.js +13 -4
  23. package/dist/lib/auth-store.js +69 -18
  24. package/dist/lib/backup.js +171 -0
  25. package/dist/lib/bootstrap.js +23 -13
  26. package/dist/lib/cli-config.js +99 -4
  27. package/dist/lib/cli-locale.js +19 -7
  28. package/dist/lib/db-connection-check.js +61 -0
  29. package/dist/lib/env-auth.js +79 -0
  30. package/dist/lib/env-config.js +8 -1
  31. package/dist/lib/prompt-validators.js +23 -5
  32. package/dist/lib/prompt-web-ui.js +143 -19
  33. package/dist/lib/run-npm.js +166 -30
  34. package/dist/lib/skills-manager.js +74 -4
  35. package/dist/lib/source-publish.js +20 -1
  36. package/dist/lib/source-registry.js +2 -2
  37. package/dist/locale/en-US.json +36 -5
  38. package/dist/locale/zh-CN.json +36 -5
  39. package/package.json +6 -3
package/README.md CHANGED
@@ -61,7 +61,7 @@ When creating a new app, it can also install NocoBase AI coding skills
61
61
  (`nocobase/skills`) globally.
62
62
 
63
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.
64
+ or offline environments where `nb init` should not install them.
65
65
 
66
66
  ### Non-Interactive Setup
67
67
 
package/README.zh-CN.md CHANGED
@@ -55,7 +55,7 @@ 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`。
58
+ 如果已经自行管理 skills,或在 CI、离线环境中运行,不希望 `nb init` 安装 skills,可以传入 `--skip-skills`。
59
59
 
60
60
  ### 非交互式初始化
61
61
 
package/bin/run.js CHANGED
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import path from 'node:path';
7
+ import pc from 'picocolors';
7
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
9
  import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
9
10
 
@@ -130,6 +131,6 @@ try {
130
131
  flush();
131
132
  } catch (error) {
132
133
  const message = formatCliEntryError(error, process.argv.slice(2));
133
- console.error(message);
134
- process.exitCode = 1;
134
+ console.error(pc.red(message));
135
+ process.exit(1);
135
136
  }
@@ -24,6 +24,18 @@ const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
24
24
  function trimValue(value) {
25
25
  return String(value ?? '').trim();
26
26
  }
27
+ function pushOptionalEnvArg(args, key, value) {
28
+ if (typeof value === 'string') {
29
+ if (!value) {
30
+ return;
31
+ }
32
+ args.push('-e', `${key}=${value}`);
33
+ return;
34
+ }
35
+ if (typeof value === 'boolean') {
36
+ args.push('-e', `${key}=${String(value)}`);
37
+ }
38
+ }
27
39
  function formatAppUrl(port) {
28
40
  const value = trimValue(port);
29
41
  return value ? `http://127.0.0.1:${value}` : undefined;
@@ -262,10 +274,13 @@ export default class AppUpgrade extends Command {
262
274
  persistDownloadVersion: requestedVersion || undefined,
263
275
  };
264
276
  }
265
- static buildLocalDownloadArgv(runtime, downloadVersion) {
277
+ static buildLocalDownloadArgv(runtime, downloadVersion, options) {
266
278
  const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
267
279
  const gitUrl = readEnvValue(runtime.env, 'gitUrl');
268
280
  const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
281
+ if (options?.verbose) {
282
+ argv.push('--verbose');
283
+ }
269
284
  argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
270
285
  if (gitUrl) {
271
286
  argv.push('--git-url', gitUrl);
@@ -284,18 +299,12 @@ export default class AppUpgrade extends Command {
284
299
  }
285
300
  return argv;
286
301
  }
287
- static buildDockerDownloadArgv(runtime, plan) {
288
- const argv = [
289
- '-y',
290
- '--no-intro',
291
- '--source',
292
- 'docker',
293
- '--replace',
294
- '--docker-registry',
295
- plan.dockerRegistry,
296
- '--version',
297
- plan.downloadVersion,
298
- ];
302
+ static buildDockerDownloadArgv(runtime, plan, options) {
303
+ const argv = ['-y', '--no-intro'];
304
+ if (options?.verbose) {
305
+ argv.push('--verbose');
306
+ }
307
+ argv.push('--source', 'docker', '--replace', '--docker-registry', plan.dockerRegistry, '--version', plan.downloadVersion);
299
308
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
300
309
  if (dockerPlatform) {
301
310
  argv.push('--docker-platform', dockerPlatform);
@@ -329,6 +338,11 @@ export default class AppUpgrade extends Command {
329
338
  const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
330
339
  const dbUser = readEnvValue(runtime.env, 'dbUser');
331
340
  const dbPassword = readEnvValue(runtime.env, 'dbPassword');
341
+ const dbSchema = readEnvValue(runtime.env, 'dbSchema');
342
+ const dbTablePrefix = readEnvValue(runtime.env, 'dbTablePrefix');
343
+ const dbUnderscored = typeof runtime.env.config.dbUnderscored === 'boolean'
344
+ ? runtime.env.config.dbUnderscored
345
+ : undefined;
332
346
  const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
333
347
  const missing = [];
334
348
  if (!networkName) {
@@ -384,7 +398,11 @@ export default class AppUpgrade extends Command {
384
398
  if (envFile) {
385
399
  args.push('--env-file', envFile);
386
400
  }
387
- 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);
401
+ 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}`);
402
+ pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
403
+ pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
404
+ pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
405
+ args.push(imageRef);
388
406
  return {
389
407
  containerName: runtime.containerName,
390
408
  networkName,
@@ -422,7 +440,9 @@ export default class AppUpgrade extends Command {
422
440
  if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
423
441
  startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
424
442
  try {
425
- await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion));
443
+ await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
444
+ verbose: flags.verbose,
445
+ }));
426
446
  succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
427
447
  }
428
448
  catch (error) {
@@ -468,7 +488,9 @@ export default class AppUpgrade extends Command {
468
488
  const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
469
489
  startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
470
490
  try {
471
- await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan));
491
+ await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan, {
492
+ verbose: flags.verbose,
493
+ }));
472
494
  succeedTask(`Docker image is ready for "${runtime.envName}".`);
473
495
  }
474
496
  catch (error) {
@@ -0,0 +1,147 @@
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, Flags } from '@oclif/core';
10
+ import { BACKUP_CREATE_TIMEOUT_MS, BACKUP_POLL_INTERVAL_MS, BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupCreateOutputPath, resolveBackupTargetEnv, runBackupCliJsonCommand, sleep, } from '../../lib/backup.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
12
+ import { announceTargetEnv, failTask, startTask, succeedTask, updateTask } from '../../lib/ui.js';
13
+ function formatBackupCreateTimeoutError(envName, name) {
14
+ return [
15
+ `Backup "${name}" did not finish in time for "${envName}".`,
16
+ `Waited ${Math.floor(BACKUP_CREATE_TIMEOUT_MS / 1000)}s but it still reports \`inProgress: true\`.`,
17
+ ].join(' ');
18
+ }
19
+ function readBackupCreateResult(response) {
20
+ const name = String(response.data?.name ?? '').trim();
21
+ if (!name) {
22
+ throw new Error('Backup creation did not return a backup name.');
23
+ }
24
+ return {
25
+ name,
26
+ inProgress: Boolean(response.data?.inProgress),
27
+ };
28
+ }
29
+ function readBackupInProgress(response, name) {
30
+ const status = response.data?.[name];
31
+ if (!status || typeof status !== 'object') {
32
+ throw new Error(`Backup status did not include "${name}".`);
33
+ }
34
+ return Boolean(status.inProgress);
35
+ }
36
+ function readDownloadOutput(response) {
37
+ const output = String(response.data?.output ?? '').trim();
38
+ return output || undefined;
39
+ }
40
+ export default class BackupCreate extends Command {
41
+ static summary = 'Create a backup through the selected env and download it locally';
42
+ static examples = [
43
+ '<%= config.bin %> <%= command.id %>',
44
+ '<%= config.bin %> <%= command.id %> --output ./fixtures/base.nbdump',
45
+ '<%= config.bin %> <%= command.id %> --env e2e --output ./fixtures',
46
+ ];
47
+ static flags = {
48
+ env: Flags.string({
49
+ char: 'e',
50
+ description: 'CLI env name to back up. Defaults to the current env when omitted',
51
+ }),
52
+ yes: Flags.boolean({
53
+ char: 'y',
54
+ description: 'Confirm using --env when it targets a different env than the current env',
55
+ default: false,
56
+ }),
57
+ output: Flags.string({
58
+ char: 'o',
59
+ description: 'Download path. When omitted, save to the current directory using the remote backup filename',
60
+ }),
61
+ 'json-output': Flags.boolean({
62
+ char: 'j',
63
+ description: 'Print the final backup result as JSON',
64
+ default: false,
65
+ }),
66
+ };
67
+ async run() {
68
+ const { flags } = await this.parse(BackupCreate);
69
+ const requestedEnv = flags.env?.trim() || undefined;
70
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
71
+ const jsonOutput = Boolean(flags['json-output']);
72
+ if (explicitEnvSelection) {
73
+ const confirmed = await ensureCrossEnvConfirmed({
74
+ command: this,
75
+ requestedEnv,
76
+ yes: flags.yes,
77
+ });
78
+ if (!confirmed) {
79
+ return;
80
+ }
81
+ }
82
+ const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
83
+ const envArgv = buildBackupEnvArgv({
84
+ requestedEnv,
85
+ explicitEnvSelection,
86
+ yes: flags.yes,
87
+ });
88
+ if (!jsonOutput) {
89
+ announceTargetEnv(envName);
90
+ }
91
+ await ensureBackupRuntimeCommands({
92
+ envName,
93
+ env,
94
+ commandIds: [
95
+ BACKUP_RUNTIME_COMMANDS.create,
96
+ BACKUP_RUNTIME_COMMANDS.status,
97
+ BACKUP_RUNTIME_COMMANDS.download,
98
+ ],
99
+ quiet: jsonOutput,
100
+ });
101
+ try {
102
+ if (!jsonOutput) {
103
+ startTask(`Creating backup for "${envName}"...`);
104
+ }
105
+ const createResponse = await runBackupCliJsonCommand(['api', 'backup', 'create', ...envArgv], { errorName: 'nb api backup create' });
106
+ const { name, inProgress } = readBackupCreateResult(createResponse);
107
+ const outputPath = await resolveBackupCreateOutputPath(flags.output, name);
108
+ const startedAt = Date.now();
109
+ let pending = inProgress;
110
+ while (pending) {
111
+ const now = Date.now();
112
+ const elapsedMs = now - startedAt;
113
+ if (elapsedMs >= BACKUP_CREATE_TIMEOUT_MS) {
114
+ throw new Error(formatBackupCreateTimeoutError(envName, name));
115
+ }
116
+ const elapsedSeconds = Math.max(1, Math.floor(elapsedMs / 1000));
117
+ if (!jsonOutput) {
118
+ updateTask(`Waiting for backup "${name}" to finish for "${envName}"... (${elapsedSeconds}s elapsed)`);
119
+ }
120
+ await sleep(BACKUP_POLL_INTERVAL_MS);
121
+ const statusResponse = await runBackupCliJsonCommand(['api', 'backup', 'status', '--name', name, ...envArgv], { errorName: 'nb api backup status' });
122
+ pending = readBackupInProgress(statusResponse, name);
123
+ }
124
+ if (!jsonOutput) {
125
+ updateTask(`Downloading backup "${name}" for "${envName}"...`);
126
+ }
127
+ const downloadResponse = await runBackupCliJsonCommand(['api', 'backup', 'download', '--name', name, '--output', outputPath, ...envArgv], { errorName: 'nb api backup download' });
128
+ const savedPath = readDownloadOutput(downloadResponse) ?? outputPath;
129
+ const result = {
130
+ env: envName,
131
+ name,
132
+ output: savedPath,
133
+ };
134
+ if (jsonOutput) {
135
+ this.log(JSON.stringify(result, null, 2));
136
+ return;
137
+ }
138
+ succeedTask(`Backup saved to ${savedPath}`);
139
+ }
140
+ catch (error) {
141
+ if (!jsonOutput) {
142
+ failTask(`Failed to create backup for "${envName}".`);
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ }
@@ -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 Backup extends Command {
11
+ static summary = 'Create or restore NocoBase backups';
12
+ async run() {
13
+ await this.parse(Backup);
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 ?? 'backup',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,105 @@
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, Flags } from '@oclif/core';
10
+ import { BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupRestoreFilePath, resolveBackupTargetEnv, resolveBackupWaitApiBaseUrl, runBackupCliCommand, } from '../../lib/backup.js';
11
+ import { waitForAppReady } from '../../lib/app-health.js';
12
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
13
+ import { confirm } from "../../lib/inquirer.js";
14
+ import { announceTargetEnv, failTask, isInteractiveTerminal, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
+ async function confirmBackupRestore(envName, filePath, force) {
16
+ if (force) {
17
+ return true;
18
+ }
19
+ if (!isInteractiveTerminal()) {
20
+ throw new Error(`\`nb backup restore\` needs confirmation. Re-run with \`--force\` to restore ${filePath} into "${envName}" in non-interactive mode.`);
21
+ }
22
+ try {
23
+ return await confirm({
24
+ message: `Restore backup "${filePath}" into "${envName}"? This will overwrite application data.`,
25
+ default: false,
26
+ });
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export default class BackupRestore extends Command {
33
+ static summary = 'Restore a backup file into the selected env';
34
+ static examples = [
35
+ '<%= config.bin %> <%= command.id %> --file ./fixtures/base.nbdump --force',
36
+ '<%= config.bin %> <%= command.id %> --env e2e --file ./fixtures/base.nbdump --yes --force',
37
+ ];
38
+ static flags = {
39
+ env: Flags.string({
40
+ char: 'e',
41
+ description: 'CLI env name to restore into. Defaults to the current env when omitted',
42
+ }),
43
+ yes: Flags.boolean({
44
+ char: 'y',
45
+ description: 'Confirm using --env when it targets a different env than the current env',
46
+ default: false,
47
+ }),
48
+ file: Flags.string({
49
+ char: 'f',
50
+ description: 'Local backup file to upload and restore',
51
+ required: true,
52
+ }),
53
+ force: Flags.boolean({
54
+ description: 'Confirm overwriting application data during restore',
55
+ default: false,
56
+ }),
57
+ };
58
+ async run() {
59
+ const { flags } = await this.parse(BackupRestore);
60
+ const requestedEnv = flags.env?.trim() || undefined;
61
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
62
+ if (explicitEnvSelection) {
63
+ const confirmed = await ensureCrossEnvConfirmed({
64
+ command: this,
65
+ requestedEnv,
66
+ yes: flags.yes,
67
+ });
68
+ if (!confirmed) {
69
+ return;
70
+ }
71
+ }
72
+ const filePath = await resolveBackupRestoreFilePath(flags.file);
73
+ const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
74
+ const restoreConfirmed = await confirmBackupRestore(envName, filePath, flags.force);
75
+ if (!restoreConfirmed) {
76
+ return;
77
+ }
78
+ const envArgv = buildBackupEnvArgv({
79
+ requestedEnv,
80
+ explicitEnvSelection,
81
+ yes: flags.yes,
82
+ });
83
+ announceTargetEnv(envName);
84
+ await ensureBackupRuntimeCommands({
85
+ envName,
86
+ env,
87
+ commandIds: [BACKUP_RUNTIME_COMMANDS.restoreUpload],
88
+ });
89
+ startTask(`Restoring backup for "${envName}" from ${filePath}...`);
90
+ try {
91
+ await runBackupCliCommand(['api', 'backup', 'restore-upload', '--file', filePath, '--force', ...envArgv], { errorName: 'nb api backup restore-upload' });
92
+ stopTask();
93
+ await waitForAppReady({
94
+ envName,
95
+ apiBaseUrl: resolveBackupWaitApiBaseUrl(env),
96
+ });
97
+ succeedTask(`Backup restored for "${envName}" from ${filePath}`);
98
+ }
99
+ catch (error) {
100
+ stopTask();
101
+ failTask(`Failed to restore backup for "${envName}".`);
102
+ throw error;
103
+ }
104
+ }
105
+ }
@@ -11,9 +11,13 @@ import { assertSupportedCliConfigKey, deleteCliConfigValue } from '../../lib/cli
11
11
  export default class ConfigDelete extends Command {
12
12
  static summary = 'Delete an explicitly configured CLI setting';
13
13
  static examples = [
14
+ '<%= config.bin %> <%= command.id %> locale',
14
15
  '<%= config.bin %> <%= command.id %> license.pkg-url',
15
16
  '<%= config.bin %> <%= command.id %> docker.network',
16
17
  '<%= config.bin %> <%= command.id %> docker.container-prefix',
18
+ '<%= config.bin %> <%= command.id %> bin.docker',
19
+ '<%= config.bin %> <%= command.id %> bin.git',
20
+ '<%= config.bin %> <%= command.id %> bin.yarn',
17
21
  ];
18
22
  static args = {
19
23
  key: Args.string({
@@ -11,9 +11,13 @@ import { assertSupportedCliConfigKey, getCliConfigValue } from '../../lib/cli-co
11
11
  export default class ConfigGet extends Command {
12
12
  static summary = 'Get the effective CLI configuration value for a key';
13
13
  static examples = [
14
+ '<%= config.bin %> <%= command.id %> locale',
14
15
  '<%= config.bin %> <%= command.id %> license.pkg-url',
15
16
  '<%= config.bin %> <%= command.id %> docker.network',
16
17
  '<%= config.bin %> <%= command.id %> docker.container-prefix',
18
+ '<%= config.bin %> <%= command.id %> bin.docker',
19
+ '<%= config.bin %> <%= command.id %> bin.git',
20
+ '<%= config.bin %> <%= command.id %> bin.yarn',
17
21
  ];
18
22
  static args = {
19
23
  key: Args.string({
@@ -10,11 +10,15 @@ import { Args, Command } from '@oclif/core';
10
10
  import { assertSupportedCliConfigKey, setCliConfigValue } from '../../lib/cli-config.js';
11
11
  export default class ConfigSet extends Command {
12
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.';
13
+ static description = 'Set a supported CLI configuration key. Supported keys: locale, license.pkg-url, docker.network, docker.container-prefix, bin.docker, bin.git, bin.yarn.';
14
14
  static examples = [
15
+ '<%= config.bin %> <%= command.id %> locale zh-CN',
15
16
  '<%= config.bin %> <%= command.id %> license.pkg-url https://pkg.nocobase.com/',
16
17
  '<%= config.bin %> <%= command.id %> docker.network nocobase',
17
18
  '<%= config.bin %> <%= command.id %> docker.container-prefix nb',
19
+ '<%= config.bin %> <%= command.id %> bin.docker /usr/local/bin/docker',
20
+ '<%= config.bin %> <%= command.id %> bin.git /usr/bin/git',
21
+ '<%= config.bin %> <%= command.id %> bin.yarn yarn',
18
22
  ];
19
23
  static args = {
20
24
  key: Args.string({