@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/run.js +3 -2
- package/dist/commands/app/upgrade.js +38 -16
- package/dist/commands/backup/create.js +147 -0
- package/dist/commands/backup/index.js +20 -0
- package/dist/commands/backup/restore.js +105 -0
- package/dist/commands/config/delete.js +4 -0
- package/dist/commands/config/get.js +4 -0
- package/dist/commands/config/set.js +5 -1
- package/dist/commands/env/add.js +129 -15
- package/dist/commands/env/auth.js +145 -12
- package/dist/commands/env/info.js +52 -8
- package/dist/commands/env/list.js +2 -2
- package/dist/commands/env/shared.js +41 -3
- package/dist/commands/init.js +254 -136
- package/dist/commands/install.js +447 -272
- package/dist/commands/license/activate.js +6 -4
- package/dist/commands/source/publish.js +17 -0
- package/dist/commands/v1.js +210 -0
- package/dist/lib/app-managed-resources.js +20 -1
- package/dist/lib/app-runtime.js +13 -4
- package/dist/lib/auth-store.js +69 -18
- package/dist/lib/backup.js +171 -0
- package/dist/lib/bootstrap.js +23 -13
- package/dist/lib/cli-config.js +99 -4
- package/dist/lib/cli-locale.js +19 -7
- package/dist/lib/db-connection-check.js +61 -0
- package/dist/lib/env-auth.js +79 -0
- package/dist/lib/env-config.js +8 -1
- package/dist/lib/prompt-validators.js +23 -5
- package/dist/lib/prompt-web-ui.js +143 -19
- package/dist/lib/run-npm.js +166 -30
- package/dist/lib/skills-manager.js +74 -4
- package/dist/lib/source-publish.js +20 -1
- package/dist/lib/source-registry.js +2 -2
- package/dist/locale/en-US.json +36 -5
- package/dist/locale/zh-CN.json +36 -5
- package/package.json +6 -3
package/dist/commands/env/add.js
CHANGED
|
@@ -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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
307
|
-
|
|
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 {
|
|
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 = '
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(`
|
|
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
|
|
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
|
-
|
|
20
|
-
return baseUrl.replace(/\/api\/?$/, '');
|
|
58
|
+
return '';
|
|
21
59
|
}
|
|
22
60
|
export function appRootPath(runtime) {
|
|
23
61
|
if (runtime.kind === 'http' || runtime.kind === 'docker') {
|