@nocobase/cli 2.1.0-beta.36 → 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.
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
  }
@@ -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({
@@ -54,9 +54,25 @@ const envAddAccessTokenPrompt = {
54
54
  required: true,
55
55
  hidden: (values) => values.authType !== 'token' || values.skipAuth === true,
56
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
+ };
57
70
  function formatDeferredAuthMessage(envName, authType) {
58
71
  const normalizedAuthType = String(authType ?? '').trim();
59
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
+ }
60
76
  if (normalizedAuthType === 'token') {
61
77
  return `${nextStep} You will be prompted for an access token.`;
62
78
  }
@@ -66,7 +82,7 @@ function formatDeferredAuthMessage(envName, authType) {
66
82
  return nextStep;
67
83
  }
68
84
  export default class EnvAdd extends Command {
69
- 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';
70
86
  static examples = [
71
87
  '<%= config.bin %> <%= command.id %>',
72
88
  '<%= config.bin %> <%= command.id %> local',
@@ -109,14 +125,20 @@ export default class EnvAdd extends Command {
109
125
  }),
110
126
  'auth-type': Flags.string({
111
127
  char: 'a',
112
- description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`); prompted in a TTY when omitted',
113
- 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'],
114
130
  }),
115
131
  'access-token': Flags.string({
116
132
  char: 't',
117
133
  aliases: ['token'],
118
134
  description: 'API key or access token when using --auth-type token (prompted in a TTY when omitted)',
119
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
+ }),
120
142
  'skip-auth': Flags.boolean({
121
143
  description: 'Save the env now and finish authentication later with `nb env auth`',
122
144
  default: false,
@@ -261,6 +283,11 @@ export default class EnvAdd extends Command {
261
283
  type: 'select',
262
284
  message: envAddText('prompts.authType.message'),
263
285
  options: [
286
+ {
287
+ value: 'basic',
288
+ label: envAddText('prompts.authType.basicLabel'),
289
+ hint: envAddText('prompts.authType.basicHint'),
290
+ },
264
291
  {
265
292
  value: 'oauth',
266
293
  label: envAddText('prompts.authType.oauthLabel'),
@@ -271,6 +298,8 @@ export default class EnvAdd extends Command {
271
298
  initialValue: 'oauth',
272
299
  required: true,
273
300
  },
301
+ username: envAddUsernamePrompt,
302
+ password: envAddPasswordPrompt,
274
303
  accessToken: envAddAccessTokenPrompt,
275
304
  };
276
305
  buildPromptValues(nameArg, flags) {
@@ -293,6 +322,12 @@ export default class EnvAdd extends Command {
293
322
  if (typeof token === 'string' && token !== '') {
294
323
  values.accessToken = token;
295
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
+ }
296
331
  return values;
297
332
  }
298
333
  buildPromptInitialValues(flags) {
@@ -309,6 +344,14 @@ export default class EnvAdd extends Command {
309
344
  }
310
345
  return {
311
346
  ...EnvAdd.prompts,
347
+ username: {
348
+ ...envAddUsernamePrompt,
349
+ hidden: () => true,
350
+ },
351
+ password: {
352
+ ...envAddPasswordPrompt,
353
+ hidden: () => true,
354
+ },
312
355
  accessToken: {
313
356
  ...envAddAccessTokenPrompt,
314
357
  hidden: () => true,
@@ -316,9 +359,14 @@ export default class EnvAdd extends Command {
316
359
  };
317
360
  }
318
361
  buildEnvConfig(results, flags) {
362
+ const authType = String(results.authType ?? '').trim();
363
+ const authUsername = authType === 'basic'
364
+ ? String(results.username ?? flags.username ?? '').trim()
365
+ : '';
319
366
  const envConfigInput = {
320
367
  apiBaseUrl: results.apiBaseUrl,
321
- authType: results.authType,
368
+ authType,
369
+ authUsername: authUsername || undefined,
322
370
  accessToken: results.accessToken,
323
371
  };
324
372
  for (const [flagName, configKey] of Object.entries(ENV_RUNTIME_FLAG_MAP)) {
@@ -357,8 +405,20 @@ export default class EnvAdd extends Command {
357
405
  printInfo(formatDeferredAuthMessage(envName, results.authType));
358
406
  return;
359
407
  }
360
- if (results.authType === 'oauth') {
361
- await this.config.runCommand('env:auth', [envName]);
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);
362
422
  }
363
423
  await this.config.runCommand('env:update', [envName]);
364
424
  printSuccess(`✔ Env "${envName}" is ready.`);
@@ -7,30 +7,30 @@
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, getEnv, resolveConfiguredAuthType, updateEnvConnection, } 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';
12
+ import { authenticateEnvWithBasic, authenticateEnvWithOauth } from '../../lib/env-auth.js';
13
13
  import { runPromptCatalog } from '../../lib/prompt-catalog.js';
14
- import { failTask, printStage, startTask, stopTask, succeedTask } from '../../lib/ui.js';
14
+ import { failTask, isInteractiveTerminal, printStage, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
15
  import EnvAdd from "./add.js";
16
16
  const envAuthPrompts = {
17
17
  authType: EnvAdd.prompts.authType,
18
+ username: EnvAdd.prompts.username,
19
+ password: EnvAdd.prompts.password,
18
20
  accessToken: EnvAdd.prompts.accessToken,
19
21
  };
20
22
  function resolveExplicitAuthType(value) {
21
- return value === 'token' || value === 'oauth' ? value : undefined;
23
+ return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
22
24
  }
23
25
  function formatMissingEnvMessage(envName) {
24
- return [
25
- `Env "${envName}" is not configured.`,
26
- `Run \`nb env add ${envName} --api-base-url <url>\` first.`,
27
- ].join('\n');
26
+ return [`Env "${envName}" is not configured.`, `Run \`nb env add ${envName} --api-base-url <url>\` first.`].join('\n');
28
27
  }
29
28
  export default class EnvAuth extends Command {
30
- static summary = 'Authenticate a saved NocoBase environment with a token or OAuth';
29
+ static summary = 'Authenticate a saved NocoBase environment with basic login, a token, or OAuth';
31
30
  static examples = [
32
31
  '<%= config.bin %> <%= command.id %>',
33
32
  '<%= config.bin %> <%= command.id %> prod',
33
+ '<%= config.bin %> <%= command.id %> prod --auth-type basic --username admin --password secret',
34
34
  '<%= config.bin %> <%= command.id %> prod --auth-type token --access-token <api-key>',
35
35
  ];
36
36
  static args = {
@@ -48,13 +48,19 @@ export default class EnvAuth extends Command {
48
48
  }),
49
49
  'auth-type': Flags.string({
50
50
  char: 'a',
51
- description: 'Authentication: token (API key) or oauth (browser login)',
52
- options: ['token', 'oauth'],
51
+ description: 'Authentication: basic (username/password login), token (API key), or oauth (browser login)',
52
+ options: ['basic', 'token', 'oauth'],
53
53
  }),
54
54
  'access-token': Flags.string({
55
55
  char: 't',
56
56
  description: 'API key or access token when using token authentication',
57
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
+ }),
58
64
  };
59
65
  async run() {
60
66
  const { args, flags } = await this.parse(EnvAuth);
@@ -63,9 +69,6 @@ export default class EnvAuth extends Command {
63
69
  if (nameArg && nameFlag && nameArg !== nameFlag) {
64
70
  this.error(`Environment name was provided both as the argument ("${nameArg}") and as --env ("${nameFlag}"). Please use only one.`);
65
71
  }
66
- if (flags['auth-type'] === 'oauth' && flags['access-token'] !== undefined) {
67
- this.error('--access-token cannot be used with --auth-type oauth.');
68
- }
69
72
  const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
70
73
  const env = await getEnv(envName, { scope: resolveDefaultConfigScope() });
71
74
  if (!env) {
@@ -73,32 +76,88 @@ export default class EnvAuth extends Command {
73
76
  }
74
77
  const tokenFromFlags = flags['access-token'];
75
78
  const tokenFlagProvided = tokenFromFlags !== undefined;
76
- const tokenProvided = typeof tokenFromFlags === 'string' && tokenFromFlags !== '';
79
+ const tokenValue = typeof tokenFromFlags === 'string' ? tokenFromFlags.trim() : '';
80
+ const tokenProvided = tokenValue !== '';
77
81
  if (tokenFlagProvided && !tokenProvided) {
78
82
  this.error('--access-token cannot be empty.');
79
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
+ }
80
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
+ }
81
109
  const savedAuthType = resolveConfiguredAuthType(env.config);
82
- const resolvedAuthType = explicitAuthType ?? (tokenProvided ? 'token' : savedAuthType);
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
+ }
83
118
  const prompted = (resolvedAuthType === 'oauth'
84
119
  ? { authType: 'oauth' }
85
- : resolvedAuthType === 'token' && tokenProvided
86
- ? { authType: 'token', accessToken: tokenFromFlags }
87
- : await runPromptCatalog(envAuthPrompts, {
88
- values: {
89
- ...(resolvedAuthType ? { authType: resolvedAuthType } : {}),
90
- },
91
- command: this,
92
- })) ?? {};
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
+ })) ?? {};
93
129
  const authType = resolveExplicitAuthType(prompted.authType ?? resolvedAuthType);
94
130
  if (!authType) {
95
131
  this.error('Choose an authentication type before continuing.');
96
132
  }
97
133
  printStage('Authenticating');
98
134
  try {
99
- if (authType === 'token') {
100
- const accessToken = String(prompted.accessToken ?? tokenFromFlags ?? '');
101
- if (accessToken.trim() === '') {
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 === '') {
102
161
  this.error('--access-token cannot be empty.');
103
162
  }
104
163
  startTask(`Saving access token for "${envName}"...`);
@@ -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') {