@nocobase/cli 2.1.0-beta.43 → 2.1.0-beta.44.test.1

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 (101) 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/dist/commands/app/autostart/disable.js +55 -0
  14. package/dist/commands/app/autostart/enable.js +55 -0
  15. package/dist/commands/app/autostart/list.js +37 -0
  16. package/dist/commands/app/autostart/run.js +84 -0
  17. package/dist/commands/app/autostart/shared.js +49 -0
  18. package/dist/commands/app/destroy.js +8 -6
  19. package/dist/commands/app/down.js +2 -2
  20. package/dist/commands/app/logs.js +2 -1
  21. package/dist/commands/app/restart.js +79 -23
  22. package/dist/commands/app/shared.js +1 -1
  23. package/dist/commands/app/start.js +134 -38
  24. package/dist/commands/app/stop.js +31 -2
  25. package/dist/commands/app/upgrade.js +3 -1
  26. package/dist/commands/config/delete.js +4 -1
  27. package/dist/commands/config/get.js +4 -1
  28. package/dist/commands/config/set.js +5 -2
  29. package/dist/commands/env/add.js +19 -39
  30. package/dist/commands/env/info.js +3 -2
  31. package/dist/commands/env/proxy/caddy.js +28 -0
  32. package/dist/commands/env/proxy/index.js +353 -0
  33. package/dist/commands/env/proxy/nginx.js +28 -0
  34. package/dist/commands/env/remove.js +112 -22
  35. package/dist/commands/env/shared.js +17 -9
  36. package/dist/commands/env/update.js +385 -21
  37. package/dist/commands/init.js +233 -91
  38. package/dist/commands/install.js +174 -68
  39. package/dist/commands/license/activate.js +63 -244
  40. package/dist/commands/license/plugins/shared.js +64 -13
  41. package/dist/commands/plugin/import.js +108 -0
  42. package/dist/commands/revision/create.js +89 -0
  43. package/dist/locale/en-US.json +105 -19
  44. package/dist/locale/zh-CN.json +102 -16
  45. package/package.json +5 -8
  46. package/scripts/build.mjs +34 -0
  47. package/scripts/clean.mjs +9 -0
  48. package/tsconfig.json +19 -0
  49. package/LICENSE.txt +0 -107
  50. package/README.zh-CN.md +0 -355
  51. package/dist/lib/api-client.js +0 -335
  52. package/dist/lib/api-command-compat.js +0 -641
  53. package/dist/lib/app-health.js +0 -139
  54. package/dist/lib/app-managed-resources.js +0 -316
  55. package/dist/lib/app-runtime.js +0 -180
  56. package/dist/lib/auth-store.js +0 -405
  57. package/dist/lib/backup.js +0 -171
  58. package/dist/lib/bootstrap.js +0 -409
  59. package/dist/lib/build-config.js +0 -18
  60. package/dist/lib/builtin-db.js +0 -86
  61. package/dist/lib/cli-config.js +0 -309
  62. package/dist/lib/cli-entry-error.js +0 -44
  63. package/dist/lib/cli-home.js +0 -47
  64. package/dist/lib/cli-locale.js +0 -141
  65. package/dist/lib/command-discovery.js +0 -39
  66. package/dist/lib/db-connection-check.js +0 -219
  67. package/dist/lib/docker-env-file.js +0 -52
  68. package/dist/lib/docker-image.js +0 -37
  69. package/dist/lib/docker-log-stream.js +0 -45
  70. package/dist/lib/env-auth.js +0 -960
  71. package/dist/lib/env-config.js +0 -95
  72. package/dist/lib/env-guard.js +0 -62
  73. package/dist/lib/generated-command.js +0 -203
  74. package/dist/lib/http-request.js +0 -49
  75. package/dist/lib/inquirer-theme.js +0 -17
  76. package/dist/lib/inquirer.js +0 -244
  77. package/dist/lib/naming.js +0 -70
  78. package/dist/lib/object-utils.js +0 -76
  79. package/dist/lib/openapi.js +0 -62
  80. package/dist/lib/plugin-storage.js +0 -64
  81. package/dist/lib/post-processors.js +0 -23
  82. package/dist/lib/prompt-catalog-core.js +0 -185
  83. package/dist/lib/prompt-catalog-terminal.js +0 -375
  84. package/dist/lib/prompt-catalog.js +0 -10
  85. package/dist/lib/prompt-validators.js +0 -258
  86. package/dist/lib/prompt-web-ui.js +0 -2227
  87. package/dist/lib/resource-command.js +0 -357
  88. package/dist/lib/resource-request.js +0 -104
  89. package/dist/lib/run-npm.js +0 -385
  90. package/dist/lib/runtime-env-vars.js +0 -32
  91. package/dist/lib/runtime-generator.js +0 -498
  92. package/dist/lib/runtime-store.js +0 -56
  93. package/dist/lib/self-manager.js +0 -301
  94. package/dist/lib/session-id.js +0 -17
  95. package/dist/lib/session-integration.js +0 -703
  96. package/dist/lib/session-store.js +0 -118
  97. package/dist/lib/skills-manager.js +0 -436
  98. package/dist/lib/source-publish.js +0 -309
  99. package/dist/lib/source-registry.js +0 -188
  100. package/dist/lib/startup-update.js +0 -309
  101. package/dist/lib/ui.js +0 -158
