@nocobase/cli 2.1.0-alpha.24 → 2.1.0-alpha.26
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 +41 -49
- package/README.zh-CN.md +38 -45
- package/bin/run.js +15 -0
- package/dist/commands/app/down.js +260 -0
- package/dist/commands/app/logs.js +98 -0
- package/dist/commands/app/restart.js +75 -0
- package/dist/commands/app/start.js +252 -0
- package/dist/commands/app/stop.js +98 -0
- package/dist/commands/app/upgrade.js +595 -0
- package/dist/commands/build.js +3 -48
- package/dist/commands/db/shared.js +19 -5
- package/dist/commands/dev.js +3 -140
- package/dist/commands/down.js +3 -184
- package/dist/commands/download.js +4 -856
- package/dist/commands/env/add.js +33 -48
- package/dist/commands/env/auth.js +6 -13
- package/dist/commands/env/info.js +152 -0
- package/dist/commands/env/list.js +27 -18
- package/dist/commands/env/remove.js +4 -10
- package/dist/commands/env/shared.js +158 -0
- package/dist/commands/env/update.js +7 -13
- package/dist/commands/env/use.js +5 -13
- package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
- package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
- package/dist/commands/init.js +270 -64
- package/dist/commands/install.js +352 -86
- package/dist/commands/logs.js +3 -81
- package/dist/commands/plugin/disable.js +64 -0
- package/dist/commands/plugin/enable.js +64 -0
- package/dist/commands/plugin/list.js +62 -0
- package/dist/commands/pm/disable.js +3 -54
- package/dist/commands/pm/enable.js +3 -54
- package/dist/commands/pm/list.js +3 -45
- package/dist/commands/restart.js +12 -0
- package/dist/commands/scaffold/migration.js +1 -1
- package/dist/commands/scaffold/plugin.js +1 -1
- package/dist/commands/self/check.js +1 -1
- package/dist/commands/self/update.js +13 -3
- package/dist/commands/skills/check.js +11 -5
- package/dist/commands/skills/index.js +1 -1
- package/dist/commands/skills/install.js +20 -7
- package/dist/commands/skills/remove.js +71 -0
- package/dist/commands/skills/update.js +27 -7
- package/dist/commands/source/build.js +58 -0
- package/dist/commands/source/dev.js +157 -0
- package/dist/commands/source/download.js +866 -0
- package/dist/commands/source/test.js +467 -0
- package/dist/commands/start.js +3 -202
- package/dist/commands/stop.js +3 -81
- package/dist/commands/test.js +3 -457
- package/dist/commands/upgrade.js +3 -574
- package/dist/help/runtime-help.js +3 -0
- package/dist/lib/api-client.js +22 -7
- package/dist/lib/app-health.js +126 -0
- package/dist/lib/app-managed-resources.js +264 -0
- package/dist/lib/app-runtime.js +16 -5
- package/dist/lib/auth-store.js +162 -43
- package/dist/lib/bootstrap.js +13 -12
- package/dist/lib/cli-home.js +38 -6
- package/dist/lib/cli-locale.js +15 -1
- package/dist/lib/env-auth.js +3 -3
- package/dist/lib/env-config.js +80 -0
- package/dist/lib/generated-command.js +10 -2
- package/dist/lib/http-request.js +49 -0
- package/dist/lib/prompt-web-ui.js +13 -6
- package/dist/lib/resource-command.js +10 -2
- package/dist/lib/runtime-generator.js +1 -1
- package/dist/lib/self-manager.js +1 -1
- package/dist/lib/skills-manager.js +173 -79
- package/dist/lib/startup-update.js +203 -0
- package/dist/locale/en-US.json +4 -1
- package/dist/locale/zh-CN.json +4 -1
- package/package.json +27 -4
- package/dist/commands/ps.js +0 -116
package/dist/commands/env/add.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Args, Command, Flags } from '@oclif/core';
|
|
10
10
|
import { upsertEnv } from '../../lib/auth-store.js';
|
|
11
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
|
+
import { buildStoredEnvConfig, } from '../../lib/env-config.js';
|
|
11
13
|
import { runPromptCatalog, } from '../../lib/prompt-catalog.js';
|
|
12
14
|
import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from '../../lib/cli-locale.js';
|
|
13
15
|
import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
|
|
@@ -32,6 +34,10 @@ const ENV_RUNTIME_FLAG_MAP = {
|
|
|
32
34
|
'db-database': 'dbDatabase',
|
|
33
35
|
'db-user': 'dbUser',
|
|
34
36
|
'db-password': 'dbPassword',
|
|
37
|
+
'root-username': 'rootUsername',
|
|
38
|
+
'root-email': 'rootEmail',
|
|
39
|
+
'root-password': 'rootPassword',
|
|
40
|
+
'root-nickname': 'rootNickname',
|
|
35
41
|
};
|
|
36
42
|
const ENV_BOOLEAN_RUNTIME_FLAG_MAP = {
|
|
37
43
|
'builtin-db': 'builtinDb',
|
|
@@ -45,7 +51,7 @@ export default class EnvAdd extends Command {
|
|
|
45
51
|
static examples = [
|
|
46
52
|
'<%= config.bin %> <%= command.id %>',
|
|
47
53
|
'<%= config.bin %> <%= command.id %> local',
|
|
48
|
-
'<%= config.bin %> <%= command.id %> local --
|
|
54
|
+
'<%= config.bin %> <%= command.id %> local --api-base-url http://localhost:13000/api --auth-type oauth',
|
|
49
55
|
];
|
|
50
56
|
static args = {
|
|
51
57
|
name: Args.string({
|
|
@@ -73,12 +79,6 @@ export default class EnvAdd extends Command {
|
|
|
73
79
|
description: 'Skip command intro when invoked by another CLI command',
|
|
74
80
|
default: false,
|
|
75
81
|
}),
|
|
76
|
-
scope: Flags.string({
|
|
77
|
-
char: 's',
|
|
78
|
-
description: 'Where to store env config: project (.nocobase in the repo) or global (user-level); prompted in a TTY when omitted',
|
|
79
|
-
options: ['project', 'global'],
|
|
80
|
-
default: 'project',
|
|
81
|
-
}),
|
|
82
82
|
'default-api-base-url': Flags.string({
|
|
83
83
|
char: 'd',
|
|
84
84
|
hidden: true,
|
|
@@ -86,7 +86,6 @@ export default class EnvAdd extends Command {
|
|
|
86
86
|
}),
|
|
87
87
|
'api-base-url': Flags.string({
|
|
88
88
|
char: 'u',
|
|
89
|
-
aliases: ['base-url'],
|
|
90
89
|
description: 'Root URL for HTTP API calls, including the /api prefix (e.g. http://localhost:13000/api); prompted in a TTY when omitted',
|
|
91
90
|
}),
|
|
92
91
|
'auth-type': Flags.string({
|
|
@@ -191,6 +190,22 @@ export default class EnvAdd extends Command {
|
|
|
191
190
|
hidden: true,
|
|
192
191
|
description: 'Database password saved with this env',
|
|
193
192
|
}),
|
|
193
|
+
'root-username': Flags.string({
|
|
194
|
+
hidden: true,
|
|
195
|
+
description: 'Initial root username saved with this env',
|
|
196
|
+
}),
|
|
197
|
+
'root-email': Flags.string({
|
|
198
|
+
hidden: true,
|
|
199
|
+
description: 'Initial root email saved with this env',
|
|
200
|
+
}),
|
|
201
|
+
'root-password': Flags.string({
|
|
202
|
+
hidden: true,
|
|
203
|
+
description: 'Initial root password saved with this env',
|
|
204
|
+
}),
|
|
205
|
+
'root-nickname': Flags.string({
|
|
206
|
+
hidden: true,
|
|
207
|
+
description: 'Initial root nickname saved with this env',
|
|
208
|
+
}),
|
|
194
209
|
};
|
|
195
210
|
static prompts = {
|
|
196
211
|
name: {
|
|
@@ -199,24 +214,6 @@ export default class EnvAdd extends Command {
|
|
|
199
214
|
placeholder: envAddText('prompts.name.placeholder'),
|
|
200
215
|
required: true,
|
|
201
216
|
},
|
|
202
|
-
scope: {
|
|
203
|
-
type: 'select',
|
|
204
|
-
message: envAddText('prompts.scope.message'),
|
|
205
|
-
options: [
|
|
206
|
-
{
|
|
207
|
-
value: 'project',
|
|
208
|
-
label: envAddText('prompts.scope.projectLabel'),
|
|
209
|
-
hint: envAddText('prompts.scope.projectHint'),
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
value: 'global',
|
|
213
|
-
label: envAddText('prompts.scope.globalLabel'),
|
|
214
|
-
hint: envAddText('prompts.scope.globalHint'),
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
initialValue: 'project',
|
|
218
|
-
required: true,
|
|
219
|
-
},
|
|
220
217
|
apiBaseUrl: {
|
|
221
218
|
type: 'text',
|
|
222
219
|
message: envAddText('prompts.apiBaseUrl.message'),
|
|
@@ -252,10 +249,7 @@ export default class EnvAdd extends Command {
|
|
|
252
249
|
if (name) {
|
|
253
250
|
values.name = name;
|
|
254
251
|
}
|
|
255
|
-
|
|
256
|
-
values.scope = flags.scope;
|
|
257
|
-
}
|
|
258
|
-
const apiFromFlag = flags['api-base-url'] ?? flags['base-url'];
|
|
252
|
+
const apiFromFlag = flags['api-base-url'];
|
|
259
253
|
if (typeof apiFromFlag === 'string' && apiFromFlag.trim() !== '') {
|
|
260
254
|
values.apiBaseUrl = apiFromFlag.trim();
|
|
261
255
|
}
|
|
@@ -277,28 +271,20 @@ export default class EnvAdd extends Command {
|
|
|
277
271
|
return initialValues;
|
|
278
272
|
}
|
|
279
273
|
buildEnvConfig(results, flags) {
|
|
280
|
-
const
|
|
281
|
-
|
|
274
|
+
const envConfigInput = {
|
|
275
|
+
apiBaseUrl: results.apiBaseUrl,
|
|
276
|
+
authType: results.authType,
|
|
277
|
+
accessToken: results.accessToken,
|
|
282
278
|
};
|
|
283
279
|
for (const [flagName, configKey] of Object.entries(ENV_RUNTIME_FLAG_MAP)) {
|
|
284
280
|
const value = flags[flagName];
|
|
285
|
-
|
|
286
|
-
envConfig[configKey] = value.trim();
|
|
287
|
-
}
|
|
281
|
+
envConfigInput[configKey] = value;
|
|
288
282
|
}
|
|
289
283
|
for (const [flagName, configKey] of Object.entries(ENV_BOOLEAN_RUNTIME_FLAG_MAP)) {
|
|
290
284
|
const value = flags[flagName];
|
|
291
|
-
|
|
292
|
-
envConfig[configKey] = value;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (flags['builtin-db'] === false) {
|
|
296
|
-
envConfig.builtinDbImage = undefined;
|
|
297
|
-
}
|
|
298
|
-
if (results.authType === 'token' && results.accessToken != null) {
|
|
299
|
-
envConfig.accessToken = String(results.accessToken);
|
|
285
|
+
envConfigInput[configKey] = value;
|
|
300
286
|
}
|
|
301
|
-
return
|
|
287
|
+
return buildStoredEnvConfig(envConfigInput);
|
|
302
288
|
}
|
|
303
289
|
async run() {
|
|
304
290
|
const { args, flags } = await this.parse(EnvAdd);
|
|
@@ -314,10 +300,9 @@ export default class EnvAdd extends Command {
|
|
|
314
300
|
command: this,
|
|
315
301
|
});
|
|
316
302
|
const envName = String(results.name);
|
|
317
|
-
const scope = results.scope;
|
|
318
303
|
const envConfig = this.buildEnvConfig(results, parsedFlags);
|
|
319
|
-
printVerbose(`Saving env "${envName}"
|
|
320
|
-
await upsertEnv(envName, envConfig, { scope });
|
|
304
|
+
printVerbose(`Saving env "${envName}" globally.`);
|
|
305
|
+
await upsertEnv(envName, envConfig, { scope: resolveDefaultConfigScope() });
|
|
321
306
|
if (results.authType === 'oauth') {
|
|
322
307
|
await this.config.runCommand('env:auth', [envName]);
|
|
323
308
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Args, Command, Flags } from '@oclif/core';
|
|
10
10
|
import { getCurrentEnvName } from '../../lib/auth-store.js';
|
|
11
|
-
import {
|
|
11
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
12
|
import { authenticateEnvWithOauth } from '../../lib/env-auth.js';
|
|
13
13
|
import { failTask, startTask, succeedTask } from '../../lib/ui.js';
|
|
14
14
|
export default class EnvAuth extends Command {
|
|
@@ -16,7 +16,6 @@ export default class EnvAuth extends Command {
|
|
|
16
16
|
static examples = [
|
|
17
17
|
'<%= config.bin %> <%= command.id %>',
|
|
18
18
|
'<%= config.bin %> <%= command.id %> prod',
|
|
19
|
-
'<%= config.bin %> <%= command.id %> --scope global',
|
|
20
19
|
];
|
|
21
20
|
static args = {
|
|
22
21
|
name: Args.string({
|
|
@@ -31,31 +30,25 @@ export default class EnvAuth extends Command {
|
|
|
31
30
|
deprecated: true,
|
|
32
31
|
description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
|
|
33
32
|
}),
|
|
34
|
-
scope: Flags.string({
|
|
35
|
-
char: 's',
|
|
36
|
-
description: 'Config scope',
|
|
37
|
-
options: ['project', 'global'],
|
|
38
|
-
}),
|
|
39
33
|
};
|
|
40
34
|
async run() {
|
|
41
35
|
const { args, flags } = await this.parse(EnvAuth);
|
|
42
|
-
const scope = flags.scope;
|
|
43
36
|
const nameArg = args.name?.trim();
|
|
44
37
|
const nameFlag = flags.env?.trim() || undefined;
|
|
45
38
|
if (nameArg && nameFlag && nameArg !== nameFlag) {
|
|
46
39
|
this.error(`Environment name was provided both as the argument ("${nameArg}") and as --env ("${nameFlag}"). Please use only one.`);
|
|
47
40
|
}
|
|
48
|
-
const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope }));
|
|
49
|
-
startTask(`Starting browser sign-in for "${envName}"
|
|
41
|
+
const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
|
|
42
|
+
startTask(`Starting browser sign-in for "${envName}"...`);
|
|
50
43
|
try {
|
|
51
44
|
await authenticateEnvWithOauth({
|
|
52
45
|
envName,
|
|
53
|
-
scope,
|
|
46
|
+
scope: resolveDefaultConfigScope(),
|
|
54
47
|
});
|
|
55
|
-
succeedTask(`Signed in to "${envName}"
|
|
48
|
+
succeedTask(`Signed in to "${envName}".`);
|
|
56
49
|
}
|
|
57
50
|
catch (error) {
|
|
58
|
-
failTask(`Sign-in failed for "${envName}"
|
|
51
|
+
failTask(`Sign-in failed for "${envName}".`);
|
|
59
52
|
throw error;
|
|
60
53
|
}
|
|
61
54
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
10
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
11
|
+
import { renderTable } from '../../lib/ui.js';
|
|
12
|
+
import { appRootPath, dbStatus, runtimeStatus, storagePath } from './shared.js';
|
|
13
|
+
function normalizeJsonValue(value) {
|
|
14
|
+
if (value === undefined || value === null || value === '') {
|
|
15
|
+
return '-';
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
function normalizeValue(value) {
|
|
23
|
+
if (value === undefined || value === null || value === '') {
|
|
24
|
+
return '-';
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'boolean') {
|
|
27
|
+
return value ? 'true' : 'false';
|
|
28
|
+
}
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
function maskSecret(value, showSecrets) {
|
|
32
|
+
const normalized = normalizeValue(value);
|
|
33
|
+
if (normalized === '-') {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
return showSecrets ? normalized : '********';
|
|
37
|
+
}
|
|
38
|
+
function createGroupTable(title, values) {
|
|
39
|
+
const rows = Object.entries(values).map(([field, value]) => [field, normalizeValue(value)]);
|
|
40
|
+
return `${title}\n${renderTable(['Field', 'Value'], rows)}`;
|
|
41
|
+
}
|
|
42
|
+
function serializeGroup(values) {
|
|
43
|
+
return Object.fromEntries(Object.entries(values).map(([field, value]) => [field, normalizeJsonValue(value)]));
|
|
44
|
+
}
|
|
45
|
+
export default class EnvInfo extends Command {
|
|
46
|
+
static hidden = false;
|
|
47
|
+
static description = 'Show grouped details for the selected NocoBase env, including app, database, API, and auth settings.';
|
|
48
|
+
static examples = [
|
|
49
|
+
'<%= config.bin %> <%= command.id %> app1',
|
|
50
|
+
'<%= config.bin %> <%= command.id %> app1 --json',
|
|
51
|
+
'<%= config.bin %> <%= command.id %> app1 --show-secrets',
|
|
52
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
53
|
+
];
|
|
54
|
+
static args = {
|
|
55
|
+
name: Args.string({
|
|
56
|
+
description: 'CLI env name to inspect. Defaults to the current env when omitted',
|
|
57
|
+
required: false,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
static flags = {
|
|
61
|
+
env: Flags.string({
|
|
62
|
+
char: 'e',
|
|
63
|
+
description: 'CLI env name to inspect. Defaults to the current env when omitted',
|
|
64
|
+
}),
|
|
65
|
+
json: Flags.boolean({
|
|
66
|
+
description: 'Output the result as JSON',
|
|
67
|
+
default: false,
|
|
68
|
+
}),
|
|
69
|
+
'show-secrets': Flags.boolean({
|
|
70
|
+
description: 'Show secret values in plain text',
|
|
71
|
+
default: false,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
async run() {
|
|
75
|
+
const { args, flags } = await this.parse(EnvInfo);
|
|
76
|
+
const envNameArg = args.name?.trim() || undefined;
|
|
77
|
+
const envNameFlag = flags.env?.trim() || undefined;
|
|
78
|
+
if (envNameArg && envNameFlag && envNameArg !== envNameFlag) {
|
|
79
|
+
this.error(`Environment name was provided both as the argument ("${envNameArg}") and as --env ("${envNameFlag}"). Please use only one.`);
|
|
80
|
+
}
|
|
81
|
+
const requestedEnv = envNameArg || envNameFlag;
|
|
82
|
+
const showSecrets = flags['show-secrets'];
|
|
83
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
84
|
+
if (!runtime) {
|
|
85
|
+
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
86
|
+
}
|
|
87
|
+
const auth = runtime.env.auth;
|
|
88
|
+
const appGroup = {
|
|
89
|
+
appRootPath: appRootPath(runtime),
|
|
90
|
+
storagePath: storagePath(runtime),
|
|
91
|
+
appPort: runtime.env.config.appPort,
|
|
92
|
+
appStatus: await runtimeStatus(runtime),
|
|
93
|
+
source: runtime.source,
|
|
94
|
+
downloadVersion: runtime.env.config.downloadVersion,
|
|
95
|
+
dockerRegistry: runtime.env.config.dockerRegistry,
|
|
96
|
+
dockerPlatform: runtime.env.config.dockerPlatform,
|
|
97
|
+
timezone: runtime.env.config.timezone,
|
|
98
|
+
};
|
|
99
|
+
const dbGroup = {
|
|
100
|
+
databaseStatus: await dbStatus(runtime),
|
|
101
|
+
builtinDb: runtime.env.config.builtinDb,
|
|
102
|
+
dbDialect: runtime.env.config.dbDialect,
|
|
103
|
+
builtinDbImage: runtime.env.config.builtinDbImage,
|
|
104
|
+
dbHost: runtime.env.config.dbHost,
|
|
105
|
+
dbPort: runtime.env.config.dbPort,
|
|
106
|
+
dbDatabase: runtime.env.config.dbDatabase,
|
|
107
|
+
dbUser: runtime.env.config.dbUser,
|
|
108
|
+
dbPassword: maskSecret(runtime.env.config.dbPassword, showSecrets),
|
|
109
|
+
};
|
|
110
|
+
const authGroup = {
|
|
111
|
+
type: auth?.type,
|
|
112
|
+
expiresAt: auth?.type === 'oauth' ? auth.expiresAt : undefined,
|
|
113
|
+
scope: auth?.type === 'oauth' ? auth.scope : undefined,
|
|
114
|
+
issuer: auth?.type === 'oauth' ? auth.issuer : undefined,
|
|
115
|
+
clientId: auth?.type === 'oauth' ? auth.clientId : undefined,
|
|
116
|
+
resource: auth?.type === 'oauth' ? auth.resource : undefined,
|
|
117
|
+
accessToken: maskSecret(auth?.accessToken, showSecrets),
|
|
118
|
+
refreshToken: maskSecret(auth?.type === 'oauth' ? auth.refreshToken : undefined, showSecrets),
|
|
119
|
+
};
|
|
120
|
+
const apiGroup = {
|
|
121
|
+
apiBaseUrl: runtime.env.apiBaseUrl,
|
|
122
|
+
'auth.type': authGroup.type,
|
|
123
|
+
'auth.expiresAt': authGroup.expiresAt,
|
|
124
|
+
'auth.scope': authGroup.scope,
|
|
125
|
+
'auth.issuer': authGroup.issuer,
|
|
126
|
+
'auth.clientId': authGroup.clientId,
|
|
127
|
+
'auth.resource': authGroup.resource,
|
|
128
|
+
'auth.accessToken': authGroup.accessToken,
|
|
129
|
+
'auth.refreshToken': authGroup.refreshToken,
|
|
130
|
+
};
|
|
131
|
+
const output = {
|
|
132
|
+
ok: true,
|
|
133
|
+
env: runtime.envName,
|
|
134
|
+
kind: runtime.kind,
|
|
135
|
+
app: serializeGroup(appGroup),
|
|
136
|
+
db: serializeGroup(dbGroup),
|
|
137
|
+
api: {
|
|
138
|
+
apiBaseUrl: normalizeJsonValue(runtime.env.apiBaseUrl),
|
|
139
|
+
auth: serializeGroup(authGroup),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
if (flags.json) {
|
|
143
|
+
this.log(JSON.stringify(output, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.log([
|
|
147
|
+
createGroupTable('App', appGroup),
|
|
148
|
+
createGroupTable('DB', dbGroup),
|
|
149
|
+
createGroupTable('API', apiGroup),
|
|
150
|
+
].join('\n\n'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -6,36 +6,45 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { Command
|
|
9
|
+
import { Command } from '@oclif/core';
|
|
10
|
+
import { resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
10
11
|
import { listEnvs } from '../../lib/auth-store.js';
|
|
11
|
-
import {
|
|
12
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
13
|
import { renderTable } from '../../lib/ui.js';
|
|
14
|
+
import { apiStatus, appUrl, resolveApiBaseUrl } from './shared.js';
|
|
13
15
|
export default class EnvList extends Command {
|
|
14
|
-
static summary = 'List configured environments';
|
|
16
|
+
static summary = 'List configured environments and API auth status';
|
|
15
17
|
static examples = [
|
|
16
18
|
'<%= config.bin %> <%= command.id %>',
|
|
17
19
|
];
|
|
18
|
-
static flags = {
|
|
19
|
-
scope: Flags.string({
|
|
20
|
-
char: 's',
|
|
21
|
-
description: 'Config scope',
|
|
22
|
-
options: ['project', 'global'],
|
|
23
|
-
}),
|
|
24
|
-
};
|
|
25
20
|
async run() {
|
|
26
|
-
|
|
27
|
-
const scope =
|
|
21
|
+
await this.parse(EnvList);
|
|
22
|
+
const scope = resolveDefaultConfigScope();
|
|
28
23
|
const { currentEnv, envs } = await listEnvs({ scope });
|
|
29
24
|
const names = Object.keys(envs).sort();
|
|
30
25
|
if (!names.length) {
|
|
31
|
-
this.log(
|
|
32
|
-
this.log('Run `nb env add <name> --base-url <url>` to add one.');
|
|
26
|
+
this.log('No envs configured.');
|
|
27
|
+
this.log('Run `nb env add <name> --api-base-url <url>` to add one.');
|
|
33
28
|
return;
|
|
34
29
|
}
|
|
35
|
-
const rows =
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (const name of names) {
|
|
36
32
|
const env = envs[name];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
const runtime = await resolveManagedAppRuntime(name);
|
|
34
|
+
const statusConfig = {
|
|
35
|
+
...env,
|
|
36
|
+
...(runtime?.env.config ?? {}),
|
|
37
|
+
};
|
|
38
|
+
rows.push([
|
|
39
|
+
name === currentEnv ? '*' : '',
|
|
40
|
+
name,
|
|
41
|
+
runtime?.kind ?? env.kind ?? '-',
|
|
42
|
+
await apiStatus(name, statusConfig, { scope }),
|
|
43
|
+
runtime ? appUrl(runtime) : resolveApiBaseUrl(env),
|
|
44
|
+
env.auth?.type ?? '',
|
|
45
|
+
env.runtime?.version ?? '',
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
this.log(renderTable(['Current', 'Name', 'Kind', 'App Status', 'URL', 'Auth', 'Runtime'], rows));
|
|
40
49
|
}
|
|
41
50
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Args, Command, Flags } from '@oclif/core';
|
|
10
10
|
import { getCurrentEnvName, removeEnv } from '../../lib/auth-store.js';
|
|
11
|
-
import {
|
|
11
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
12
|
import { confirmAction, isInteractiveTerminal, printVerbose, setVerboseMode } from '../../lib/ui.js';
|
|
13
13
|
export default class EnvRemove extends Command {
|
|
14
14
|
static summary = 'Remove a configured environment';
|
|
@@ -26,11 +26,6 @@ export default class EnvRemove extends Command {
|
|
|
26
26
|
description: 'Show detailed progress output',
|
|
27
27
|
default: false,
|
|
28
28
|
}),
|
|
29
|
-
scope: Flags.string({
|
|
30
|
-
char: 's',
|
|
31
|
-
description: 'Config scope',
|
|
32
|
-
options: ['project', 'global'],
|
|
33
|
-
}),
|
|
34
29
|
};
|
|
35
30
|
static args = {
|
|
36
31
|
name: Args.string({
|
|
@@ -41,8 +36,7 @@ export default class EnvRemove extends Command {
|
|
|
41
36
|
async run() {
|
|
42
37
|
const { args, flags } = await this.parse(EnvRemove);
|
|
43
38
|
setVerboseMode(flags.verbose);
|
|
44
|
-
const
|
|
45
|
-
const currentEnv = await getCurrentEnvName({ scope });
|
|
39
|
+
const currentEnv = await getCurrentEnvName({ scope: resolveDefaultConfigScope() });
|
|
46
40
|
if (args.name === currentEnv && !flags.force) {
|
|
47
41
|
if (!isInteractiveTerminal()) {
|
|
48
42
|
this.error('Refusing to remove the current env without confirmation. Re-run with `--force`.');
|
|
@@ -54,8 +48,8 @@ export default class EnvRemove extends Command {
|
|
|
54
48
|
}
|
|
55
49
|
}
|
|
56
50
|
printVerbose(`Removing env "${args.name}"`);
|
|
57
|
-
const result = await removeEnv(args.name, { scope });
|
|
58
|
-
this.log(`Removed env "${result.removed}"
|
|
51
|
+
const result = await removeEnv(args.name, { scope: resolveDefaultConfigScope() });
|
|
52
|
+
this.log(`Removed env "${result.removed}".`);
|
|
59
53
|
if (result.hasEnvs) {
|
|
60
54
|
this.log(`Current env: ${result.currentEnv}`);
|
|
61
55
|
return;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, } from '../../lib/app-runtime.js';
|
|
10
|
+
import { executeRawApiRequest } from '../../lib/api-client.js';
|
|
11
|
+
export function resolveApiBaseUrl(config) {
|
|
12
|
+
return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
|
|
13
|
+
}
|
|
14
|
+
export function appUrl(runtime) {
|
|
15
|
+
const port = String(runtime.env.config.appPort ?? '').trim();
|
|
16
|
+
if (port) {
|
|
17
|
+
return `http://127.0.0.1:${port}`;
|
|
18
|
+
}
|
|
19
|
+
const baseUrl = resolveApiBaseUrl(runtime.env.config);
|
|
20
|
+
return baseUrl.replace(/\/api\/?$/, '');
|
|
21
|
+
}
|
|
22
|
+
export function appRootPath(runtime) {
|
|
23
|
+
if (runtime.kind === 'http' || runtime.kind === 'docker') {
|
|
24
|
+
return '-';
|
|
25
|
+
}
|
|
26
|
+
if (runtime.kind === 'local') {
|
|
27
|
+
return String(runtime.projectRoot ?? runtime.env.appRootPath ?? '').trim() || '-';
|
|
28
|
+
}
|
|
29
|
+
return String(runtime.env.config.appRootPath ?? '').trim() || '-';
|
|
30
|
+
}
|
|
31
|
+
export function storagePath(runtime) {
|
|
32
|
+
if (runtime.kind === 'http') {
|
|
33
|
+
return '-';
|
|
34
|
+
}
|
|
35
|
+
const value = String(runtime.env.storagePath ?? runtime.env.config.storagePath ?? '').trim();
|
|
36
|
+
return value || '-';
|
|
37
|
+
}
|
|
38
|
+
function collectErrorCodes(value) {
|
|
39
|
+
if (!value || typeof value !== 'object') {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return value.flatMap((item) => collectErrorCodes(item));
|
|
44
|
+
}
|
|
45
|
+
const out = [];
|
|
46
|
+
const record = value;
|
|
47
|
+
if (typeof record.code === 'string') {
|
|
48
|
+
out.push(record.code);
|
|
49
|
+
}
|
|
50
|
+
for (const key of ['data', 'error', 'errors']) {
|
|
51
|
+
out.push(...collectErrorCodes(record[key]));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function isAuthFailureData(value) {
|
|
56
|
+
const codes = collectErrorCodes(value);
|
|
57
|
+
return codes.some((code) => ['EMPTY_TOKEN', 'INVALID_TOKEN', 'EXPIRED_TOKEN', 'BLOCKED_TOKEN', 'EXPIRED_SESSION', 'NOT_EXIST_USER'].includes(code));
|
|
58
|
+
}
|
|
59
|
+
function isNetworkFailure(error) {
|
|
60
|
+
if (!(error instanceof Error)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return (error.name === 'AbortError'
|
|
64
|
+
|| /fetch failed|network|timeout|timed out|abort|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i.test(error.message));
|
|
65
|
+
}
|
|
66
|
+
function isUnconfiguredFailure(error) {
|
|
67
|
+
return error instanceof Error && /missing (a )?base url|missing base URL/i.test(error.message);
|
|
68
|
+
}
|
|
69
|
+
function isAuthFailure(error) {
|
|
70
|
+
return (error instanceof Error
|
|
71
|
+
&& /EMPTY_TOKEN|INVALID_TOKEN|EXPIRED_TOKEN|BLOCKED_TOKEN|EXPIRED_SESSION|NOT_EXIST_USER|invalid_grant|sign in|signin|authentication failed/i.test(error.message));
|
|
72
|
+
}
|
|
73
|
+
export async function apiStatus(envName, config, options = {}) {
|
|
74
|
+
if (!resolveApiBaseUrl(config)) {
|
|
75
|
+
return 'unconfigured';
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const response = await executeRawApiRequest({
|
|
79
|
+
envName,
|
|
80
|
+
scope: options.scope,
|
|
81
|
+
method: 'GET',
|
|
82
|
+
path: '/auth:check',
|
|
83
|
+
timeoutMs: options.timeoutMs ?? 2000,
|
|
84
|
+
});
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
return 'ok';
|
|
87
|
+
}
|
|
88
|
+
if (response.status === 401 || response.status === 403 || isAuthFailureData(response.data)) {
|
|
89
|
+
return 'auth failed';
|
|
90
|
+
}
|
|
91
|
+
return 'error';
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (isUnconfiguredFailure(error)) {
|
|
95
|
+
return 'unconfigured';
|
|
96
|
+
}
|
|
97
|
+
if (isAuthFailure(error)) {
|
|
98
|
+
return 'auth failed';
|
|
99
|
+
}
|
|
100
|
+
if (isNetworkFailure(error)) {
|
|
101
|
+
return 'unreachable';
|
|
102
|
+
}
|
|
103
|
+
return 'error';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function isLocalAppHealthy(runtime) {
|
|
107
|
+
const port = String(runtime.env.config.appPort ?? '').trim();
|
|
108
|
+
if (!port) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/__health_check`, {
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
return response.ok && text.trim().toLowerCase() === 'ok';
|
|
119
|
+
}
|
|
120
|
+
catch (_error) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function dockerStatus(containerName) {
|
|
128
|
+
if (!(await dockerContainerExists(containerName))) {
|
|
129
|
+
return 'missing';
|
|
130
|
+
}
|
|
131
|
+
return await dockerContainerIsRunning(containerName) ? 'running' : 'stopped';
|
|
132
|
+
}
|
|
133
|
+
export async function dbStatus(runtime) {
|
|
134
|
+
if (!runtime.env.config.builtinDb) {
|
|
135
|
+
return runtime.kind === 'http' ? 'external' : '-';
|
|
136
|
+
}
|
|
137
|
+
if (runtime.kind === 'http') {
|
|
138
|
+
return 'external';
|
|
139
|
+
}
|
|
140
|
+
if (runtime.kind === 'ssh') {
|
|
141
|
+
return '-';
|
|
142
|
+
}
|
|
143
|
+
const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
144
|
+
const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.workspaceName);
|
|
145
|
+
return await dockerStatus(containerName);
|
|
146
|
+
}
|
|
147
|
+
export async function runtimeStatus(runtime) {
|
|
148
|
+
if (runtime.kind === 'http') {
|
|
149
|
+
return 'http';
|
|
150
|
+
}
|
|
151
|
+
if (runtime.kind === 'ssh') {
|
|
152
|
+
return 'ssh';
|
|
153
|
+
}
|
|
154
|
+
if (runtime.kind === 'docker') {
|
|
155
|
+
return await dockerStatus(runtime.containerName);
|
|
156
|
+
}
|
|
157
|
+
return await isLocalAppHealthy(runtime) ? 'running' : 'stopped';
|
|
158
|
+
}
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { Args, Command, Flags } from '@oclif/core';
|
|
12
12
|
import { getCurrentEnvName } from '../../lib/auth-store.js';
|
|
13
13
|
import { updateEnvRuntime } from '../../lib/bootstrap.js';
|
|
14
|
-
import {
|
|
14
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
15
15
|
import { failTask, startTask, succeedTask } from '../../lib/ui.js';
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
export default class EnvUpdate extends Command {
|
|
@@ -31,12 +31,7 @@ export default class EnvUpdate extends Command {
|
|
|
31
31
|
description: 'Show detailed progress output',
|
|
32
32
|
default: false,
|
|
33
33
|
}),
|
|
34
|
-
|
|
35
|
-
char: 's',
|
|
36
|
-
description: 'Config scope',
|
|
37
|
-
options: ['project', 'global'],
|
|
38
|
-
}),
|
|
39
|
-
'base-url': Flags.string({
|
|
34
|
+
'api-base-url': Flags.string({
|
|
40
35
|
description: 'NocoBase API base URL override. When provided, persist it to the target env before saving the refreshed runtime.',
|
|
41
36
|
}),
|
|
42
37
|
role: Flags.string({
|
|
@@ -49,21 +44,20 @@ export default class EnvUpdate extends Command {
|
|
|
49
44
|
};
|
|
50
45
|
async run() {
|
|
51
46
|
const { args, flags } = await this.parse(EnvUpdate);
|
|
52
|
-
const scope = flags.scope;
|
|
53
47
|
const envName = args.name;
|
|
54
|
-
const envLabel = envName ?? (await getCurrentEnvName({ scope }));
|
|
55
|
-
startTask(`Updating env runtime: ${envLabel}
|
|
48
|
+
const envLabel = envName ?? (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
|
|
49
|
+
startTask(`Updating env runtime: ${envLabel}`);
|
|
56
50
|
try {
|
|
57
51
|
const runtime = await updateEnvRuntime({
|
|
58
52
|
envName,
|
|
59
|
-
scope,
|
|
60
|
-
baseUrl: flags['base-url'],
|
|
53
|
+
scope: resolveDefaultConfigScope(),
|
|
54
|
+
baseUrl: flags['api-base-url'],
|
|
61
55
|
role: flags.role,
|
|
62
56
|
token: flags.token,
|
|
63
57
|
configFile: path.join(path.dirname(path.dirname(path.dirname(__dirname))), 'nocobase-ctl.config.json'),
|
|
64
58
|
verbose: flags.verbose,
|
|
65
59
|
});
|
|
66
|
-
succeedTask(`Updated env "${envLabel}" to runtime "${runtime.version}"
|
|
60
|
+
succeedTask(`Updated env "${envLabel}" to runtime "${runtime.version}".`);
|
|
67
61
|
}
|
|
68
62
|
catch (error) {
|
|
69
63
|
failTask(`Failed to update env "${envLabel}".`);
|