@nocobase/cli 2.1.0-beta.22 → 2.1.0-beta.24
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 +16 -5
- package/README.zh-CN.md +14 -4
- package/dist/commands/db/check.js +132 -0
- package/dist/commands/{app → env}/info.js +19 -7
- package/dist/commands/env/list.js +23 -9
- package/dist/commands/{app → env}/shared.js +70 -7
- package/dist/commands/init.js +8 -2
- package/dist/commands/install.js +37 -0
- package/dist/lib/api-client.js +20 -6
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/prompt-web-ui.js +13 -6
- package/dist/lib/startup-update.js +4 -1
- package/dist/locale/en-US.json +16 -9
- package/dist/locale/zh-CN.json +16 -9
- package/package.json +7 -4
- package/dist/commands/app/ps.js +0 -60
- package/dist/commands/ps.js +0 -12
package/README.md
CHANGED
|
@@ -60,12 +60,16 @@ nb init --ui
|
|
|
60
60
|
When creating a new app, it can also install NocoBase AI coding skills
|
|
61
61
|
(`nocobase/skills`) globally.
|
|
62
62
|
|
|
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.
|
|
65
|
+
|
|
63
66
|
### Non-Interactive Setup
|
|
64
67
|
|
|
65
68
|
When prompts are skipped, an app/env name is required:
|
|
66
69
|
|
|
67
70
|
```bash
|
|
68
71
|
nb init --env app1 --yes
|
|
72
|
+
nb init --env app1 --yes --skip-skills
|
|
69
73
|
```
|
|
70
74
|
|
|
71
75
|
Install with Docker:
|
|
@@ -111,11 +115,12 @@ If `nb init` was interrupted after the env config had already been saved, you ca
|
|
|
111
115
|
|
|
112
116
|
```bash
|
|
113
117
|
nb init --env app1 --resume
|
|
118
|
+
nb init --env app1 --resume --skip-skills
|
|
114
119
|
```
|
|
115
120
|
|
|
116
121
|
`--resume` reuses the saved workspace env config for app, source, database, and env connection settings. In interactive mode, it only asks for any missing setup-only values.
|
|
117
122
|
|
|
118
|
-
In non-interactive mode,
|
|
123
|
+
In non-interactive resume mode, `nb init --resume --yes` uses default initialization values unless these flags are passed explicitly:
|
|
119
124
|
|
|
120
125
|
- `--lang`
|
|
121
126
|
- `--root-username`
|
|
@@ -128,7 +133,7 @@ In non-interactive mode, pass these setup-only flags again because they are not
|
|
|
128
133
|
| Command | Description |
|
|
129
134
|
| --- | --- |
|
|
130
135
|
| `nb init` | Set up NocoBase and connect it as a CLI env for coding agents. |
|
|
131
|
-
| `nb app` | Manage app runtimes: start, stop, restart, logs,
|
|
136
|
+
| `nb app` | Manage app runtimes: start, stop, restart, logs, cleanup, and upgrades. |
|
|
132
137
|
| `nb source` | Manage the local source project: download, develop, build, and test. |
|
|
133
138
|
| `nb db` | Inspect or manage built-in database runtime status for local envs. |
|
|
134
139
|
| `nb env` | Manage saved CLI env connections. |
|
|
@@ -137,13 +142,13 @@ In non-interactive mode, pass these setup-only flags again because they are not
|
|
|
137
142
|
| `nb self` | Check or update the installed NocoBase CLI. |
|
|
138
143
|
| `nb skills` | Check, install, or update global NocoBase AI coding skills. |
|
|
139
144
|
|
|
140
|
-
Recommended style:
|
|
145
|
+
Recommended style: pass the env name explicitly when operating on a specific env. Runtime commands accept `--env`, and `nb env info` also accepts a positional env name:
|
|
141
146
|
|
|
142
147
|
```bash
|
|
143
148
|
nb app start --env app1
|
|
144
149
|
nb app restart --env app1
|
|
145
150
|
nb app logs --env app1
|
|
146
|
-
nb
|
|
151
|
+
nb env info app1
|
|
147
152
|
nb db ps --env app1
|
|
148
153
|
```
|
|
149
154
|
|
|
@@ -291,12 +296,18 @@ Show the current env:
|
|
|
291
296
|
nb env
|
|
292
297
|
```
|
|
293
298
|
|
|
294
|
-
List configured envs:
|
|
299
|
+
List configured envs with token-verified API status:
|
|
295
300
|
|
|
296
301
|
```bash
|
|
297
302
|
nb env list
|
|
298
303
|
```
|
|
299
304
|
|
|
305
|
+
Show details for one env:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
nb env info app1
|
|
309
|
+
```
|
|
310
|
+
|
|
300
311
|
Switch the current env:
|
|
301
312
|
|
|
302
313
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -55,12 +55,15 @@ 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`。
|
|
59
|
+
|
|
58
60
|
### 非交互式初始化
|
|
59
61
|
|
|
60
62
|
跳过交互提示时,必须提供 app/env name:
|
|
61
63
|
|
|
62
64
|
```bash
|
|
63
65
|
nb init --env app1 --yes
|
|
66
|
+
nb init --env app1 --yes --skip-skills
|
|
64
67
|
```
|
|
65
68
|
|
|
66
69
|
使用 Docker 安装:
|
|
@@ -106,11 +109,12 @@ nb init --env app1 --yes --source git --version fix/cli-v2
|
|
|
106
109
|
|
|
107
110
|
```bash
|
|
108
111
|
nb init --env app1 --resume
|
|
112
|
+
nb init --env app1 --resume --skip-skills
|
|
109
113
|
```
|
|
110
114
|
|
|
111
115
|
`--resume` 会复用工作区里已保存的 env config,包括应用、source、数据库和 env 连接相关配置。在交互模式下,只会继续补齐缺失的初始化参数。
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
在非交互恢复模式下,如果没有显式传入这些参数,`nb init --resume --yes` 会使用默认初始化值:
|
|
114
118
|
|
|
115
119
|
- `--lang`
|
|
116
120
|
- `--root-username`
|
|
@@ -130,13 +134,13 @@ nb init --env app1 --resume
|
|
|
130
134
|
| `nb api` | 通过 CLI 调用 NocoBase API 资源。 |
|
|
131
135
|
| `nb plugin` | 管理选中 NocoBase env 的插件。 |
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
推荐在操作指定 env 时显式传入 env 名称。运行时命令支持 `--env`,`nb env info` 也支持位置参数:
|
|
134
138
|
|
|
135
139
|
```bash
|
|
136
140
|
nb app start --env app1
|
|
137
141
|
nb app restart --env app1
|
|
138
142
|
nb app logs --env app1
|
|
139
|
-
nb
|
|
143
|
+
nb env info app1
|
|
140
144
|
nb db ps --env app1
|
|
141
145
|
```
|
|
142
146
|
|
|
@@ -252,12 +256,18 @@ nb app down --env app1 --all --yes
|
|
|
252
256
|
nb env
|
|
253
257
|
```
|
|
254
258
|
|
|
255
|
-
查看已配置的 env
|
|
259
|
+
查看已配置的 env 及 Token 验证后的 API 状态:
|
|
256
260
|
|
|
257
261
|
```bash
|
|
258
262
|
nb env list
|
|
259
263
|
```
|
|
260
264
|
|
|
265
|
+
查看某个 env 的详情:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
nb env info app1
|
|
269
|
+
```
|
|
270
|
+
|
|
261
271
|
切换当前 env:
|
|
262
272
|
|
|
263
273
|
```bash
|
|
@@ -0,0 +1,132 @@
|
|
|
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 { Command, Flags } from '@oclif/core';
|
|
10
|
+
import { formatMissingManagedAppEnvMessage } from '../../lib/app-runtime.js';
|
|
11
|
+
import { getEnv } from '../../lib/auth-store.js';
|
|
12
|
+
import { checkExternalDbConnection, formatDbCheckAddress, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
|
|
13
|
+
import { validateTcpPort } from "../../lib/prompt-validators.js";
|
|
14
|
+
function trimValue(value) {
|
|
15
|
+
const text = String(value ?? '').trim();
|
|
16
|
+
return text || undefined;
|
|
17
|
+
}
|
|
18
|
+
function resolveRequiredDbField(flagValue, envValue) {
|
|
19
|
+
return trimValue(flagValue) ?? trimValue(envValue);
|
|
20
|
+
}
|
|
21
|
+
function formatMissingFieldsMessage(missing) {
|
|
22
|
+
return [
|
|
23
|
+
'Missing database settings for connectivity check.',
|
|
24
|
+
`Required: ${missing.join(', ')}.`,
|
|
25
|
+
'Pass `--env <name>` to reuse a saved env, or provide all `--db-*` flags explicitly.',
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
28
|
+
export default class DbCheck extends Command {
|
|
29
|
+
static description = 'Check whether the current machine can connect to a database using saved env config or explicit --db-* flags.';
|
|
30
|
+
static examples = [
|
|
31
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
32
|
+
'<%= config.bin %> <%= command.id %> --db-dialect postgres --db-host 127.0.0.1 --db-port 5432 --db-database nocobase --db-user nocobase --db-password secret',
|
|
33
|
+
'<%= config.bin %> <%= command.id %> --env app1 --db-password new-secret --json',
|
|
34
|
+
];
|
|
35
|
+
static flags = {
|
|
36
|
+
env: Flags.string({
|
|
37
|
+
char: 'e',
|
|
38
|
+
description: 'CLI env name to read saved database settings from. Defaults to the current env when omitted.',
|
|
39
|
+
}),
|
|
40
|
+
'db-dialect': Flags.string({
|
|
41
|
+
description: 'Database dialect: postgres, kingbase, mysql, or mariadb.',
|
|
42
|
+
options: ['postgres', 'kingbase', 'mysql', 'mariadb'],
|
|
43
|
+
}),
|
|
44
|
+
'db-host': Flags.string({
|
|
45
|
+
description: 'Database host name or IP address.',
|
|
46
|
+
}),
|
|
47
|
+
'db-port': Flags.string({
|
|
48
|
+
description: 'Database TCP port.',
|
|
49
|
+
}),
|
|
50
|
+
'db-database': Flags.string({
|
|
51
|
+
description: 'Database name.',
|
|
52
|
+
}),
|
|
53
|
+
'db-user': Flags.string({
|
|
54
|
+
description: 'Database username.',
|
|
55
|
+
}),
|
|
56
|
+
'db-password': Flags.string({
|
|
57
|
+
description: 'Database password.',
|
|
58
|
+
}),
|
|
59
|
+
json: Flags.boolean({
|
|
60
|
+
description: 'Output the check result as JSON.',
|
|
61
|
+
default: false,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
async run() {
|
|
65
|
+
const { flags } = await this.parse(DbCheck);
|
|
66
|
+
const envName = flags.env?.trim() || undefined;
|
|
67
|
+
const env = envName || !flags['db-host'] ? await getEnv(envName) : undefined;
|
|
68
|
+
if (envName && !env) {
|
|
69
|
+
this.error(formatMissingManagedAppEnvMessage(envName));
|
|
70
|
+
}
|
|
71
|
+
const config = env?.config ?? {};
|
|
72
|
+
const dbConfig = {
|
|
73
|
+
builtinDb: false,
|
|
74
|
+
dbDialect: resolveRequiredDbField(flags['db-dialect'], config.dbDialect),
|
|
75
|
+
dbHost: resolveRequiredDbField(flags['db-host'], config.dbHost),
|
|
76
|
+
dbPort: resolveRequiredDbField(flags['db-port'], config.dbPort),
|
|
77
|
+
dbDatabase: resolveRequiredDbField(flags['db-database'], config.dbDatabase),
|
|
78
|
+
dbUser: resolveRequiredDbField(flags['db-user'], config.dbUser),
|
|
79
|
+
dbPassword: flags['db-password'] !== undefined
|
|
80
|
+
? String(flags['db-password'] ?? '')
|
|
81
|
+
: String(config.dbPassword ?? ''),
|
|
82
|
+
};
|
|
83
|
+
const missing = [];
|
|
84
|
+
if (!dbConfig.dbDialect) {
|
|
85
|
+
missing.push('--db-dialect');
|
|
86
|
+
}
|
|
87
|
+
if (!dbConfig.dbHost) {
|
|
88
|
+
missing.push('--db-host');
|
|
89
|
+
}
|
|
90
|
+
if (!dbConfig.dbPort) {
|
|
91
|
+
missing.push('--db-port');
|
|
92
|
+
}
|
|
93
|
+
if (!dbConfig.dbDatabase) {
|
|
94
|
+
missing.push('--db-database');
|
|
95
|
+
}
|
|
96
|
+
if (!dbConfig.dbUser) {
|
|
97
|
+
missing.push('--db-user');
|
|
98
|
+
}
|
|
99
|
+
if (!dbConfig.dbPassword) {
|
|
100
|
+
missing.push('--db-password');
|
|
101
|
+
}
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
this.error(formatMissingFieldsMessage(missing));
|
|
104
|
+
}
|
|
105
|
+
const portError = validateTcpPort(dbConfig.dbPort);
|
|
106
|
+
if (portError) {
|
|
107
|
+
this.error(portError);
|
|
108
|
+
}
|
|
109
|
+
const connectionConfig = readExternalDbConnectionConfig(dbConfig);
|
|
110
|
+
if (!connectionConfig) {
|
|
111
|
+
this.error('Unsupported or incomplete database settings for connectivity check.');
|
|
112
|
+
}
|
|
113
|
+
const address = formatDbCheckAddress(connectionConfig);
|
|
114
|
+
const validationError = await checkExternalDbConnection(connectionConfig);
|
|
115
|
+
if (flags.json) {
|
|
116
|
+
this.log(JSON.stringify({
|
|
117
|
+
ok: !validationError,
|
|
118
|
+
env: env?.name,
|
|
119
|
+
dialect: connectionConfig.dialect,
|
|
120
|
+
address,
|
|
121
|
+
error: validationError ?? null,
|
|
122
|
+
}, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (validationError) {
|
|
126
|
+
this.error(validationError);
|
|
127
|
+
}
|
|
128
|
+
this.log(env?.name
|
|
129
|
+
? `Database check passed for env "${env.name}" (${connectionConfig.dialect} ${address}).`
|
|
130
|
+
: `Database check passed (${connectionConfig.dialect} ${address}).`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
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, Flags } from '@oclif/core';
|
|
9
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
10
10
|
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
11
11
|
import { renderTable } from '../../lib/ui.js';
|
|
12
12
|
import { appRootPath, dbStatus, runtimeStatus, storagePath } from './shared.js';
|
|
@@ -42,14 +42,21 @@ function createGroupTable(title, values) {
|
|
|
42
42
|
function serializeGroup(values) {
|
|
43
43
|
return Object.fromEntries(Object.entries(values).map(([field, value]) => [field, normalizeJsonValue(value)]));
|
|
44
44
|
}
|
|
45
|
-
export default class
|
|
45
|
+
export default class EnvInfo extends Command {
|
|
46
46
|
static hidden = false;
|
|
47
|
-
static description = 'Show grouped details for the selected NocoBase
|
|
47
|
+
static description = 'Show grouped details for the selected NocoBase env, including app, database, API, and auth settings.';
|
|
48
48
|
static examples = [
|
|
49
|
+
'<%= config.bin %> <%= command.id %> app1',
|
|
50
|
+
'<%= config.bin %> <%= command.id %> app1 --json',
|
|
51
|
+
'<%= config.bin %> <%= command.id %> app1 --show-secrets',
|
|
49
52
|
'<%= config.bin %> <%= command.id %> --env app1',
|
|
50
|
-
'<%= config.bin %> <%= command.id %> --env app1 --json',
|
|
51
|
-
'<%= config.bin %> <%= command.id %> --env app1 --show-secrets',
|
|
52
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
|
+
};
|
|
53
60
|
static flags = {
|
|
54
61
|
env: Flags.string({
|
|
55
62
|
char: 'e',
|
|
@@ -65,8 +72,13 @@ export default class AppInfo extends Command {
|
|
|
65
72
|
}),
|
|
66
73
|
};
|
|
67
74
|
async run() {
|
|
68
|
-
const { flags } = await this.parse(
|
|
69
|
-
const
|
|
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;
|
|
70
82
|
const showSecrets = flags['show-secrets'];
|
|
71
83
|
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
72
84
|
if (!runtime) {
|
|
@@ -7,30 +7,44 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { Command } from '@oclif/core';
|
|
10
|
+
import { resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
10
11
|
import { listEnvs } from '../../lib/auth-store.js';
|
|
11
12
|
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
13
|
import { renderTable } from '../../lib/ui.js';
|
|
13
|
-
|
|
14
|
-
return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
|
|
15
|
-
}
|
|
14
|
+
import { apiStatus, appUrl, resolveApiBaseUrl } from './shared.js';
|
|
16
15
|
export default class EnvList extends Command {
|
|
17
|
-
static summary = 'List configured environments';
|
|
16
|
+
static summary = 'List configured environments and API auth status';
|
|
18
17
|
static examples = [
|
|
19
18
|
'<%= config.bin %> <%= command.id %>',
|
|
20
19
|
];
|
|
21
20
|
async run() {
|
|
22
21
|
await this.parse(EnvList);
|
|
23
|
-
const
|
|
22
|
+
const scope = resolveDefaultConfigScope();
|
|
23
|
+
const { currentEnv, envs } = await listEnvs({ scope });
|
|
24
24
|
const names = Object.keys(envs).sort();
|
|
25
25
|
if (!names.length) {
|
|
26
26
|
this.log('No envs configured.');
|
|
27
27
|
this.log('Run `nb env add <name> --api-base-url <url>` to add one.');
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
const rows =
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (const name of names) {
|
|
31
32
|
const env = envs[name];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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));
|
|
35
49
|
}
|
|
36
50
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
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 { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning,
|
|
9
|
+
import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, } from '../../lib/app-runtime.js';
|
|
10
|
+
import { executeRawApiRequest } from '../../lib/api-client.js';
|
|
10
11
|
export function resolveApiBaseUrl(config) {
|
|
11
12
|
return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
|
|
12
13
|
}
|
|
@@ -18,12 +19,6 @@ export function appUrl(runtime) {
|
|
|
18
19
|
const baseUrl = resolveApiBaseUrl(runtime.env.config);
|
|
19
20
|
return baseUrl.replace(/\/api\/?$/, '');
|
|
20
21
|
}
|
|
21
|
-
export function appNetwork(runtime) {
|
|
22
|
-
if (runtime.kind === 'docker') {
|
|
23
|
-
return runtime.workspaceName?.trim() || defaultWorkspaceName();
|
|
24
|
-
}
|
|
25
|
-
return '-';
|
|
26
|
-
}
|
|
27
22
|
export function appRootPath(runtime) {
|
|
28
23
|
if (runtime.kind === 'http' || runtime.kind === 'docker') {
|
|
29
24
|
return '-';
|
|
@@ -40,6 +35,74 @@ export function storagePath(runtime) {
|
|
|
40
35
|
const value = String(runtime.env.storagePath ?? runtime.env.config.storagePath ?? '').trim();
|
|
41
36
|
return value || '-';
|
|
42
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
|
+
}
|
|
43
106
|
async function isLocalAppHealthy(runtime) {
|
|
44
107
|
const port = String(runtime.env.config.appPort ?? '').trim();
|
|
45
108
|
if (!port) {
|
package/dist/commands/init.js
CHANGED
|
@@ -139,6 +139,12 @@ function logInitUiReady(command, url) {
|
|
|
139
139
|
function logInitUiBrowserOpenFallback() {
|
|
140
140
|
p.log.warn(translateCli('commands.init.messages.uiOpenBrowserFallback'));
|
|
141
141
|
}
|
|
142
|
+
function formatBrowserOpenError(error) {
|
|
143
|
+
if (error instanceof Error) {
|
|
144
|
+
return error.message;
|
|
145
|
+
}
|
|
146
|
+
return String(error);
|
|
147
|
+
}
|
|
142
148
|
export default class Init extends Command {
|
|
143
149
|
static summary = 'Set up NocoBase so coding agents can connect and work with it';
|
|
144
150
|
static description = `Set up NocoBase for coding agents in the current workspace.
|
|
@@ -297,7 +303,6 @@ Prompt modes:
|
|
|
297
303
|
}),
|
|
298
304
|
'skip-skills': Flags.boolean({
|
|
299
305
|
description: 'Skip installing or updating NocoBase AI coding skills during init',
|
|
300
|
-
hidden: true,
|
|
301
306
|
default: false,
|
|
302
307
|
}),
|
|
303
308
|
'ui-host': Flags.string({
|
|
@@ -405,8 +410,9 @@ Prompt modes:
|
|
|
405
410
|
onServerStart: ({ url }) => {
|
|
406
411
|
logInitUiReady(this, url);
|
|
407
412
|
},
|
|
408
|
-
onOpenBrowserError: (_url,
|
|
413
|
+
onOpenBrowserError: (_url, err) => {
|
|
409
414
|
logInitUiBrowserOpenFallback();
|
|
415
|
+
p.log.info(`Browser open error: ${formatBrowserOpenError(err)}`);
|
|
410
416
|
},
|
|
411
417
|
});
|
|
412
418
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runPromptCatalog, } from "../lib/prompt-catalog.js";
|
|
|
18
18
|
import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
|
|
19
19
|
import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
|
|
20
20
|
import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
|
|
21
|
+
import { validateExternalDbConfig } from "../lib/db-connection-check.js";
|
|
21
22
|
import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
|
|
22
23
|
import { run, runNocoBaseCommand } from '../lib/run-npm.js';
|
|
23
24
|
import { startTask, stopTask, updateTask } from '../lib/ui.js';
|
|
@@ -180,6 +181,16 @@ function validateBuiltinDbEnabled(value, values) {
|
|
|
180
181
|
}
|
|
181
182
|
return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
|
|
182
183
|
}
|
|
184
|
+
async function validateExternalDbPromptField(value, values) {
|
|
185
|
+
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
186
|
+
if (builtinDb) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return await validateExternalDbConfig(values);
|
|
193
|
+
}
|
|
183
194
|
function defaultInstallAppRootPath(envName) {
|
|
184
195
|
const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
|
|
185
196
|
return `./${name}/source/`;
|
|
@@ -447,6 +458,7 @@ export default class Install extends Command {
|
|
|
447
458
|
initialValue: (values) => defaultDbHostForBuiltinDb(values),
|
|
448
459
|
yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
|
|
449
460
|
required: true,
|
|
461
|
+
validate: validateExternalDbPromptField,
|
|
450
462
|
hidden: (values) => Boolean(values.builtinDb),
|
|
451
463
|
},
|
|
452
464
|
dbPort: {
|
|
@@ -464,6 +476,7 @@ export default class Install extends Command {
|
|
|
464
476
|
message: installText('prompts.dbDatabase.message'),
|
|
465
477
|
initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
|
|
466
478
|
required: true,
|
|
479
|
+
validate: validateExternalDbPromptField,
|
|
467
480
|
},
|
|
468
481
|
dbUser: {
|
|
469
482
|
type: 'text',
|
|
@@ -471,6 +484,7 @@ export default class Install extends Command {
|
|
|
471
484
|
initialValue: DEFAULT_INSTALL_DB_USER,
|
|
472
485
|
yesInitialValue: DEFAULT_INSTALL_DB_USER,
|
|
473
486
|
required: true,
|
|
487
|
+
validate: validateExternalDbPromptField,
|
|
474
488
|
},
|
|
475
489
|
dbPassword: {
|
|
476
490
|
type: 'password',
|
|
@@ -478,6 +492,7 @@ export default class Install extends Command {
|
|
|
478
492
|
initialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
479
493
|
yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
480
494
|
required: true,
|
|
495
|
+
validate: validateExternalDbPromptField,
|
|
481
496
|
},
|
|
482
497
|
};
|
|
483
498
|
static rootUserPrompts = {
|
|
@@ -741,6 +756,9 @@ export default class Install extends Command {
|
|
|
741
756
|
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
742
757
|
const source = String(values.source ?? '').trim();
|
|
743
758
|
if (!builtinDb || source === 'docker') {
|
|
759
|
+
if (!builtinDb) {
|
|
760
|
+
return await validateExternalDbConfig({ ...values, dbPort: value });
|
|
761
|
+
}
|
|
744
762
|
return undefined;
|
|
745
763
|
}
|
|
746
764
|
return await Install.validateResumeAwareTcpPort(value, values, 'db');
|
|
@@ -765,6 +783,23 @@ export default class Install extends Command {
|
|
|
765
783
|
});
|
|
766
784
|
return reusesManagedPort ? undefined : portError;
|
|
767
785
|
}
|
|
786
|
+
static async ensureExternalDbReadyForInstall(dbResults) {
|
|
787
|
+
const builtinDb = dbResults.builtinDb === undefined ? true : Boolean(dbResults.builtinDb);
|
|
788
|
+
if (builtinDb) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const dialect = String(dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
792
|
+
const host = String(dbResults.dbHost ?? '').trim();
|
|
793
|
+
const port = String(dbResults.dbPort ?? '').trim();
|
|
794
|
+
const database = String(dbResults.dbDatabase ?? '').trim();
|
|
795
|
+
const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
|
|
796
|
+
const target = database ? `${address}/${database}` : address;
|
|
797
|
+
p.log.step(`Checking external ${dialect} database: ${target}`);
|
|
798
|
+
const validationError = await validateExternalDbConfig(dbResults);
|
|
799
|
+
if (validationError) {
|
|
800
|
+
throw new Error(validationError);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
768
803
|
static async readResumePortValidationContext(values) {
|
|
769
804
|
if (!Boolean(values.resume)) {
|
|
770
805
|
return undefined;
|
|
@@ -2044,6 +2079,7 @@ export default class Install extends Command {
|
|
|
2044
2079
|
const workspaceName = usesDockerResources
|
|
2045
2080
|
? await Install.ensureWorkspaceName()
|
|
2046
2081
|
: undefined;
|
|
2082
|
+
await Install.ensureExternalDbReadyForInstall(dbResults);
|
|
2047
2083
|
if (!parsed.resume) {
|
|
2048
2084
|
await this.saveInstalledEnv({
|
|
2049
2085
|
envName,
|
|
@@ -2053,6 +2089,7 @@ export default class Install extends Command {
|
|
|
2053
2089
|
rootResults,
|
|
2054
2090
|
envAddResults,
|
|
2055
2091
|
});
|
|
2092
|
+
p.log.info(`Saved install config for env "${envName}"`);
|
|
2056
2093
|
}
|
|
2057
2094
|
let builtinDbPlan;
|
|
2058
2095
|
if (Boolean(dbResults.builtinDb)) {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -235,10 +235,24 @@ export async function executeRawApiRequest(options) {
|
|
|
235
235
|
}
|
|
236
236
|
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
237
237
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController() : undefined;
|
|
239
|
+
const timeout = controller
|
|
240
|
+
? setTimeout(() => {
|
|
241
|
+
controller.abort();
|
|
242
|
+
}, options.timeoutMs)
|
|
243
|
+
: undefined;
|
|
244
|
+
try {
|
|
245
|
+
const response = await fetchWithPreservedAuthRedirect(url.toString(), {
|
|
246
|
+
method: options.method.toUpperCase(),
|
|
247
|
+
headers,
|
|
248
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
249
|
+
signal: controller?.signal,
|
|
250
|
+
});
|
|
251
|
+
return parseResponse(response);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
if (timeout) {
|
|
255
|
+
clearTimeout(timeout);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
244
258
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
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 { translateCli } from "./cli-locale.js";
|
|
10
|
+
import { validateTcpPort } from "./prompt-validators.js";
|
|
11
|
+
const DB_CONNECTION_TIMEOUT_MS = 5_000;
|
|
12
|
+
const externalDbValidationCache = new Map();
|
|
13
|
+
function trimPromptValue(value) {
|
|
14
|
+
return String(value ?? '').trim();
|
|
15
|
+
}
|
|
16
|
+
export function readExternalDbConnectionConfig(values) {
|
|
17
|
+
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
18
|
+
if (builtinDb) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const dialect = trimPromptValue(values.dbDialect || 'postgres');
|
|
22
|
+
if (dialect !== 'postgres' && dialect !== 'kingbase' && dialect !== 'mysql' && dialect !== 'mariadb') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const host = trimPromptValue(values.dbHost);
|
|
26
|
+
const portText = trimPromptValue(values.dbPort);
|
|
27
|
+
const database = trimPromptValue(values.dbDatabase);
|
|
28
|
+
const user = trimPromptValue(values.dbUser);
|
|
29
|
+
const password = String(values.dbPassword ?? '');
|
|
30
|
+
if (!host || !portText || !database || !user || !password) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (validateTcpPort(portText)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
dialect,
|
|
38
|
+
host,
|
|
39
|
+
port: Number.parseInt(portText, 10),
|
|
40
|
+
database,
|
|
41
|
+
user,
|
|
42
|
+
password,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function formatDbCheckAddress(config) {
|
|
46
|
+
return `${config.host}:${config.port}/${config.database}`;
|
|
47
|
+
}
|
|
48
|
+
function buildValidationCacheKey(config) {
|
|
49
|
+
return JSON.stringify(config);
|
|
50
|
+
}
|
|
51
|
+
function formatDbConnectionError(config, error) {
|
|
52
|
+
const maybeError = error;
|
|
53
|
+
const code = String(maybeError?.code ?? '').trim().toUpperCase();
|
|
54
|
+
const errno = typeof maybeError?.errno === 'number' ? maybeError.errno : undefined;
|
|
55
|
+
const rawMessage = String(maybeError?.message || maybeError?.sqlMessage || error || '').trim();
|
|
56
|
+
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EHOSTUNREACH' || code === 'ECONNRESET') {
|
|
57
|
+
return translateCli('validators.dbConnection.unreachable', {
|
|
58
|
+
host: config.host,
|
|
59
|
+
port: config.port,
|
|
60
|
+
details: rawMessage,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (code === 'ETIMEDOUT') {
|
|
64
|
+
return translateCli('validators.dbConnection.timeout', {
|
|
65
|
+
host: config.host,
|
|
66
|
+
port: config.port,
|
|
67
|
+
seconds: Math.ceil(DB_CONNECTION_TIMEOUT_MS / 1000),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (code === '28P01' || code === '28000' || code === 'ER_ACCESS_DENIED_ERROR' || errno === 1045) {
|
|
71
|
+
return translateCli('validators.dbConnection.authenticationFailed', {
|
|
72
|
+
user: config.user,
|
|
73
|
+
database: config.database,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (code === '3D000' || code === 'ER_BAD_DB_ERROR' || errno === 1049) {
|
|
77
|
+
return translateCli('validators.dbConnection.databaseNotFound', {
|
|
78
|
+
database: config.database,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return translateCli('validators.dbConnection.connectionFailed', {
|
|
82
|
+
details: rawMessage || code || String(error),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function checkPostgresFamilyConnection(config) {
|
|
86
|
+
const { default: pg } = await import('pg');
|
|
87
|
+
const client = new pg.Client({
|
|
88
|
+
host: config.host,
|
|
89
|
+
port: config.port,
|
|
90
|
+
user: config.user,
|
|
91
|
+
password: config.password,
|
|
92
|
+
database: config.database,
|
|
93
|
+
connectionTimeoutMillis: DB_CONNECTION_TIMEOUT_MS,
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await client.connect();
|
|
97
|
+
await client.query('SELECT 1');
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await Promise.resolve(client.end()).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function checkMysqlConnection(config) {
|
|
104
|
+
const { default: mysql } = await import('mysql2/promise');
|
|
105
|
+
const connection = await mysql.createConnection({
|
|
106
|
+
host: config.host,
|
|
107
|
+
port: config.port,
|
|
108
|
+
user: config.user,
|
|
109
|
+
password: config.password,
|
|
110
|
+
database: config.database,
|
|
111
|
+
connectTimeout: DB_CONNECTION_TIMEOUT_MS,
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
await connection.query('SELECT 1');
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function checkMariaDbConnection(config) {
|
|
121
|
+
const { default: mariadb } = await import('mariadb');
|
|
122
|
+
const connection = await mariadb.createConnection({
|
|
123
|
+
host: config.host,
|
|
124
|
+
port: config.port,
|
|
125
|
+
user: config.user,
|
|
126
|
+
password: config.password,
|
|
127
|
+
database: config.database,
|
|
128
|
+
connectTimeout: DB_CONNECTION_TIMEOUT_MS,
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await connection.query('SELECT 1');
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function performExternalDbConnectionCheck(config) {
|
|
138
|
+
try {
|
|
139
|
+
switch (config.dialect) {
|
|
140
|
+
case 'postgres':
|
|
141
|
+
case 'kingbase': {
|
|
142
|
+
await checkPostgresFamilyConnection(config);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
case 'mysql': {
|
|
146
|
+
await checkMysqlConnection(config);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
case 'mariadb': {
|
|
150
|
+
await checkMariaDbConnection(config);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return formatDbConnectionError(config, error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function checkExternalDbConnection(config) {
|
|
160
|
+
const cacheKey = buildValidationCacheKey(config);
|
|
161
|
+
const cached = externalDbValidationCache.get(cacheKey);
|
|
162
|
+
if (cached) {
|
|
163
|
+
return await cached;
|
|
164
|
+
}
|
|
165
|
+
const pending = performExternalDbConnectionCheck(config);
|
|
166
|
+
externalDbValidationCache.set(cacheKey, pending);
|
|
167
|
+
return await pending;
|
|
168
|
+
}
|
|
169
|
+
export async function validateExternalDbConfig(values) {
|
|
170
|
+
const config = readExternalDbConnectionConfig(values);
|
|
171
|
+
if (!config) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return await checkExternalDbConnection(config);
|
|
175
|
+
}
|
|
176
|
+
export function clearExternalDbValidationCache() {
|
|
177
|
+
externalDbValidationCache.clear();
|
|
178
|
+
}
|
|
@@ -559,17 +559,23 @@ function readFormFromClientStrippingPwcMeta(o) {
|
|
|
559
559
|
const { [PWC_FORM_META_STEP]: _meta, ...rest } = o;
|
|
560
560
|
return rest;
|
|
561
561
|
}
|
|
562
|
-
function openUrlInDefaultBrowser(url) {
|
|
562
|
+
function openUrlInDefaultBrowser(url, onError) {
|
|
563
|
+
const reportError = (error) => {
|
|
564
|
+
onError?.(url, error);
|
|
565
|
+
};
|
|
563
566
|
const platform = process.platform;
|
|
567
|
+
let child;
|
|
564
568
|
if (platform === 'darwin') {
|
|
565
|
-
spawn('open', [url], { stdio: 'ignore', detached: true })
|
|
569
|
+
child = spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
566
570
|
}
|
|
567
571
|
else if (platform === 'win32') {
|
|
568
|
-
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true })
|
|
572
|
+
child = spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true });
|
|
569
573
|
}
|
|
570
574
|
else {
|
|
571
|
-
spawn('xdg-open', [url], { stdio: 'ignore', detached: true })
|
|
575
|
+
child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
572
576
|
}
|
|
577
|
+
child.once('error', reportError);
|
|
578
|
+
child.unref();
|
|
573
579
|
}
|
|
574
580
|
function closePromptWebUiServer(server, done) {
|
|
575
581
|
server.close(done);
|
|
@@ -2084,11 +2090,12 @@ function runPromptCatalogWebUIImpl(options) {
|
|
|
2084
2090
|
const port = addr.port;
|
|
2085
2091
|
const startUrl = `http://${host}:${port}/`;
|
|
2086
2092
|
options.onServerStart?.({ host, port, url: startUrl });
|
|
2093
|
+
const onOpenBrowserError = options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u));
|
|
2087
2094
|
try {
|
|
2088
|
-
openUrlInDefaultBrowser(startUrl);
|
|
2095
|
+
openUrlInDefaultBrowser(startUrl, onOpenBrowserError);
|
|
2089
2096
|
}
|
|
2090
2097
|
catch (e) {
|
|
2091
|
-
|
|
2098
|
+
onOpenBrowserError(startUrl, e);
|
|
2092
2099
|
}
|
|
2093
2100
|
timeoutId = setTimeout(() => rejectAndClose(new Error('Local UI timeout — close the tab and try again, or resubmit within the time limit.')), timeoutMs);
|
|
2094
2101
|
});
|
|
@@ -20,7 +20,10 @@ function getStateFile() {
|
|
|
20
20
|
return path.join(resolveCliHomeDir('global'), STARTUP_UPDATE_STATE_FILE);
|
|
21
21
|
}
|
|
22
22
|
function todayStamp(now = new Date()) {
|
|
23
|
-
|
|
23
|
+
const year = now.getFullYear();
|
|
24
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
25
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
26
|
+
return `${year}-${month}-${day}`;
|
|
24
27
|
}
|
|
25
28
|
function shouldSkipByArgv(argv) {
|
|
26
29
|
const tokens = argv.filter((token) => token && !token.startsWith('-'));
|
package/dist/locale/en-US.json
CHANGED
|
@@ -62,6 +62,13 @@
|
|
|
62
62
|
"allocateNotDockerPublished": "Failed to allocate an available TCP port that is not already published by Docker.",
|
|
63
63
|
"alreadyInUse": "Port {{port}} is already in use. Choose another port.",
|
|
64
64
|
"alreadyInUseByDocker": "Port {{port}} is already in use by a Docker container. Choose another port."
|
|
65
|
+
},
|
|
66
|
+
"dbConnection": {
|
|
67
|
+
"unreachable": "Can't reach the database at {{host}}:{{port}}. Check the host, port, and network connectivity. Details: {{details}}",
|
|
68
|
+
"timeout": "Timed out connecting to the database at {{host}}:{{port}} after about {{seconds}} seconds.",
|
|
69
|
+
"authenticationFailed": "Failed to sign in to database \"{{database}}\" with user \"{{user}}\". Check the username and password.",
|
|
70
|
+
"databaseNotFound": "Database \"{{database}}\" does not exist or is not accessible with the current connection settings.",
|
|
71
|
+
"connectionFailed": "Database connection check failed. Details: {{details}}"
|
|
65
72
|
}
|
|
66
73
|
},
|
|
67
74
|
"commands": {
|
|
@@ -273,7 +280,7 @@
|
|
|
273
280
|
"envExists": "Env \"{{envName}}\" already exists. Choose another env name."
|
|
274
281
|
},
|
|
275
282
|
"messages": {
|
|
276
|
-
"title": "Set Up
|
|
283
|
+
"title": "Set Up NocoBase for Coding Agents",
|
|
277
284
|
"appNameRequiredWhenSkipped": "Env name is required when prompts are skipped.",
|
|
278
285
|
"appNameEnvHelp": "Use `nb init --yes --env <envName>` to continue.",
|
|
279
286
|
"resumeEnvRequired": "Env name is required when resuming setup.",
|
|
@@ -303,24 +310,24 @@
|
|
|
303
310
|
}
|
|
304
311
|
},
|
|
305
312
|
"webUi": {
|
|
306
|
-
"pageTitle": "Set Up
|
|
307
|
-
"documentHeading": "Set Up
|
|
308
|
-
"documentHint": "Connect an existing NocoBase app, or install a new one
|
|
313
|
+
"pageTitle": "Set Up NocoBase for Coding Agents",
|
|
314
|
+
"documentHeading": "Set Up NocoBase for Coding Agents",
|
|
315
|
+
"documentHint": "Connect an existing NocoBase app, or install a new one, so coding agents can access and work with NocoBase.",
|
|
309
316
|
"gettingStarted": {
|
|
310
317
|
"title": "Getting started",
|
|
311
|
-
"description": "
|
|
318
|
+
"description": "Choose whether to connect an existing app or install a new one."
|
|
312
319
|
},
|
|
313
320
|
"connectExistingApp": {
|
|
314
321
|
"title": "Connect an existing app",
|
|
315
|
-
"description": "
|
|
322
|
+
"description": "Save your app connection."
|
|
316
323
|
},
|
|
317
324
|
"createNewApp": {
|
|
318
|
-
"title": "
|
|
319
|
-
"description": "Set
|
|
325
|
+
"title": "Install a new app",
|
|
326
|
+
"description": "Set app basics and install options."
|
|
320
327
|
},
|
|
321
328
|
"downloadAppFiles": {
|
|
322
329
|
"title": "Download app files",
|
|
323
|
-
"description": "Choose
|
|
330
|
+
"description": "Choose how to get the app files."
|
|
324
331
|
},
|
|
325
332
|
"configureDatabase": {
|
|
326
333
|
"title": "Configure the database",
|
package/dist/locale/zh-CN.json
CHANGED
|
@@ -62,6 +62,13 @@
|
|
|
62
62
|
"allocateNotDockerPublished": "分配未被 Docker 占用的可用 TCP 端口失败。",
|
|
63
63
|
"alreadyInUse": "端口 {{port}} 已被占用,请更换其他端口。",
|
|
64
64
|
"alreadyInUseByDocker": "端口 {{port}} 已被 Docker 容器占用,请更换其他端口。"
|
|
65
|
+
},
|
|
66
|
+
"dbConnection": {
|
|
67
|
+
"unreachable": "无法连接到数据库 {{host}}:{{port}}。请检查主机、端口和网络连通性。详情:{{details}}",
|
|
68
|
+
"timeout": "连接数据库 {{host}}:{{port}} 超时,约 {{seconds}} 秒内未成功建立连接。",
|
|
69
|
+
"authenticationFailed": "无法使用用户 \"{{user}}\" 登录数据库 \"{{database}}\"。请检查用户名和密码。",
|
|
70
|
+
"databaseNotFound": "数据库 \"{{database}}\" 不存在,或当前连接配置无权访问该数据库。",
|
|
71
|
+
"connectionFailed": "数据库连接检查失败。详情:{{details}}"
|
|
65
72
|
}
|
|
66
73
|
},
|
|
67
74
|
"commands": {
|
|
@@ -273,7 +280,7 @@
|
|
|
273
280
|
"envExists": "Env \"{{envName}}\" 已存在,请换一个 env name。"
|
|
274
281
|
},
|
|
275
282
|
"messages": {
|
|
276
|
-
"title": "
|
|
283
|
+
"title": "配置供 Coding Agents 使用的 NocoBase",
|
|
277
284
|
"appNameRequiredWhenSkipped": "跳过 prompts 时必须提供 Env name。",
|
|
278
285
|
"appNameEnvHelp": "请使用 `nb init --yes --env <envName>` 继续。",
|
|
279
286
|
"resumeEnvRequired": "恢复安装时必须提供 Env name。",
|
|
@@ -303,24 +310,24 @@
|
|
|
303
310
|
}
|
|
304
311
|
},
|
|
305
312
|
"webUi": {
|
|
306
|
-
"pageTitle": "
|
|
307
|
-
"documentHeading": "
|
|
308
|
-
"documentHint": "连接已有的 NocoBase
|
|
313
|
+
"pageTitle": "配置供 Coding Agents 使用的 NocoBase",
|
|
314
|
+
"documentHeading": "配置供 Coding Agents 使用的 NocoBase",
|
|
315
|
+
"documentHint": "连接已有的 NocoBase 应用,或安装一个新的应用,让 Coding Agents 可以在当前工作区中访问和操作 NocoBase。",
|
|
309
316
|
"gettingStarted": {
|
|
310
317
|
"title": "开始设置",
|
|
311
|
-
"description": "
|
|
318
|
+
"description": "选择连接已有应用,或安装一个新应用。"
|
|
312
319
|
},
|
|
313
320
|
"connectExistingApp": {
|
|
314
321
|
"title": "连接已有应用",
|
|
315
|
-
"description": "
|
|
322
|
+
"description": "保存现有应用连接。"
|
|
316
323
|
},
|
|
317
324
|
"createNewApp": {
|
|
318
|
-
"title": "
|
|
319
|
-
"description": "
|
|
325
|
+
"title": "安装新应用",
|
|
326
|
+
"description": "设置应用基础信息和安装选项。"
|
|
320
327
|
},
|
|
321
328
|
"downloadAppFiles": {
|
|
322
329
|
"title": "下载应用文件",
|
|
323
|
-
"description": "
|
|
330
|
+
"description": "选择获取应用文件的方式。"
|
|
324
331
|
},
|
|
325
332
|
"configureDatabase": {
|
|
326
333
|
"title": "配置数据库",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.24",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"topicSeparator": " ",
|
|
38
38
|
"topics": {
|
|
39
39
|
"app": {
|
|
40
|
-
"description": "Manage NocoBase app runtimes: start, stop, restart, logs,
|
|
40
|
+
"description": "Manage NocoBase app runtimes: start, stop, restart, logs, and upgrades."
|
|
41
41
|
},
|
|
42
42
|
"source": {
|
|
43
43
|
"description": "Work with the local NocoBase source project: download, develop, build, and test."
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"description": "Manage the built-in database for the selected env."
|
|
57
57
|
},
|
|
58
58
|
"env": {
|
|
59
|
-
"description": "Manage NocoBase project environments and
|
|
59
|
+
"description": "Manage NocoBase project environments, status, details, and command runtimes."
|
|
60
60
|
},
|
|
61
61
|
"self": {
|
|
62
62
|
"description": "Inspect or update the NocoBase CLI itself."
|
|
@@ -78,8 +78,11 @@
|
|
|
78
78
|
"@oclif/core": "^4.10.4",
|
|
79
79
|
"cross-spawn": "^7.0.6",
|
|
80
80
|
"lodash": "^4.17.21",
|
|
81
|
+
"mariadb": "^2.5.6",
|
|
82
|
+
"mysql2": "^3.14.0",
|
|
81
83
|
"openapi-types": "^12.1.3",
|
|
82
84
|
"ora": "^8.2.0",
|
|
85
|
+
"pg": "^8.14.1",
|
|
83
86
|
"picocolors": "^1.1.1"
|
|
84
87
|
},
|
|
85
88
|
"devDependencies": {
|
|
@@ -91,5 +94,5 @@
|
|
|
91
94
|
"type": "git",
|
|
92
95
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
93
96
|
},
|
|
94
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
|
|
95
98
|
}
|
package/dist/commands/app/ps.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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 { Command, Flags } from '@oclif/core';
|
|
10
|
-
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../lib/app-runtime.js';
|
|
11
|
-
import { listEnvs } from '../../lib/auth-store.js';
|
|
12
|
-
import { renderTable } from '../../lib/ui.js';
|
|
13
|
-
import { appNetwork, appRootPath, appUrl, dbStatus, runtimeStatus, storagePath } from './shared.js';
|
|
14
|
-
export default class AppPs extends Command {
|
|
15
|
-
static hidden = false;
|
|
16
|
-
static description = 'Show NocoBase runtime status for configured envs without starting or stopping anything.';
|
|
17
|
-
static examples = [
|
|
18
|
-
'<%= config.bin %> <%= command.id %>',
|
|
19
|
-
'<%= config.bin %> <%= command.id %> --env app1',
|
|
20
|
-
];
|
|
21
|
-
static flags = {
|
|
22
|
-
env: Flags.string({
|
|
23
|
-
char: 'e',
|
|
24
|
-
description: 'CLI env name to inspect. Omit to show all configured envs',
|
|
25
|
-
}),
|
|
26
|
-
};
|
|
27
|
-
async run() {
|
|
28
|
-
const { flags } = await this.parse(AppPs);
|
|
29
|
-
const requestedEnv = flags.env?.trim() || undefined;
|
|
30
|
-
const envNames = requestedEnv
|
|
31
|
-
? [requestedEnv]
|
|
32
|
-
: Object.keys((await listEnvs()).envs).sort();
|
|
33
|
-
if (!envNames.length) {
|
|
34
|
-
this.log('No NocoBase env is configured yet. Run `nb init` to create one first.');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const rows = [];
|
|
38
|
-
for (const envName of envNames) {
|
|
39
|
-
const runtime = await resolveManagedAppRuntime(envName);
|
|
40
|
-
if (!runtime) {
|
|
41
|
-
if (requestedEnv) {
|
|
42
|
-
this.error(formatMissingManagedAppEnvMessage(envName));
|
|
43
|
-
}
|
|
44
|
-
rows.push([envName, '-', 'missing', '-', '-', '-', '-', '']);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
rows.push([
|
|
48
|
-
runtime.envName,
|
|
49
|
-
runtime.kind,
|
|
50
|
-
await runtimeStatus(runtime),
|
|
51
|
-
await dbStatus(runtime),
|
|
52
|
-
appNetwork(runtime),
|
|
53
|
-
appRootPath(runtime),
|
|
54
|
-
storagePath(runtime),
|
|
55
|
-
appUrl(runtime),
|
|
56
|
-
]);
|
|
57
|
-
}
|
|
58
|
-
this.log(renderTable(['Env', 'Kind', 'App Status', 'Database Status', 'Network', 'App Root', 'Storage', 'URL'], rows));
|
|
59
|
-
}
|
|
60
|
-
}
|
package/dist/commands/ps.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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 AppPs from './app/ps.js';
|
|
10
|
-
export default class Ps extends AppPs {
|
|
11
|
-
static hidden = true;
|
|
12
|
-
}
|