@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
@@ -13,7 +13,7 @@ import { buildStoredEnvConfig, } from '../../lib/env-config.js';
13
13
  import { runPromptCatalog, } from '../../lib/prompt-catalog.js';
14
14
  import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from '../../lib/cli-locale.js';
15
15
  import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
16
- import { printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
16
+ import { printInfo, printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
17
17
  const ENV_RUNTIME_FLAG_MAP = {
18
18
  source: 'source',
19
19
  'download-version': 'downloadVersion',
@@ -33,6 +33,8 @@ const ENV_RUNTIME_FLAG_MAP = {
33
33
  'db-database': 'dbDatabase',
34
34
  'db-user': 'dbUser',
35
35
  'db-password': 'dbPassword',
36
+ 'db-schema': 'dbSchema',
37
+ 'db-table-prefix': 'dbTablePrefix',
36
38
  'root-username': 'rootUsername',
37
39
  'root-email': 'rootEmail',
38
40
  'root-password': 'rootPassword',
@@ -43,10 +45,44 @@ const ENV_BOOLEAN_RUNTIME_FLAG_MAP = {
43
45
  'dev-dependencies': 'devDependencies',
44
46
  build: 'build',
45
47
  'build-dts': 'buildDts',
48
+ 'db-underscored': 'dbUnderscored',
46
49
  };
47
50
  const envAddText = (key, values) => localeText(`commands.envAdd.${key}`, values);
51
+ const envAddAccessTokenPrompt = {
52
+ type: 'text',
53
+ message: envAddText('prompts.accessToken.message'),
54
+ required: true,
55
+ hidden: (values) => values.authType !== 'token' || values.skipAuth === true,
56
+ };
57
+ const envAddUsernamePrompt = {
58
+ type: 'text',
59
+ message: envAddText('prompts.username.message'),
60
+ placeholder: envAddText('prompts.username.placeholder'),
61
+ required: true,
62
+ hidden: (values) => values.authType !== 'basic' || values.skipAuth === true,
63
+ };
64
+ const envAddPasswordPrompt = {
65
+ type: 'password',
66
+ message: envAddText('prompts.password.message'),
67
+ required: true,
68
+ hidden: (values) => values.authType !== 'basic' || values.skipAuth === true,
69
+ };
70
+ function formatDeferredAuthMessage(envName, authType) {
71
+ const normalizedAuthType = String(authType ?? '').trim();
72
+ const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
73
+ if (normalizedAuthType === 'basic') {
74
+ return `${nextStep} You will be prompted for a username and password.`;
75
+ }
76
+ if (normalizedAuthType === 'token') {
77
+ return `${nextStep} You will be prompted for an access token.`;
78
+ }
79
+ if (normalizedAuthType === 'oauth') {
80
+ return `${nextStep} A browser sign-in flow will be started.`;
81
+ }
82
+ return nextStep;
83
+ }
48
84
  export default class EnvAdd extends Command {
49
- static summary = 'Save a named NocoBase API endpoint (token or OAuth), then switch the CLI to use it';
85
+ static summary = 'Save a named NocoBase API endpoint (basic, token, or OAuth), then switch the CLI to use it';
50
86
  static examples = [
51
87
  '<%= config.bin %> <%= command.id %>',
52
88
  '<%= config.bin %> <%= command.id %> local',
@@ -89,14 +125,24 @@ export default class EnvAdd extends Command {
89
125
  }),
90
126
  'auth-type': Flags.string({
91
127
  char: 'a',
92
- description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`); prompted in a TTY when omitted',
93
- options: ['token', 'oauth'],
128
+ description: 'Authentication: basic (username/password login), token (API key), or oauth (browser login via `nb env auth`); prompted in a TTY when omitted',
129
+ options: ['basic', 'token', 'oauth'],
94
130
  }),
95
131
  'access-token': Flags.string({
96
132
  char: 't',
97
133
  aliases: ['token'],
98
134
  description: 'API key or access token when using --auth-type token (prompted in a TTY when omitted)',
99
135
  }),
136
+ username: Flags.string({
137
+ description: 'Username when using --auth-type basic (prompted in a TTY when omitted)',
138
+ }),
139
+ password: Flags.string({
140
+ description: 'Password when using --auth-type basic (prompted in a TTY when omitted)',
141
+ }),
142
+ 'skip-auth': Flags.boolean({
143
+ description: 'Save the env now and finish authentication later with `nb env auth`',
144
+ default: false,
145
+ }),
100
146
  source: Flags.string({
101
147
  hidden: true,
102
148
  description: 'Application source saved with this env',
@@ -189,6 +235,19 @@ export default class EnvAdd extends Command {
189
235
  hidden: true,
190
236
  description: 'Database password saved with this env',
191
237
  }),
238
+ 'db-schema': Flags.string({
239
+ hidden: true,
240
+ description: 'Database schema saved with this env',
241
+ }),
242
+ 'db-table-prefix': Flags.string({
243
+ hidden: true,
244
+ description: 'Database table prefix saved with this env',
245
+ }),
246
+ 'db-underscored': Flags.boolean({
247
+ allowNo: true,
248
+ hidden: true,
249
+ description: 'Whether this env uses underscored database naming',
250
+ }),
192
251
  'root-username': Flags.string({
193
252
  hidden: true,
194
253
  description: 'Initial root username saved with this env',
@@ -224,6 +283,11 @@ export default class EnvAdd extends Command {
224
283
  type: 'select',
225
284
  message: envAddText('prompts.authType.message'),
226
285
  options: [
286
+ {
287
+ value: 'basic',
288
+ label: envAddText('prompts.authType.basicLabel'),
289
+ hint: envAddText('prompts.authType.basicHint'),
290
+ },
227
291
  {
228
292
  value: 'oauth',
229
293
  label: envAddText('prompts.authType.oauthLabel'),
@@ -234,13 +298,9 @@ export default class EnvAdd extends Command {
234
298
  initialValue: 'oauth',
235
299
  required: true,
236
300
  },
237
- accessToken: {
238
- type: 'text',
239
- message: envAddText('prompts.accessToken.message'),
240
- placeholder: envAddText('prompts.accessToken.placeholder'),
241
- required: true,
242
- hidden: (values) => values.authType !== 'token',
243
- },
301
+ username: envAddUsernamePrompt,
302
+ password: envAddPasswordPrompt,
303
+ accessToken: envAddAccessTokenPrompt,
244
304
  };
245
305
  buildPromptValues(nameArg, flags) {
246
306
  const values = {};
@@ -255,10 +315,19 @@ export default class EnvAdd extends Command {
255
315
  if (flags['auth-type']) {
256
316
  values.authType = flags['auth-type'];
257
317
  }
318
+ if (flags['skip-auth']) {
319
+ values.skipAuth = true;
320
+ }
258
321
  const token = flags['access-token'] ?? flags.token;
259
322
  if (typeof token === 'string' && token !== '') {
260
323
  values.accessToken = token;
261
324
  }
325
+ if (flags.username !== undefined) {
326
+ values.username = String(flags.username ?? '').trim();
327
+ }
328
+ if (flags.password !== undefined) {
329
+ values.password = String(flags.password ?? '');
330
+ }
262
331
  return values;
263
332
  }
264
333
  buildPromptInitialValues(flags) {
@@ -269,10 +338,35 @@ export default class EnvAdd extends Command {
269
338
  }
270
339
  return initialValues;
271
340
  }
341
+ buildPromptCatalog(flags) {
342
+ if (!flags['skip-auth']) {
343
+ return EnvAdd.prompts;
344
+ }
345
+ return {
346
+ ...EnvAdd.prompts,
347
+ username: {
348
+ ...envAddUsernamePrompt,
349
+ hidden: () => true,
350
+ },
351
+ password: {
352
+ ...envAddPasswordPrompt,
353
+ hidden: () => true,
354
+ },
355
+ accessToken: {
356
+ ...envAddAccessTokenPrompt,
357
+ hidden: () => true,
358
+ },
359
+ };
360
+ }
272
361
  buildEnvConfig(results, flags) {
362
+ const authType = String(results.authType ?? '').trim();
363
+ const authUsername = authType === 'basic'
364
+ ? String(results.username ?? flags.username ?? '').trim()
365
+ : '';
273
366
  const envConfigInput = {
274
367
  apiBaseUrl: results.apiBaseUrl,
275
- authType: results.authType,
368
+ authType,
369
+ authUsername: authUsername || undefined,
276
370
  accessToken: results.accessToken,
277
371
  };
278
372
  for (const [flagName, configKey] of Object.entries(ENV_RUNTIME_FLAG_MAP)) {
@@ -288,12 +382,15 @@ export default class EnvAdd extends Command {
288
382
  async run() {
289
383
  const { args, flags } = await this.parse(EnvAdd);
290
384
  const parsedFlags = flags;
385
+ if (parsedFlags['skip-auth'] && (parsedFlags['access-token'] !== undefined || parsedFlags.token !== undefined)) {
386
+ this.error('--skip-auth cannot be used with --access-token or --token.');
387
+ }
291
388
  applyCliLocale(parsedFlags.locale);
292
389
  setVerboseMode(parsedFlags.verbose);
293
390
  if (!parsedFlags['no-intro']) {
294
391
  printStage('Connect to NocoBase');
295
392
  }
296
- const results = await runPromptCatalog(EnvAdd.prompts, {
393
+ const results = await runPromptCatalog(this.buildPromptCatalog(parsedFlags), {
297
394
  values: this.buildPromptValues(args.name, parsedFlags),
298
395
  initialValues: this.buildPromptInitialValues(parsedFlags),
299
396
  command: this,
@@ -303,8 +400,25 @@ export default class EnvAdd extends Command {
303
400
  printVerbose(`Saving env "${envName}" globally.`);
304
401
  await upsertEnv(envName, envConfig, { scope: resolveDefaultConfigScope() });
305
402
  await setCurrentEnv(envName, { scope: resolveDefaultConfigScope() });
306
- if (results.authType === 'oauth') {
307
- await this.config.runCommand('env:auth', [envName]);
403
+ if (parsedFlags['skip-auth']) {
404
+ printSuccess(`✔ Env "${envName}" was saved.`);
405
+ printInfo(formatDeferredAuthMessage(envName, results.authType));
406
+ return;
407
+ }
408
+ if (results.authType === 'oauth' || results.authType === 'basic') {
409
+ const authArgv = [envName];
410
+ if (results.authType === 'basic') {
411
+ authArgv.push('--auth-type', 'basic');
412
+ const username = String(results.username ?? '').trim();
413
+ const password = String(results.password ?? '');
414
+ if (username) {
415
+ authArgv.push('--username', username);
416
+ }
417
+ if (password) {
418
+ authArgv.push('--password', password);
419
+ }
420
+ }
421
+ await this.config.runCommand('env:auth', authArgv);
308
422
  }
309
423
  await this.config.runCommand('env:update', [envName]);
310
424
  printSuccess(`✔ Env "${envName}" is ready.`);
@@ -7,15 +7,31 @@
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 { getCurrentEnvName } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, resolveConfiguredAuthType, updateEnvConnection } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
- import { authenticateEnvWithOauth } from '../../lib/env-auth.js';
13
- import { failTask, printStage, startTask, succeedTask } from '../../lib/ui.js';
12
+ import { authenticateEnvWithBasic, authenticateEnvWithOauth } from '../../lib/env-auth.js';
13
+ import { runPromptCatalog } from '../../lib/prompt-catalog.js';
14
+ import { failTask, isInteractiveTerminal, printStage, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
+ import EnvAdd from "./add.js";
16
+ const envAuthPrompts = {
17
+ authType: EnvAdd.prompts.authType,
18
+ username: EnvAdd.prompts.username,
19
+ password: EnvAdd.prompts.password,
20
+ accessToken: EnvAdd.prompts.accessToken,
21
+ };
22
+ function resolveExplicitAuthType(value) {
23
+ return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
24
+ }
25
+ function formatMissingEnvMessage(envName) {
26
+ return [`Env "${envName}" is not configured.`, `Run \`nb env add ${envName} --api-base-url <url>\` first.`].join('\n');
27
+ }
14
28
  export default class EnvAuth extends Command {
15
- static summary = 'Sign in to a saved NocoBase environment with OAuth';
29
+ static summary = 'Authenticate a saved NocoBase environment with basic login, a token, or OAuth';
16
30
  static examples = [
17
31
  '<%= config.bin %> <%= command.id %>',
18
32
  '<%= config.bin %> <%= command.id %> prod',
33
+ '<%= config.bin %> <%= command.id %> prod --auth-type basic --username admin --password secret',
34
+ '<%= config.bin %> <%= command.id %> prod --auth-type token --access-token <api-key>',
19
35
  ];
20
36
  static args = {
21
37
  name: Args.string({
@@ -30,6 +46,21 @@ export default class EnvAuth extends Command {
30
46
  deprecated: true,
31
47
  description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
32
48
  }),
49
+ 'auth-type': Flags.string({
50
+ char: 'a',
51
+ description: 'Authentication: basic (username/password login), token (API key), or oauth (browser login)',
52
+ options: ['basic', 'token', 'oauth'],
53
+ }),
54
+ 'access-token': Flags.string({
55
+ char: 't',
56
+ description: 'API key or access token when using token authentication',
57
+ }),
58
+ username: Flags.string({
59
+ description: 'Username when using basic authentication (prompted in a TTY when omitted)',
60
+ }),
61
+ password: Flags.string({
62
+ description: 'Password when using basic authentication (prompted in a TTY when omitted)',
63
+ }),
33
64
  };
34
65
  async run() {
35
66
  const { args, flags } = await this.parse(EnvAuth);
@@ -39,17 +70,119 @@ export default class EnvAuth extends Command {
39
70
  this.error(`Environment name was provided both as the argument ("${nameArg}") and as --env ("${nameFlag}"). Please use only one.`);
40
71
  }
41
72
  const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
42
- printStage('Signing in');
43
- startTask(`Starting browser sign-in for "${envName}"...`);
73
+ const env = await getEnv(envName, { scope: resolveDefaultConfigScope() });
74
+ if (!env) {
75
+ this.error(formatMissingEnvMessage(envName));
76
+ }
77
+ const tokenFromFlags = flags['access-token'];
78
+ const tokenFlagProvided = tokenFromFlags !== undefined;
79
+ const tokenValue = typeof tokenFromFlags === 'string' ? tokenFromFlags.trim() : '';
80
+ const tokenProvided = tokenValue !== '';
81
+ if (tokenFlagProvided && !tokenProvided) {
82
+ this.error('--access-token cannot be empty.');
83
+ }
84
+ const usernameFromFlags = flags.username;
85
+ const usernameFlagProvided = usernameFromFlags !== undefined;
86
+ const usernameProvided = typeof usernameFromFlags === 'string' && usernameFromFlags.trim() !== '';
87
+ if (usernameFlagProvided && !usernameProvided) {
88
+ this.error('--username cannot be empty.');
89
+ }
90
+ const passwordFromFlags = flags.password;
91
+ const passwordFlagProvided = passwordFromFlags !== undefined;
92
+ const passwordProvided = typeof passwordFromFlags === 'string' && passwordFromFlags.trim() !== '';
93
+ if (passwordFlagProvided && !passwordProvided) {
94
+ this.error('--password cannot be empty.');
95
+ }
96
+ const explicitAuthType = resolveExplicitAuthType(flags['auth-type']);
97
+ if (tokenFlagProvided && (usernameFlagProvided || passwordFlagProvided)) {
98
+ this.error('--access-token cannot be used with --username or --password.');
99
+ }
100
+ if (explicitAuthType === 'oauth' && (tokenFlagProvided || usernameFlagProvided || passwordFlagProvided)) {
101
+ this.error('--auth-type oauth cannot be used with --access-token, --username, or --password.');
102
+ }
103
+ if (explicitAuthType === 'token' && (usernameFlagProvided || passwordFlagProvided)) {
104
+ this.error('--auth-type token cannot be used with --username or --password.');
105
+ }
106
+ if (explicitAuthType === 'basic' && tokenFlagProvided) {
107
+ this.error('--auth-type basic cannot be used with --access-token.');
108
+ }
109
+ const savedAuthType = resolveConfiguredAuthType(env.config);
110
+ const resolvedAuthType = explicitAuthType ??
111
+ (tokenProvided ? 'token' : usernameFlagProvided || passwordFlagProvided ? 'basic' : savedAuthType);
112
+ if (resolvedAuthType === 'basic' && !usernameProvided && !isInteractiveTerminal()) {
113
+ this.error('--username is required when using basic authentication in non-interactive mode.');
114
+ }
115
+ if (resolvedAuthType === 'basic' && !passwordProvided && !isInteractiveTerminal()) {
116
+ this.error('--password is required when using basic authentication in non-interactive mode.');
117
+ }
118
+ const prompted = (resolvedAuthType === 'oauth'
119
+ ? { authType: 'oauth' }
120
+ : await runPromptCatalog(envAuthPrompts, {
121
+ values: {
122
+ ...(resolvedAuthType ? { authType: resolvedAuthType } : {}),
123
+ ...(usernameFlagProvided ? { username: String(usernameFromFlags ?? '').trim() } : {}),
124
+ ...(passwordFlagProvided ? { password: String(passwordFromFlags ?? '') } : {}),
125
+ ...(tokenFlagProvided ? { accessToken: tokenValue } : {}),
126
+ },
127
+ command: this,
128
+ })) ?? {};
129
+ const authType = resolveExplicitAuthType(prompted.authType ?? resolvedAuthType);
130
+ if (!authType) {
131
+ this.error('Choose an authentication type before continuing.');
132
+ }
133
+ printStage('Authenticating');
44
134
  try {
45
- await authenticateEnvWithOauth({
46
- envName,
47
- scope: resolveDefaultConfigScope(),
48
- });
49
- succeedTask(`✔ Signed in to "${envName}".`);
135
+ if (authType === 'basic') {
136
+ const username = String(prompted.username ?? usernameFromFlags ?? '').trim();
137
+ const password = String(prompted.password ?? passwordFromFlags ?? '');
138
+ if (!username) {
139
+ this.error('--username is required when using basic authentication.');
140
+ }
141
+ if (!password) {
142
+ this.error('--password cannot be empty.');
143
+ }
144
+ startTask(`Signing in with username and password for "${envName}"...`);
145
+ const accessToken = await authenticateEnvWithBasic({
146
+ envName,
147
+ username,
148
+ password,
149
+ scope: resolveDefaultConfigScope(),
150
+ });
151
+ await updateEnvConnection(envName, {
152
+ authType: 'basic',
153
+ authUsername: username,
154
+ accessToken,
155
+ }, { scope: resolveDefaultConfigScope() });
156
+ stopTask();
157
+ }
158
+ else if (authType === 'token') {
159
+ const accessToken = String(prompted.accessToken ?? tokenFromFlags ?? '').trim();
160
+ if (accessToken === '') {
161
+ this.error('--access-token cannot be empty.');
162
+ }
163
+ startTask(`Saving access token for "${envName}"...`);
164
+ await updateEnvConnection(envName, {
165
+ authType: 'token',
166
+ accessToken,
167
+ }, { scope: resolveDefaultConfigScope() });
168
+ stopTask();
169
+ }
170
+ else {
171
+ startTask(`Starting browser sign-in for "${envName}"...`);
172
+ await updateEnvConnection(envName, {
173
+ authType: 'oauth',
174
+ }, { scope: resolveDefaultConfigScope() });
175
+ await authenticateEnvWithOauth({
176
+ envName,
177
+ scope: resolveDefaultConfigScope(),
178
+ });
179
+ stopTask();
180
+ }
181
+ await this.config.runCommand('env:update', [envName]);
182
+ succeedTask(`✔ Authenticated "${envName}".`);
50
183
  }
51
184
  catch (error) {
52
- failTask(`Sign-in failed for "${envName}".`);
185
+ failTask(`Authentication failed for "${envName}".`);
53
186
  throw error;
54
187
  }
55
188
  }
@@ -10,7 +10,9 @@ import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
11
11
  import { resolveBuiltinDbConnection } from '../../lib/builtin-db.js';
12
12
  import { renderTable } from '../../lib/ui.js';
13
- import { appRootPath, dbStatus, runtimeStatus, storagePath } from './shared.js';
13
+ import { appRootPath, appUrl, dbStatus, runtimeStatus, storagePath } from './shared.js';
14
+ const MISSING_FIELD = Symbol('missingField');
15
+ const FORBIDDEN_FIELD_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
14
16
  function normalizeJsonValue(value) {
15
17
  if (value === undefined || value === null || value === '') {
16
18
  return '-';
@@ -43,6 +45,26 @@ function createGroupTable(title, values) {
43
45
  function serializeGroup(values) {
44
46
  return Object.fromEntries(Object.entries(values).map(([field, value]) => [field, normalizeJsonValue(value)]));
45
47
  }
48
+ function resolveFieldPath(value, path) {
49
+ const segments = path
50
+ .split('.')
51
+ .map((segment) => segment.trim())
52
+ .filter(Boolean);
53
+ if (segments.length === 0) {
54
+ return MISSING_FIELD;
55
+ }
56
+ let current = value;
57
+ for (const segment of segments) {
58
+ if (!current ||
59
+ typeof current !== 'object' ||
60
+ FORBIDDEN_FIELD_PATH_SEGMENTS.has(segment) ||
61
+ !Object.prototype.hasOwnProperty.call(current, segment)) {
62
+ return MISSING_FIELD;
63
+ }
64
+ current = current[segment];
65
+ }
66
+ return current;
67
+ }
46
68
  export default class EnvInfo extends Command {
47
69
  static hidden = false;
48
70
  static description = 'Show grouped details for the selected NocoBase env, including app, database, API, and auth settings.';
@@ -50,6 +72,7 @@ export default class EnvInfo extends Command {
50
72
  '<%= config.bin %> <%= command.id %> app1',
51
73
  '<%= config.bin %> <%= command.id %> app1 --json',
52
74
  '<%= config.bin %> <%= command.id %> app1 --show-secrets',
75
+ '<%= config.bin %> <%= command.id %> app1 --field app.url',
53
76
  ];
54
77
  static args = {
55
78
  name: Args.string({
@@ -68,6 +91,9 @@ export default class EnvInfo extends Command {
68
91
  description: 'Output the result as JSON',
69
92
  default: false,
70
93
  }),
94
+ field: Flags.string({
95
+ description: 'Return only a single field using dot notation, for example app.url or api.auth.type',
96
+ }),
71
97
  'show-secrets': Flags.boolean({
72
98
  description: 'Show secret values in plain text',
73
99
  default: false,
@@ -82,6 +108,7 @@ export default class EnvInfo extends Command {
82
108
  }
83
109
  const requestedEnv = envNameArg || envNameFlag;
84
110
  const showSecrets = flags['show-secrets'];
111
+ const fieldPath = flags.field?.trim() || undefined;
85
112
  const runtime = await resolveManagedAppRuntime(requestedEnv);
86
113
  if (!runtime) {
87
114
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
@@ -90,7 +117,9 @@ export default class EnvInfo extends Command {
90
117
  const builtinDbConnection = (runtime.kind === 'local' || runtime.kind === 'docker') && runtime.env.config.builtinDb
91
118
  ? await resolveBuiltinDbConnection(runtime)
92
119
  : undefined;
120
+ const dbDialect = builtinDbConnection?.dbDialect ?? runtime.env.config.dbDialect;
93
121
  const appGroup = {
122
+ url: appUrl(runtime),
94
123
  appRootPath: appRootPath(runtime),
95
124
  storagePath: storagePath(runtime),
96
125
  appPort: runtime.env.config.appPort,
@@ -104,16 +133,21 @@ export default class EnvInfo extends Command {
104
133
  const dbGroup = {
105
134
  databaseStatus: await dbStatus(runtime),
106
135
  builtinDb: runtime.env.config.builtinDb,
107
- dbDialect: runtime.env.config.dbDialect,
136
+ dbDialect,
108
137
  builtinDbImage: runtime.env.config.builtinDbImage,
109
138
  dbHost: builtinDbConnection?.dbHost ?? runtime.env.config.dbHost,
110
139
  dbPort: builtinDbConnection?.dbPort ?? runtime.env.config.dbPort,
111
140
  dbDatabase: runtime.env.config.dbDatabase,
112
141
  dbUser: runtime.env.config.dbUser,
113
142
  dbPassword: maskSecret(runtime.env.config.dbPassword, showSecrets),
143
+ dbTablePrefix: runtime.env.config.dbTablePrefix,
144
+ dbUnderscored: runtime.env.config.dbUnderscored,
145
+ ...(dbDialect === 'postgres' ? { dbSchema: runtime.env.config.dbSchema } : {}),
114
146
  };
115
147
  const authGroup = {
116
- type: auth?.type,
148
+ type: runtime.env.authType ?? auth?.type,
149
+ sessionType: auth?.type,
150
+ username: runtime.env.config.authUsername,
117
151
  expiresAt: auth?.type === 'oauth' ? auth.expiresAt : undefined,
118
152
  scope: auth?.type === 'oauth' ? auth.scope : undefined,
119
153
  issuer: auth?.type === 'oauth' ? auth.issuer : undefined,
@@ -125,6 +159,8 @@ export default class EnvInfo extends Command {
125
159
  const apiGroup = {
126
160
  apiBaseUrl: runtime.env.apiBaseUrl,
127
161
  'auth.type': authGroup.type,
162
+ 'auth.sessionType': authGroup.sessionType,
163
+ 'auth.username': authGroup.username,
128
164
  'auth.expiresAt': authGroup.expiresAt,
129
165
  'auth.scope': authGroup.scope,
130
166
  'auth.issuer': authGroup.issuer,
@@ -144,14 +180,22 @@ export default class EnvInfo extends Command {
144
180
  auth: serializeGroup(authGroup),
145
181
  },
146
182
  };
183
+ if (fieldPath) {
184
+ const selected = resolveFieldPath(output, fieldPath);
185
+ if (selected === MISSING_FIELD) {
186
+ this.error(`Unknown field "${fieldPath}". Use dot notation like app.url, db.databaseStatus, or api.auth.type.`);
187
+ }
188
+ if (flags.json) {
189
+ this.log(JSON.stringify(selected, null, 2));
190
+ return;
191
+ }
192
+ this.log(typeof selected === 'object' ? JSON.stringify(selected, null, 2) : String(selected));
193
+ return;
194
+ }
147
195
  if (flags.json) {
148
196
  this.log(JSON.stringify(output, null, 2));
149
197
  return;
150
198
  }
151
- this.log([
152
- createGroupTable('App', appGroup),
153
- createGroupTable('DB', dbGroup),
154
- createGroupTable('API', apiGroup),
155
- ].join('\n\n'));
199
+ this.log([createGroupTable('App', appGroup), createGroupTable('DB', dbGroup), createGroupTable('API', apiGroup)].join('\n\n'));
156
200
  }
157
201
  }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command } from '@oclif/core';
10
- import { getCurrentEnvName, listEnvs } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, listEnvs, resolveConfiguredAuthType } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { renderTable } from '../../lib/ui.js';
13
13
  import { resolveApiBaseUrl } from './shared.js';
@@ -35,7 +35,7 @@ export default class EnvList extends Command {
35
35
  name,
36
36
  env.kind ?? '-',
37
37
  resolveApiBaseUrl(env),
38
- env.auth?.type ?? '',
38
+ resolveConfiguredAuthType(env) ?? env.auth?.type ?? '',
39
39
  env.runtime?.version ?? '',
40
40
  ]);
41
41
  }
@@ -11,13 +11,51 @@ import { executeRawApiRequest } from '../../lib/api-client.js';
11
11
  export function resolveApiBaseUrl(config) {
12
12
  return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
13
13
  }
14
+ function buildAppPath(publicPath, subapp) {
15
+ const normalizedPublicPath = publicPath.replace(/\/+$/, '');
16
+ if (!subapp) {
17
+ return normalizedPublicPath ? `${normalizedPublicPath}/` : '/';
18
+ }
19
+ const normalizedSubapp = subapp.replace(/^\/+|\/+$/g, '');
20
+ return `${normalizedPublicPath ? normalizedPublicPath : ''}/apps/${normalizedSubapp}/`;
21
+ }
22
+ export function resolveAppUrlFromApiBaseUrl(apiBaseUrl) {
23
+ const value = String(apiBaseUrl ?? '').trim();
24
+ if (!value) {
25
+ return '';
26
+ }
27
+ try {
28
+ const url = new URL(value);
29
+ const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
30
+ if (subappMatch) {
31
+ url.pathname = buildAppPath(subappMatch[1] ?? '', subappMatch[2]);
32
+ url.search = '';
33
+ url.hash = '';
34
+ return url.toString();
35
+ }
36
+ const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
37
+ if (appMatch) {
38
+ url.pathname = buildAppPath(appMatch[1] ?? '');
39
+ url.search = '';
40
+ url.hash = '';
41
+ return url.toString();
42
+ }
43
+ }
44
+ catch {
45
+ return value;
46
+ }
47
+ return value;
48
+ }
14
49
  export function appUrl(runtime) {
50
+ const resolvedFromApiBaseUrl = resolveAppUrlFromApiBaseUrl(runtime.env.apiBaseUrl ?? resolveApiBaseUrl(runtime.env.config));
51
+ if (resolvedFromApiBaseUrl) {
52
+ return resolvedFromApiBaseUrl;
53
+ }
15
54
  const port = String(runtime.env.config.appPort ?? '').trim();
16
55
  if (port) {
17
- return `http://127.0.0.1:${port}`;
56
+ return `http://127.0.0.1:${port}/`;
18
57
  }
19
- const baseUrl = resolveApiBaseUrl(runtime.env.config);
20
- return baseUrl.replace(/\/api\/?$/, '');
58
+ return '';
21
59
  }
22
60
  export function appRootPath(runtime) {
23
61
  if (runtime.kind === 'http' || runtime.kind === 'docker') {