@nocobase/cli 2.1.0-beta.23 → 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 +5 -0
- package/README.zh-CN.md +4 -0
- package/dist/commands/db/check.js +132 -0
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +37 -0
- package/dist/lib/db-connection-check.js +178 -0
- 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 +5 -2
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,6 +115,7 @@ 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.
|
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,6 +109,7 @@ 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 连接相关配置。在交互模式下,只会继续补齐缺失的初始化参数。
|
|
@@ -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
|
+
}
|
package/dist/commands/init.js
CHANGED
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)) {
|
|
@@ -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
|
+
}
|
|
@@ -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",
|
|
@@ -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
|
}
|