@@ -7,26 +7,104 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
+ import { resolveManagedAppRuntime } from '../../lib/app-runtime.js';
10
11
  import { getCurrentEnvName, loadAuthConfig, removeEnv } from '../../lib/auth-store.js';
11
12
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
- import { confirm } from "../../lib/inquirer.js";
13
- import { isInteractiveTerminal, printVerbose, setVerboseMode } from '../../lib/ui.js';
13
+ import { confirm, input } from "../../lib/inquirer.js";
14
+ import { isInteractiveTerminal, printInfo, printVerbose, setVerboseMode } from '../../lib/ui.js';
15
+ function formatRemoveForceRequiredMessage(envName, purge) {
16
+ if (purge) {
17
+ return [
18
+ `Refusing to purge env "${envName}" without confirmation in non-interactive mode.`,
19
+ 'Re-run with `--purge --force` to continue.',
20
+ ].join('\n');
21
+ }
22
+ return [
23
+ `Refusing to remove env "${envName}" without confirmation in non-interactive mode.`,
24
+ 'Re-run with `--force` to continue.',
25
+ ].join('\n');
26
+ }
27
+ function buildRemovePrompt(runtime, options) {
28
+ const subject = options.isCurrent ? `current env "${runtime.envName}"` : `env "${runtime.envName}"`;
29
+ if (options.purge) {
30
+ const lines = [`Purge ${subject}?`];
31
+ if (runtime.kind === 'local' || runtime.kind === 'docker') {
32
+ lines.push('This removes CLI-managed local runtime resources for this env on this machine.');
33
+ lines.push('Storage data will be removed, and downloaded local app files will be removed when applicable.');
34
+ lines.push('External database services are not managed by the CLI and will be left untouched.');
35
+ }
36
+ else {
37
+ lines.push('This env has no CLI-managed local runtime resources on this machine.');
38
+ lines.push('Only the saved CLI env config will be removed. External services are not touched.');
39
+ }
40
+ lines.push(`Type "${runtime.envName}" to confirm:`);
41
+ return lines.join('\n');
42
+ }
43
+ const lines = [`Remove ${subject}?`];
44
+ if (runtime.kind === 'local' || runtime.kind === 'docker') {
45
+ lines.push('NocoBase and any CLI-managed built-in database for this env will be stopped on this machine.');
46
+ lines.push('The saved CLI env config will then be removed. Storage data and local app files will be kept.');
47
+ }
48
+ else {
49
+ lines.push('Only the saved CLI env config will be removed.');
50
+ }
51
+ return lines.join('\n');
52
+ }
53
+ async function confirmEnvRemoval(runtime, options) {
54
+ if (!isInteractiveTerminal()) {
55
+ if (options.force) {
56
+ return true;
57
+ }
58
+ throw new Error(formatRemoveForceRequiredMessage(runtime.envName, options.purge));
59
+ }
60
+ if (options.force) {
61
+ return true;
62
+ }
63
+ if (options.purge) {
64
+ try {
65
+ await input({
66
+ message: buildRemovePrompt(runtime, options),
67
+ required: true,
68
+ validate: (value) => (value.trim() === runtime.envName ? true : `Type "${runtime.envName}" to confirm.`),
69
+ placeholder: runtime.envName,
70
+ });
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ try {
78
+ return await confirm({
79
+ message: buildRemovePrompt(runtime, options),
80
+ default: false,
81
+ });
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
14
87
  export default class EnvRemove extends Command {
15
88
  static summary = 'Remove a configured environment';
16
- static description = 'Remove the saved CLI env config for an environment. This command does not clean local app files, containers, or storage data.';
89
+ static description = 'Remove a configured env. Local and Docker envs stop CLI-managed runtime resources on this machine first; pass `--purge` to also delete managed local resources, storage data, and downloaded app files when applicable.';
17
90
  static examples = [
18
91
  '<%= config.bin %> <%= command.id %> staging',
19
- '<%= config.bin %> <%= command.id %> staging --yes',
92
+ '<%= config.bin %> <%= command.id %> staging --force',
93
+ '<%= config.bin %> <%= command.id %> staging --purge --force',
20
94
  ];
21
95
  static flags = {
22
96
  yes: Flags.boolean({
23
97
  char: 'y',
24
- description: 'Skip confirmation and remove the saved CLI env config',
98
+ hidden: true,
25
99
  default: false,
26
100
  }),
27
101
  force: Flags.boolean({
28
102
  char: 'f',
29
- hidden: true,
103
+ description: 'Skip confirmation for the selected remove mode',
104
+ default: false,
105
+ }),
106
+ purge: Flags.boolean({
107
+ description: 'Also remove CLI-managed local runtime resources, storage data, and downloaded app files when applicable. For remote API envs, only the saved CLI env config will be removed.',
30
108
  default: false,
31
109
  }),
32
110
  verbose: Flags.boolean({
@@ -49,25 +127,37 @@ export default class EnvRemove extends Command {
49
127
  this.error(`Env "${args.name}" is not configured`);
50
128
  }
51
129
  const currentEnv = await getCurrentEnvName({ scope });
130
+ const runtime = await resolveManagedAppRuntime(args.name);
131
+ if (!runtime) {
132
+ this.error(`Env "${args.name}" is not configured`);
133
+ }
52
134
  const skipConfirmation = flags.yes || flags.force;
53
- if (!skipConfirmation) {
54
- if (!isInteractiveTerminal()) {
55
- this.error(`Refusing to remove env "${args.name}" without confirmation in non-interactive mode. Re-run with \`--yes\` to remove only the saved CLI env config.`);
56
- }
57
- const subject = args.name === currentEnv ? `current env "${args.name}"` : `env "${args.name}"`;
58
- let confirmed = false;
59
- try {
60
- confirmed = await confirm({
61
- message: `Remove ${subject}? Only the saved CLI env config will be removed.`,
62
- default: false,
63
- });
64
- }
65
- catch {
66
- return;
67
- }
68
- if (!confirmed) {
135
+ let confirmed = false;
136
+ try {
137
+ confirmed = await confirmEnvRemoval(runtime, {
138
+ force: skipConfirmation,
139
+ isCurrent: args.name === currentEnv,
140
+ purge: flags.purge,
141
+ });
142
+ }
143
+ catch (error) {
144
+ this.error(error instanceof Error ? error.message : String(error));
145
+ }
146
+ if (!confirmed) {
147
+ return;
148
+ }
149
+ const runCommand = this.config.runCommand.bind(this.config);
150
+ const verboseArgv = flags.verbose ? ['--verbose'] : [];
151
+ if (flags.purge) {
152
+ if (runtime.kind === 'local' || runtime.kind === 'docker') {
153
+ await runCommand('app:destroy', ['--env', runtime.envName, '--force', ...verboseArgv]);
69
154
  return;
70
155
  }
156
+ printInfo(`No local CLI-managed resources were found for "${runtime.envName}". Removing the saved CLI env config only.`);
157
+ }
158
+ else if (runtime.kind === 'local' || runtime.kind === 'docker') {
159
+ printVerbose(`Stopping CLI-managed runtime resources for "${runtime.envName}" before removing the env config.`);
160
+ await runCommand('app:stop', ['--env', runtime.envName, '--with-db', '--yes', ...verboseArgv]);
71
161
  }
72
162
  printVerbose(`Removing env "${args.name}"`);
73
163
  const result = await removeEnv(args.name, { scope });
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, } from '../../lib/app-runtime.js';
10
10
  import { executeRawApiRequest } from '../../lib/api-client.js';
11
+ import { buildLocalAppUrl } from '../../lib/app-public-path.js';
11
12
  export function resolveApiBaseUrl(config) {
12
13
  return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
13
14
  }
@@ -53,16 +54,23 @@ export function appUrl(runtime) {
53
54
  }
54
55
  const port = String(runtime.env.config.appPort ?? '').trim();
55
56
  if (port) {
56
- return `http://127.0.0.1:${port}/`;
57
+ return buildLocalAppUrl(port, runtime.env.config?.appPublicPath) ?? '';
57
58
  }
58
59
  return '';
59
60
  }
60
- export function appRootPath(runtime) {
61
+ export function appPath(runtime) {
62
+ if (runtime.kind === 'http') {
63
+ return '-';
64
+ }
65
+ const value = String(runtime.env.appPath ?? runtime.env.config.appPath ?? '').trim();
66
+ return value || '-';
67
+ }
68
+ export function sourcePath(runtime) {
61
69
  if (runtime.kind === 'http' || runtime.kind === 'docker') {
62
70
  return '-';
63
71
  }
64
72
  if (runtime.kind === 'local') {
65
- return String(runtime.projectRoot ?? runtime.env.appRootPath ?? '').trim() || '-';
73
+ return String(runtime.projectRoot ?? runtime.env.sourcePath ?? runtime.env.appRootPath ?? '').trim() || '-';
66
74
  }
67
75
  return String(runtime.env.config.appRootPath ?? '').trim() || '-';
68
76
  }
@@ -98,15 +106,15 @@ function isNetworkFailure(error) {
98
106
  if (!(error instanceof Error)) {
99
107
  return false;
100
108
  }
101
- return (error.name === 'AbortError'
102
- || /fetch failed|network|timeout|timed out|abort|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i.test(error.message));
109
+ return (error.name === 'AbortError' ||
110
+ /fetch failed|network|timeout|timed out|abort|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i.test(error.message));
103
111
  }
104
112
  function isUnconfiguredFailure(error) {
105
113
  return error instanceof Error && /missing (a )?base url|missing base URL/i.test(error.message);
106
114
  }
107
115
  function isAuthFailure(error) {
108
- return (error instanceof Error
109
- && /EMPTY_TOKEN|INVALID_TOKEN|EXPIRED_TOKEN|BLOCKED_TOKEN|EXPIRED_SESSION|NOT_EXIST_USER|invalid_grant|sign in|signin|authentication failed/i.test(error.message));
116
+ return (error instanceof Error &&
117
+ /EMPTY_TOKEN|INVALID_TOKEN|EXPIRED_TOKEN|BLOCKED_TOKEN|EXPIRED_SESSION|NOT_EXIST_USER|invalid_grant|sign in|signin|authentication failed/i.test(error.message));
110
118
  }
111
119
  export async function apiStatus(envName, config, options = {}) {
112
120
  if (!resolveApiBaseUrl(config)) {
@@ -166,7 +174,7 @@ async function dockerStatus(containerName) {
166
174
  if (!(await dockerContainerExists(containerName))) {
167
175
  return 'missing';
168
176
  }
169
- return await dockerContainerIsRunning(containerName) ? 'running' : 'stopped';
177
+ return (await dockerContainerIsRunning(containerName)) ? 'running' : 'stopped';
170
178
  }
171
179
  export async function dbStatus(runtime) {
172
180
  if (!runtime.env.config.builtinDb) {
@@ -192,5 +200,5 @@ export async function runtimeStatus(runtime) {
192
200
  if (runtime.kind === 'docker') {
193
201
  return await dockerStatus(runtime.containerName);
194
202
  }
195
- return await isLocalAppHealthy(runtime) ? 'running' : 'stopped';
203
+ return (await isLocalAppHealthy(runtime)) ? 'running' : 'stopped';
196
204
  }
@@ -9,20 +9,168 @@
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { Args, Command, Flags } from '@oclif/core';
12
- import { getCurrentEnvName } from '../../lib/auth-store.js';
12
+ import { getCurrentEnvName, getEnv, replaceEnvConfig } from '../../lib/auth-store.js';
13
13
  import { updateEnvRuntime } from '../../lib/bootstrap.js';
14
14
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
15
- import { failTask, printVerbose, setVerboseMode, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
+ import { ENV_BOOLEAN_CONFIG_FLAG_MAP, ENV_STRING_CONFIG_FLAG_MAP } from '../../lib/env-command-config.js';
16
+ import { buildStoredEnvConfig } from '../../lib/env-config.js';
17
+ import { failTask, printInfo, printVerbose, printWarningBlock, setVerboseMode, startTask, stopTask, succeedTask } from '../../lib/ui.js';
16
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const UPDATE_STRING_FLAGS = [
20
+ 'source',
21
+ 'download-version',
22
+ 'docker-registry',
23
+ 'docker-platform',
24
+ 'git-url',
25
+ 'npm-registry',
26
+ 'app-path',
27
+ 'app-root-path',
28
+ 'storage-path',
29
+ 'app-public-path',
30
+ 'cdn-base-url',
31
+ 'env-file',
32
+ 'app-port',
33
+ 'app-key',
34
+ 'timezone',
35
+ 'db-dialect',
36
+ 'builtin-db-image',
37
+ 'db-host',
38
+ 'db-port',
39
+ 'db-database',
40
+ 'db-user',
41
+ 'db-password',
42
+ 'db-schema',
43
+ 'db-table-prefix',
44
+ ];
45
+ const UPDATE_BOOLEAN_FLAGS = ['builtin-db', 'dev-dependencies', 'build', 'build-dts', 'db-underscored'];
46
+ const UPDATE_SPECIAL_FIELDS = ['api-base-url', 'auth-type', 'access-token', 'username'];
47
+ const UNSETTABLE_FIELDS = new Set([...UPDATE_SPECIAL_FIELDS, ...UPDATE_STRING_FLAGS, ...UPDATE_BOOLEAN_FLAGS]);
48
+ const SOURCE_SETTING_FIELDS = new Set([
49
+ 'source',
50
+ 'download-version',
51
+ 'docker-registry',
52
+ 'docker-platform',
53
+ 'git-url',
54
+ 'npm-registry',
55
+ 'dev-dependencies',
56
+ 'build',
57
+ 'build-dts',
58
+ ]);
59
+ const APP_RESTART_FIELDS = new Set([
60
+ 'app-path',
61
+ 'app-root-path',
62
+ 'storage-path',
63
+ 'app-public-path',
64
+ 'env-file',
65
+ 'app-port',
66
+ 'app-key',
67
+ 'timezone',
68
+ 'db-host',
69
+ 'db-port',
70
+ 'db-database',
71
+ 'db-user',
72
+ 'db-password',
73
+ 'db-schema',
74
+ 'db-table-prefix',
75
+ 'db-underscored',
76
+ ]);
77
+ const APP_RESTART_WITH_DB_FIELDS = new Set([
78
+ 'builtin-db',
79
+ 'db-dialect',
80
+ 'builtin-db-image',
81
+ 'db-port',
82
+ 'db-database',
83
+ 'db-user',
84
+ 'db-password',
85
+ 'storage-path',
86
+ ]);
87
+ const __dirnameConfigFile = path.join(path.dirname(path.dirname(path.dirname(__dirname))), 'nocobase-ctl.config.json');
88
+ function hasTokenOverride(flags) {
89
+ return flags['access-token'] !== undefined || flags.token !== undefined;
90
+ }
91
+ function collectProvidedConfigFields(flags) {
92
+ const fields = new Set();
93
+ for (const field of UPDATE_SPECIAL_FIELDS) {
94
+ if (field === 'access-token') {
95
+ if (hasTokenOverride(flags)) {
96
+ fields.add(field);
97
+ }
98
+ continue;
99
+ }
100
+ if (flags[field] !== undefined) {
101
+ fields.add(field);
102
+ }
103
+ }
104
+ for (const field of UPDATE_STRING_FLAGS) {
105
+ if (flags[field] !== undefined) {
106
+ fields.add(field);
107
+ }
108
+ }
109
+ for (const field of UPDATE_BOOLEAN_FLAGS) {
110
+ if (flags[field] !== undefined) {
111
+ fields.add(field);
112
+ }
113
+ }
114
+ return fields;
115
+ }
116
+ function normalizeUnsetFields(unset) {
117
+ const normalized = (unset ?? [])
118
+ .flatMap((value) => value.split(','))
119
+ .map((value) => value.trim())
120
+ .filter(Boolean);
121
+ for (const field of normalized) {
122
+ if (!UNSETTABLE_FIELDS.has(field)) {
123
+ throw new Error(`Unsupported --unset field "${field}". Supported fields: ${Array.from(UNSETTABLE_FIELDS).sort().join(', ')}.`);
124
+ }
125
+ }
126
+ return Array.from(new Set(normalized));
127
+ }
128
+ function buildCurrentConfigInput(env) {
129
+ return {
130
+ ...env.config,
131
+ apiBaseUrl: env.apiBaseUrl,
132
+ authType: env.authType,
133
+ authUsername: env.config.authUsername,
134
+ accessToken: env.auth?.type === 'token' ? env.auth.accessToken : undefined,
135
+ };
136
+ }
137
+ function applyUnsetField(nextInput, field) {
138
+ switch (field) {
139
+ case 'api-base-url':
140
+ delete nextInput.apiBaseUrl;
141
+ return;
142
+ case 'auth-type':
143
+ delete nextInput.authType;
144
+ return;
145
+ case 'access-token':
146
+ delete nextInput.accessToken;
147
+ return;
148
+ case 'username':
149
+ delete nextInput.authUsername;
150
+ return;
151
+ default:
152
+ if (field in ENV_STRING_CONFIG_FLAG_MAP) {
153
+ delete nextInput[ENV_STRING_CONFIG_FLAG_MAP[field]];
154
+ return;
155
+ }
156
+ if (field in ENV_BOOLEAN_CONFIG_FLAG_MAP) {
157
+ delete nextInput[ENV_BOOLEAN_CONFIG_FLAG_MAP[field]];
158
+ }
159
+ }
160
+ }
17
161
  export default class EnvUpdate extends Command {
18
- static summary = 'Refresh an environment runtime from swagger:get and persist connection overrides';
162
+ static summary = 'Refresh an environment runtime from swagger:get, or update the saved env config for one environment';
19
163
  static examples = [
20
164
  '<%= config.bin %> <%= command.id %>',
21
165
  '<%= config.bin %> <%= command.id %> prod',
166
+ '<%= config.bin %> <%= command.id %> prod --api-base-url http://localhost:13000/api --access-token <token>',
167
+ '<%= config.bin %> <%= command.id %> local --app-port 13080 --timezone Asia/Shanghai',
168
+ '<%= config.bin %> <%= command.id %> local --cdn-base-url https://cdn.example.com/nocobase/',
169
+ '<%= config.bin %> <%= command.id %> local --unset git-url --unset npm-registry',
22
170
  ];
23
171
  static args = {
24
172
  name: Args.string({
25
- description: 'Configured environment name to refresh. Defaults to the current env when omitted',
173
+ description: 'Configured environment name to update. Defaults to the current env when omitted',
26
174
  required: false,
27
175
  }),
28
176
  };
@@ -32,33 +180,138 @@ export default class EnvUpdate extends Command {
32
180
  default: false,
33
181
  }),
34
182
  'api-base-url': Flags.string({
35
- description: 'NocoBase API base URL override. When provided, persist it to the target env before saving the refreshed runtime.',
183
+ char: 'u',
184
+ description: 'Root URL for HTTP API calls, including the /api prefix (e.g. http://localhost:13000/api)',
36
185
  }),
37
- role: Flags.string({
38
- description: 'Role override, sent as X-Role',
186
+ 'auth-type': Flags.string({
187
+ description: 'Authentication: basic, token, or oauth',
188
+ options: ['basic', 'token', 'oauth'],
39
189
  }),
40
- token: Flags.string({
190
+ 'access-token': Flags.string({
41
191
  char: 't',
42
- description: 'API key override. When provided, persist it to the target env before saving the refreshed runtime.',
192
+ aliases: ['token'],
193
+ description: 'API key or access token for token-based authentication',
194
+ }),
195
+ username: Flags.string({
196
+ description: 'Username to save for basic authentication',
197
+ }),
198
+ source: Flags.string({
199
+ description: 'Saved application source type for this env',
200
+ options: ['docker', 'git', 'local', 'npm'],
201
+ }),
202
+ 'download-version': Flags.string({
203
+ aliases: ['version'],
204
+ description: 'Saved downloaded app version for this env',
205
+ }),
206
+ 'docker-registry': Flags.string({
207
+ description: 'Saved Docker registry for this env',
208
+ }),
209
+ 'docker-platform': Flags.string({
210
+ description: 'Saved Docker image platform for this env',
211
+ options: ['auto', 'linux/amd64', 'linux/arm64'],
212
+ }),
213
+ 'git-url': Flags.string({
214
+ description: 'Saved Git repository URL for this env',
215
+ }),
216
+ 'npm-registry': Flags.string({
217
+ description: 'Saved npm registry for this env',
218
+ }),
219
+ 'dev-dependencies': Flags.boolean({
220
+ allowNo: true,
221
+ description: 'Whether development dependencies are installed for this env',
222
+ }),
223
+ build: Flags.boolean({
224
+ allowNo: true,
225
+ description: 'Whether the app should be built after source download',
226
+ }),
227
+ 'build-dts': Flags.boolean({
228
+ allowNo: true,
229
+ description: 'Whether declaration files should be emitted during build',
230
+ }),
231
+ 'app-path': Flags.string({
232
+ description: 'Saved app path for this env',
233
+ }),
234
+ 'app-root-path': Flags.string({
235
+ hidden: true,
236
+ description: 'Saved application root path for this env',
237
+ }),
238
+ 'storage-path': Flags.string({
239
+ hidden: true,
240
+ description: 'Saved storage path for this env',
241
+ }),
242
+ 'app-public-path': Flags.string({
243
+ description: 'Saved application public path for this env',
244
+ }),
245
+ 'cdn-base-url': Flags.string({
246
+ description: 'Saved client asset CDN base URL (CDN_BASE_URL) for this env',
247
+ }),
248
+ 'env-file': Flags.string({
249
+ hidden: true,
250
+ description: 'Saved Docker --env-file path for this env',
251
+ }),
252
+ 'app-port': Flags.string({
253
+ description: 'Saved application HTTP port for this env',
254
+ }),
255
+ 'app-key': Flags.string({
256
+ description: 'Saved application secret key for this env',
257
+ }),
258
+ timezone: Flags.string({
259
+ description: 'Saved application timezone for this env',
260
+ }),
261
+ 'builtin-db': Flags.boolean({
262
+ allowNo: true,
263
+ description: 'Whether this env uses a CLI-managed built-in database',
264
+ }),
265
+ 'db-dialect': Flags.string({
266
+ description: 'Saved database dialect for this env',
267
+ options: ['kingbase', 'mariadb', 'mysql', 'postgres'],
268
+ }),
269
+ 'builtin-db-image': Flags.string({
270
+ description: 'Saved built-in database image for this env',
271
+ }),
272
+ 'db-host': Flags.string({
273
+ description: 'Saved database host for this env',
274
+ }),
275
+ 'db-port': Flags.string({
276
+ description: 'Saved database port for this env',
277
+ }),
278
+ 'db-database': Flags.string({
279
+ description: 'Saved database name for this env',
280
+ }),
281
+ 'db-user': Flags.string({
282
+ description: 'Saved database user for this env',
283
+ }),
284
+ 'db-password': Flags.string({
285
+ description: 'Saved database password for this env',
286
+ }),
287
+ 'db-schema': Flags.string({
288
+ description: 'Saved database schema for this env',
289
+ }),
290
+ 'db-table-prefix': Flags.string({
291
+ description: 'Saved database table prefix for this env',
292
+ }),
293
+ 'db-underscored': Flags.boolean({
294
+ allowNo: true,
295
+ description: 'Whether this env uses underscored database naming',
296
+ }),
297
+ unset: Flags.string({
298
+ multiple: true,
299
+ description: 'Unset one or more saved env config fields by canonical flag name',
43
300
  }),
44
301
  };
45
- async run() {
46
- const { args, flags } = await this.parse(EnvUpdate);
47
- setVerboseMode(Boolean(flags.verbose));
48
- const envName = args.name;
49
- const envLabel = envName ?? (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
50
- startTask(`Updating env runtime: ${envLabel}`);
302
+ buildRuntimeUpdateTaskMessage(envLabel) {
303
+ return `Updating env runtime: ${envLabel}`;
304
+ }
305
+ async refreshRuntime(envName, envLabel, verbose) {
306
+ startTask(this.buildRuntimeUpdateTaskMessage(envLabel));
51
307
  try {
52
308
  const runtime = await updateEnvRuntime({
53
309
  envName,
54
310
  scope: resolveDefaultConfigScope(),
55
- baseUrl: flags['api-base-url'],
56
- role: flags.role,
57
- token: flags.token,
58
- configFile: path.join(path.dirname(path.dirname(path.dirname(__dirname))), 'nocobase-ctl.config.json'),
59
- verbose: flags.verbose,
311
+ configFile: __dirnameConfigFile,
312
+ verbose,
60
313
  });
61
- if (flags.verbose) {
314
+ if (verbose) {
62
315
  succeedTask(`Updated env "${envLabel}" to runtime "${runtime.version}".`);
63
316
  }
64
317
  else {
@@ -71,4 +324,115 @@ export default class EnvUpdate extends Command {
71
324
  throw error;
72
325
  }
73
326
  }
327
+ printConfigUpdateHints(envName, changedFields, nextConfig) {
328
+ if (changedFields.size === 0) {
329
+ return;
330
+ }
331
+ printInfo('Saved env config was updated. Runtime commands were not refreshed automatically.');
332
+ const shouldRestartWithDb = Array.from(changedFields).some((field) => APP_RESTART_WITH_DB_FIELDS.has(field)) &&
333
+ (nextConfig.builtinDb === true || changedFields.has('builtin-db'));
334
+ if (shouldRestartWithDb) {
335
+ printInfo(`Run \`nb app restart --env ${envName} --with-db\` when you're ready to apply these changes.`);
336
+ return;
337
+ }
338
+ if (Array.from(changedFields).some((field) => APP_RESTART_FIELDS.has(field))) {
339
+ printInfo(`Run \`nb app restart --env ${envName}\` when you're ready to apply these changes.`);
340
+ }
341
+ if (Array.from(changedFields).some((field) => SOURCE_SETTING_FIELDS.has(field))) {
342
+ printInfo('Saved source settings were updated. Existing local source files are not replaced automatically.');
343
+ }
344
+ if (Array.from(changedFields).some((field) => field === 'auth-type' || field === 'access-token' || field === 'username') &&
345
+ (nextConfig.authType === 'basic' || nextConfig.authType === 'oauth' || !nextConfig.accessToken)) {
346
+ printInfo(`Run \`nb env auth ${envName}\` if you need to authenticate this env again before using runtime commands.`);
347
+ }
348
+ }
349
+ async run() {
350
+ const { args, flags } = await this.parse(EnvUpdate);
351
+ const parsedFlags = flags;
352
+ setVerboseMode(Boolean(parsedFlags.verbose));
353
+ const unsetFields = normalizeUnsetFields(parsedFlags.unset);
354
+ const providedFields = collectProvidedConfigFields(parsedFlags);
355
+ const hasConfigChanges = providedFields.size > 0 || unsetFields.length > 0;
356
+ const tokenOverride = hasTokenOverride(parsedFlags);
357
+ for (const field of unsetFields) {
358
+ if (providedFields.has(field)) {
359
+ this.error(`Cannot combine --unset ${field} with an explicit update for the same field.`);
360
+ }
361
+ }
362
+ if (tokenOverride && parsedFlags['auth-type'] && parsedFlags['auth-type'] !== 'token') {
363
+ this.error('--access-token or --token can only be used with --auth-type token.');
364
+ }
365
+ if (!hasConfigChanges) {
366
+ const envName = args.name;
367
+ const envLabel = envName ?? (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
368
+ await this.refreshRuntime(envName, envLabel, Boolean(parsedFlags.verbose));
369
+ return;
370
+ }
371
+ const currentEnv = await getEnv(args.name, { scope: resolveDefaultConfigScope() });
372
+ if (!currentEnv) {
373
+ this.error(args.name?.trim()
374
+ ? `Env "${args.name.trim()}" is not configured`
375
+ : 'No env is configured. Run `nb init --ui` or `nb env add <name> --api-base-url <url>` first.');
376
+ }
377
+ const envName = String(currentEnv.name ?? '').trim();
378
+ const effectiveAuthType = parsedFlags['auth-type'] ?? (tokenOverride ? 'token' : currentEnv.authType);
379
+ if (parsedFlags.username !== undefined && effectiveAuthType !== 'basic') {
380
+ this.error('--username can only be used when the env uses basic authentication.');
381
+ }
382
+ const nextInput = buildCurrentConfigInput(currentEnv);
383
+ nextInput.apiBaseUrl = currentEnv.apiBaseUrl;
384
+ nextInput.authType = currentEnv.authType;
385
+ nextInput.authUsername = currentEnv.config.authUsername;
386
+ if (parsedFlags['api-base-url'] !== undefined) {
387
+ nextInput.apiBaseUrl = parsedFlags['api-base-url'];
388
+ }
389
+ if (parsedFlags['auth-type'] !== undefined) {
390
+ nextInput.authType = parsedFlags['auth-type'];
391
+ }
392
+ if (parsedFlags.username !== undefined) {
393
+ nextInput.authUsername = String(parsedFlags.username ?? '').trim();
394
+ }
395
+ if (tokenOverride) {
396
+ nextInput.authType = 'token';
397
+ nextInput.accessToken = parsedFlags['access-token'] ?? parsedFlags.token;
398
+ }
399
+ for (const field of UPDATE_STRING_FLAGS) {
400
+ if (parsedFlags[field] !== undefined) {
401
+ nextInput[ENV_STRING_CONFIG_FLAG_MAP[field]] = parsedFlags[field];
402
+ }
403
+ }
404
+ for (const field of UPDATE_BOOLEAN_FLAGS) {
405
+ if (parsedFlags[field] !== undefined) {
406
+ nextInput[ENV_BOOLEAN_CONFIG_FLAG_MAP[field]] = parsedFlags[field];
407
+ }
408
+ }
409
+ for (const field of unsetFields) {
410
+ applyUnsetField(nextInput, field);
411
+ }
412
+ const nextConfig = buildStoredEnvConfig(nextInput);
413
+ startTask(`Saving env config: ${envName}`);
414
+ try {
415
+ await replaceEnvConfig(envName, nextConfig, { scope: resolveDefaultConfigScope() });
416
+ succeedTask(`Saved env config for "${envName}".`);
417
+ }
418
+ catch (error) {
419
+ failTask(`Failed to save env config for "${envName}".`);
420
+ throw error;
421
+ }
422
+ const shouldRefreshRuntime = providedFields.has('api-base-url') || providedFields.has('access-token');
423
+ if (!shouldRefreshRuntime) {
424
+ this.printConfigUpdateHints(envName, new Set([...providedFields, ...unsetFields]), nextConfig);
425
+ return;
426
+ }
427
+ try {
428
+ await this.refreshRuntime(envName, envName, Boolean(parsedFlags.verbose));
429
+ }
430
+ catch (error) {
431
+ this.printConfigUpdateHints(envName, new Set([...providedFields, ...unsetFields]), nextConfig);
432
+ const message = error instanceof Error ? error.message : String(error);
433
+ printWarningBlock(`Saved env config for "${envName}", but failed to refresh the runtime.\n${message}`);
434
+ return;
435
+ }
436
+ printVerbose(`Updated env "${envName}" config and refreshed the runtime.`);
437
+ }
74
438
  }