@nocobase/cli 2.1.0-alpha.25 → 2.1.0-alpha.27

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.
Files changed (84) hide show
  1. package/README.md +61 -49
  2. package/README.zh-CN.md +40 -47
  3. package/dist/commands/app/down.js +259 -0
  4. package/dist/commands/app/logs.js +98 -0
  5. package/dist/commands/app/restart.js +75 -0
  6. package/dist/commands/app/start.js +252 -0
  7. package/dist/commands/app/stop.js +98 -0
  8. package/dist/commands/app/upgrade.js +579 -0
  9. package/dist/commands/build.js +3 -48
  10. package/dist/commands/config/delete.js +30 -0
  11. package/dist/commands/config/get.js +29 -0
  12. package/dist/commands/config/index.js +20 -0
  13. package/dist/commands/config/list.js +29 -0
  14. package/dist/commands/config/set.js +35 -0
  15. package/dist/commands/db/check.js +230 -0
  16. package/dist/commands/db/shared.js +1 -1
  17. package/dist/commands/dev.js +3 -147
  18. package/dist/commands/down.js +3 -188
  19. package/dist/commands/download.js +4 -856
  20. package/dist/commands/env/add.js +28 -23
  21. package/dist/commands/env/info.js +152 -0
  22. package/dist/commands/env/list.js +23 -9
  23. package/dist/commands/env/shared.js +158 -0
  24. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  25. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  26. package/dist/commands/init.js +83 -6
  27. package/dist/commands/install.js +361 -82
  28. package/dist/commands/license/activate.js +357 -0
  29. package/dist/commands/license/env.js +94 -0
  30. package/dist/commands/license/generate-id.js +107 -0
  31. package/dist/commands/license/id.js +52 -0
  32. package/dist/commands/license/index.js +20 -0
  33. package/dist/commands/license/plugins/clean.js +98 -0
  34. package/dist/commands/license/plugins/index.js +20 -0
  35. package/dist/commands/license/plugins/list.js +50 -0
  36. package/dist/commands/license/plugins/shared.js +325 -0
  37. package/dist/commands/license/plugins/sync.js +267 -0
  38. package/dist/commands/license/shared.js +411 -0
  39. package/dist/commands/license/status.js +50 -0
  40. package/dist/commands/logs.js +3 -88
  41. package/dist/commands/plugin/disable.js +64 -0
  42. package/dist/commands/plugin/enable.js +64 -0
  43. package/dist/commands/plugin/list.js +62 -0
  44. package/dist/commands/pm/disable.js +3 -54
  45. package/dist/commands/pm/enable.js +3 -54
  46. package/dist/commands/pm/list.js +3 -52
  47. package/dist/commands/restart.js +3 -65
  48. package/dist/commands/scaffold/migration.js +1 -1
  49. package/dist/commands/scaffold/plugin.js +1 -1
  50. package/dist/commands/skills/remove.js +71 -0
  51. package/dist/commands/skills/update.js +7 -0
  52. package/dist/commands/source/build.js +58 -0
  53. package/dist/commands/source/dev.js +157 -0
  54. package/dist/commands/source/download.js +866 -0
  55. package/dist/commands/source/test.js +467 -0
  56. package/dist/commands/start.js +3 -209
  57. package/dist/commands/stop.js +3 -88
  58. package/dist/commands/test.js +3 -457
  59. package/dist/commands/upgrade.js +3 -585
  60. package/dist/help/runtime-help.js +3 -0
  61. package/dist/lib/api-client.js +94 -9
  62. package/dist/lib/app-health.js +126 -0
  63. package/dist/lib/app-managed-resources.js +264 -0
  64. package/dist/lib/app-runtime.js +26 -10
  65. package/dist/lib/auth-store.js +29 -63
  66. package/dist/lib/build-config.js +8 -0
  67. package/dist/lib/cli-config.js +176 -0
  68. package/dist/lib/cli-home.js +12 -26
  69. package/dist/lib/cli-locale.js +15 -1
  70. package/dist/lib/db-connection-check.js +178 -0
  71. package/dist/lib/env-config.js +80 -0
  72. package/dist/lib/generated-command.js +23 -3
  73. package/dist/lib/plugin-storage.js +127 -0
  74. package/dist/lib/prompt-validators.js +4 -4
  75. package/dist/lib/prompt-web-ui.js +13 -6
  76. package/dist/lib/runtime-generator.js +89 -10
  77. package/dist/lib/self-manager.js +57 -2
  78. package/dist/lib/skills-manager.js +34 -7
  79. package/dist/lib/startup-update.js +85 -7
  80. package/dist/locale/en-US.json +16 -13
  81. package/dist/locale/zh-CN.json +16 -13
  82. package/nocobase-ctl.config.json +82 -0
  83. package/package.json +41 -6
  84. package/dist/commands/ps.js +0 -119
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
- import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath, } from './cli-home.js';
11
+ import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath, } from './cli-home.js';
12
12
  function normalizeStoredEnvKind(value) {
13
13
  const kind = String(value ?? '').trim();
14
14
  if (kind === 'remote') {
@@ -68,8 +68,22 @@ function normalizeEnvConfigEntry(entry) {
68
68
  };
69
69
  }
70
70
  function normalizeAuthConfig(config) {
71
+ const settings = config.settings ?? {};
71
72
  return {
72
73
  name: config.name || config.dockerResourcePrefix,
74
+ settings: {
75
+ ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
76
+ ...(settings.docker?.network || settings.docker?.containerPrefix
77
+ ? {
78
+ docker: {
79
+ ...(settings.docker?.network ? { network: normalizeOptionalString(settings.docker.network) } : {}),
80
+ ...(settings.docker?.containerPrefix
81
+ ? { containerPrefix: normalizeOptionalString(settings.docker.containerPrefix) }
82
+ : {}),
83
+ },
84
+ }
85
+ : {}),
86
+ },
73
87
  currentEnv: config.currentEnv || 'default',
74
88
  envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
75
89
  };
@@ -83,45 +97,21 @@ function createDefaultConfig() {
83
97
  envs: {},
84
98
  };
85
99
  }
86
- function hasConfiguredEnvs(config) {
87
- return Object.keys(config.envs).length > 0;
88
- }
89
- function shouldFallbackToLegacyProjectScope(options = {}) {
90
- const requestedScope = options.scope ?? resolveDefaultConfigScope();
91
- return requestedScope === 'global';
92
- }
93
- async function loadExactAuthConfig(options = {}) {
100
+ async function readStoredAuthConfig(filePath) {
94
101
  try {
95
- const content = await fs.readFile(getConfigFile(options), 'utf8');
102
+ const content = await fs.readFile(filePath, 'utf8');
96
103
  const parsed = JSON.parse(content);
97
104
  return normalizeAuthConfig(parsed);
98
105
  }
99
106
  catch (_error) {
100
- return createDefaultConfig();
107
+ return undefined;
101
108
  }
102
109
  }
103
- async function resolveEnvStorageScope(envName, options = {}) {
104
- const requestedScope = options.scope ?? resolveDefaultConfigScope();
105
- if (requestedScope !== 'global') {
106
- return { ...options, scope: requestedScope };
107
- }
108
- const globalConfig = await loadExactAuthConfig({ scope: 'global' });
109
- if (globalConfig.envs[envName]) {
110
- return { ...options, scope: 'global' };
111
- }
112
- const projectConfig = await loadExactAuthConfig({ scope: 'project' });
113
- if (projectConfig.envs[envName]) {
114
- return { ...options, scope: 'project' };
115
- }
116
- return { ...options, scope: 'global' };
110
+ export async function loadExactAuthConfig(options = {}) {
111
+ return (await readStoredAuthConfig(getConfigFile(options))) ?? createDefaultConfig();
117
112
  }
118
113
  export async function loadAuthConfig(options = {}) {
119
- const config = await loadExactAuthConfig(options);
120
- if (!shouldFallbackToLegacyProjectScope(options) || hasConfiguredEnvs(config)) {
121
- return config;
122
- }
123
- const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
124
- return hasConfiguredEnvs(legacyProjectConfig) ? legacyProjectConfig : config;
114
+ return await loadExactAuthConfig(options);
125
115
  }
126
116
  export async function saveAuthConfig(config, options = {}) {
127
117
  const filePath = getConfigFile(options);
@@ -140,24 +130,12 @@ export async function getCurrentEnvName(options = {}) {
140
130
  return config.currentEnv || 'default';
141
131
  }
142
132
  export async function setCurrentEnv(envName, options = {}) {
143
- const writeOptions = await resolveEnvStorageScope(envName, options);
144
- const config = await loadExactAuthConfig(writeOptions);
133
+ const config = await loadExactAuthConfig(options);
145
134
  if (!config.envs[envName]) {
146
135
  throw new Error(`Env "${envName}" is not configured`);
147
136
  }
148
137
  config.currentEnv = envName;
149
- await saveAuthConfig(config, writeOptions);
150
- }
151
- export async function ensureWorkspaceName(defaultName, options = {}) {
152
- const config = await loadExactAuthConfig(options);
153
- const existing = config.name?.trim();
154
- if (existing) {
155
- return existing;
156
- }
157
- const next = defaultName.trim();
158
- config.name = next;
159
138
  await saveAuthConfig(config, options);
160
- return next;
161
139
  }
162
140
  export class Env {
163
141
  config;
@@ -231,16 +209,7 @@ export async function getEnv(envName, options = {}) {
231
209
  const resolved = envName?.trim() || config.currentEnv || 'default';
232
210
  const envConfig = config.envs[resolved];
233
211
  if (!envConfig) {
234
- if (!shouldFallbackToLegacyProjectScope(loadOptions)) {
235
- return undefined;
236
- }
237
- const legacyProjectConfig = await loadExactAuthConfig({ scope: 'project' });
238
- const legacyResolved = envName?.trim() || legacyProjectConfig.currentEnv || 'default';
239
- const legacyEnvConfig = legacyProjectConfig.envs[legacyResolved];
240
- if (!legacyEnvConfig) {
241
- return undefined;
242
- }
243
- return new Env({ ...(normalizeEnvConfigEntry(legacyEnvConfig) ?? {}), name: legacyResolved });
212
+ return undefined;
244
213
  }
245
214
  return new Env({ ...(normalizeEnvConfigEntry(envConfig) ?? {}), name: resolved });
246
215
  }
@@ -266,12 +235,11 @@ function areAuthConfigsEquivalent(left, right) {
266
235
  return false;
267
236
  }
268
237
  async function writeEnv(envName, updater, options = {}) {
269
- const writeOptions = await resolveEnvStorageScope(envName, options);
270
- const config = await loadExactAuthConfig(writeOptions);
238
+ const config = await loadExactAuthConfig(options);
271
239
  const previous = config.envs[envName];
272
240
  config.envs[envName] = updater(previous);
273
241
  config.currentEnv = envName;
274
- await saveAuthConfig(config, writeOptions);
242
+ await saveAuthConfig(config, options);
275
243
  }
276
244
  export async function upsertEnv(envName, config, options = {}) {
277
245
  await writeEnv(envName, (previous) => {
@@ -327,19 +295,17 @@ export async function setEnvOauthSession(envName, auth, options = {}) {
327
295
  }), options);
328
296
  }
329
297
  export async function setEnvRuntime(envName, runtime, options = {}) {
330
- const writeOptions = await resolveEnvStorageScope(envName, options);
331
- const config = await loadExactAuthConfig(writeOptions);
298
+ const config = await loadExactAuthConfig(options);
332
299
  const current = config.envs[envName] ?? {};
333
300
  config.envs[envName] = {
334
301
  ...current,
335
302
  runtime,
336
303
  };
337
304
  config.currentEnv = envName;
338
- await saveAuthConfig(config, writeOptions);
305
+ await saveAuthConfig(config, options);
339
306
  }
340
307
  export async function removeEnv(envName, options = {}) {
341
- const writeOptions = await resolveEnvStorageScope(envName, options);
342
- const config = await loadExactAuthConfig(writeOptions);
308
+ const config = await loadExactAuthConfig(options);
343
309
  if (!config.envs[envName]) {
344
310
  throw new Error(`Env "${envName}" is not configured`);
345
311
  }
@@ -348,7 +314,7 @@ export async function removeEnv(envName, options = {}) {
348
314
  const nextEnv = Object.keys(config.envs).sort()[0];
349
315
  config.currentEnv = nextEnv ?? 'default';
350
316
  }
351
- await saveAuthConfig(config, writeOptions);
317
+ await saveAuthConfig(config, options);
352
318
  return {
353
319
  removed: envName,
354
320
  currentEnv: config.currentEnv || 'default',
@@ -1,3 +1,11 @@
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
+ */
1
9
  import { promises as fs } from 'node:fs';
2
10
  export async function loadBuildConfig(filePath) {
3
11
  try {
@@ -0,0 +1,176 @@
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 { loadExactAuthConfig, saveAuthConfig } from './auth-store.js';
10
+ import { resolveDefaultConfigScope } from './cli-home.js';
11
+ export const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
12
+ export const DEFAULT_DOCKER_NETWORK = 'nocobase';
13
+ export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
14
+ export const SUPPORTED_CLI_CONFIG_KEYS = [
15
+ 'license.pkg-url',
16
+ 'docker.network',
17
+ 'docker.container-prefix',
18
+ ];
19
+ function trimValue(value) {
20
+ const text = String(value ?? '').trim();
21
+ return text || undefined;
22
+ }
23
+ function resolveScope(options = {}) {
24
+ return {
25
+ scope: options.scope ?? resolveDefaultConfigScope(),
26
+ };
27
+ }
28
+ export function isSupportedCliConfigKey(value) {
29
+ return SUPPORTED_CLI_CONFIG_KEYS.includes(value);
30
+ }
31
+ export function assertSupportedCliConfigKey(value) {
32
+ if (!isSupportedCliConfigKey(value)) {
33
+ throw new Error(`Unsupported config key "${value}". Supported keys: ${SUPPORTED_CLI_CONFIG_KEYS.join(', ')}`);
34
+ }
35
+ return value;
36
+ }
37
+ function cloneSettings(config) {
38
+ return {
39
+ license: config.settings?.license ? { ...config.settings.license } : undefined,
40
+ docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
41
+ };
42
+ }
43
+ function pruneSettings(config) {
44
+ const license = config.settings?.license;
45
+ if (license && !trimValue(license.pkgUrl)) {
46
+ delete config.settings?.license;
47
+ }
48
+ const docker = config.settings?.docker;
49
+ if (docker && !trimValue(docker.network) && !trimValue(docker.containerPrefix)) {
50
+ delete config.settings?.docker;
51
+ }
52
+ if (config.settings
53
+ && !config.settings.license
54
+ && !config.settings.docker) {
55
+ delete config.settings;
56
+ }
57
+ }
58
+ export function getExplicitCliConfigValue(config, key) {
59
+ switch (key) {
60
+ case 'license.pkg-url':
61
+ return trimValue(config.settings?.license?.pkgUrl);
62
+ case 'docker.network':
63
+ return trimValue(config.settings?.docker?.network);
64
+ case 'docker.container-prefix':
65
+ return trimValue(config.settings?.docker?.containerPrefix);
66
+ }
67
+ }
68
+ export function getEffectiveCliConfigValue(config, key) {
69
+ const explicit = getExplicitCliConfigValue(config, key);
70
+ if (explicit) {
71
+ return explicit;
72
+ }
73
+ switch (key) {
74
+ case 'license.pkg-url':
75
+ return DEFAULT_LICENSE_PKG_URL;
76
+ case 'docker.network':
77
+ return trimValue(config.name) || DEFAULT_DOCKER_NETWORK;
78
+ case 'docker.container-prefix':
79
+ return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
80
+ }
81
+ }
82
+ export function normalizeCliConfigValue(key, value) {
83
+ const normalized = value.trim();
84
+ if (!normalized) {
85
+ throw new Error(`Config key "${key}" requires a non-empty value.`);
86
+ }
87
+ if (key === 'license.pkg-url') {
88
+ return normalized.replace(/\/+$/, '') + '/';
89
+ }
90
+ return normalized;
91
+ }
92
+ export async function loadCliConfig(options = {}) {
93
+ return await loadExactAuthConfig(resolveScope(options));
94
+ }
95
+ export async function getCliConfigValue(key, options = {}) {
96
+ const config = await loadCliConfig(options);
97
+ return getEffectiveCliConfigValue(config, key);
98
+ }
99
+ export async function listExplicitCliConfigValues(options = {}) {
100
+ const config = await loadCliConfig(options);
101
+ const out = {};
102
+ for (const key of SUPPORTED_CLI_CONFIG_KEYS) {
103
+ const value = getExplicitCliConfigValue(config, key);
104
+ if (value) {
105
+ out[key] = value;
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ export async function setCliConfigValue(key, value, options = {}) {
111
+ const scope = resolveScope(options);
112
+ const config = await loadExactAuthConfig(scope);
113
+ const normalized = normalizeCliConfigValue(key, value);
114
+ config.settings = cloneSettings(config);
115
+ switch (key) {
116
+ case 'license.pkg-url':
117
+ config.settings.license = {
118
+ ...(config.settings.license ?? {}),
119
+ pkgUrl: normalized,
120
+ };
121
+ break;
122
+ case 'docker.network':
123
+ config.settings.docker = {
124
+ ...(config.settings.docker ?? {}),
125
+ network: normalized,
126
+ };
127
+ break;
128
+ case 'docker.container-prefix':
129
+ config.settings.docker = {
130
+ ...(config.settings.docker ?? {}),
131
+ containerPrefix: normalized,
132
+ };
133
+ break;
134
+ }
135
+ pruneSettings(config);
136
+ await saveAuthConfig(config, scope);
137
+ return normalized;
138
+ }
139
+ export async function deleteCliConfigValue(key, options = {}) {
140
+ const scope = resolveScope(options);
141
+ const config = await loadExactAuthConfig(scope);
142
+ const hadValue = Boolean(getExplicitCliConfigValue(config, key));
143
+ if (!hadValue) {
144
+ return false;
145
+ }
146
+ config.settings = cloneSettings(config);
147
+ switch (key) {
148
+ case 'license.pkg-url':
149
+ if (config.settings.license) {
150
+ delete config.settings.license.pkgUrl;
151
+ }
152
+ break;
153
+ case 'docker.network':
154
+ if (config.settings.docker) {
155
+ delete config.settings.docker.network;
156
+ }
157
+ break;
158
+ case 'docker.container-prefix':
159
+ if (config.settings.docker) {
160
+ delete config.settings.docker.containerPrefix;
161
+ }
162
+ break;
163
+ }
164
+ pruneSettings(config);
165
+ await saveAuthConfig(config, scope);
166
+ return true;
167
+ }
168
+ export async function resolveDockerNetworkName(options = {}) {
169
+ return await getCliConfigValue('docker.network', options);
170
+ }
171
+ export async function resolveDockerContainerPrefix(options = {}) {
172
+ return await getCliConfigValue('docker.container-prefix', options);
173
+ }
174
+ export async function resolveLicensePkgUrlFromConfig(options = {}) {
175
+ return await getCliConfigValue('license.pkg-url', options);
176
+ }
@@ -6,45 +6,30 @@
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 fs from 'node:fs';
10
9
  import os from 'node:os';
11
10
  import path from 'node:path';
12
11
  export const CLI_HOME_DIRNAME = '.nocobase';
13
- export const NB_CONFIG_SCOPE_ENV = 'NB_CONFIG_SCOPE';
14
- export const NB_ENV_ROOT_ENV = 'NB_ENV_ROOT';
12
+ export const NB_CLI_ROOT_ENV = 'NB_CLI_ROOT';
15
13
  export function resolveDefaultConfigScope() {
16
- const raw = String(process.env[NB_CONFIG_SCOPE_ENV] ?? '').trim().toLowerCase();
17
- return raw === 'project' ? 'project' : 'global';
14
+ return 'global';
15
+ }
16
+ function readConfiguredPath(name) {
17
+ const value = String(process.env[name] ?? '').trim();
18
+ return value || undefined;
18
19
  }
19
20
  function resolveGlobalCliHomeRoot() {
20
- if (process.env.NOCOBASE_CTL_HOME) {
21
- return process.env.NOCOBASE_CTL_HOME;
22
- }
23
- return os.homedir();
21
+ return readConfiguredPath(NB_CLI_ROOT_ENV) ?? os.homedir();
24
22
  }
25
23
  export function resolveCliHomeRoot(scope = resolveDefaultConfigScope()) {
26
- const cwdRoot = process.cwd();
27
- if (scope === 'project') {
28
- return cwdRoot;
29
- }
30
- if (scope === 'global') {
31
- return resolveGlobalCliHomeRoot();
32
- }
33
- const cwdCliHome = path.join(cwdRoot, CLI_HOME_DIRNAME);
34
- if (fs.existsSync(cwdCliHome)) {
35
- return cwdRoot;
36
- }
24
+ void scope;
37
25
  return resolveGlobalCliHomeRoot();
38
26
  }
39
27
  export function resolveCliHomeDir(scope = resolveDefaultConfigScope()) {
40
28
  return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
41
29
  }
42
30
  export function resolveEnvRoot(scope = resolveDefaultConfigScope()) {
43
- const envRoot = String(process.env[NB_ENV_ROOT_ENV] ?? '').trim();
44
- if (envRoot) {
45
- return path.resolve(envRoot);
46
- }
47
- return resolveCliHomeRoot(scope);
31
+ void scope;
32
+ return resolveCliHomeRoot();
48
33
  }
49
34
  export function resolveEnvRelativePath(relativePath, scope = resolveDefaultConfigScope()) {
50
35
  return path.resolve(resolveEnvRoot(scope), relativePath);
@@ -57,5 +42,6 @@ export function resolveConfiguredEnvPath(value, scope = resolveDefaultConfigScop
57
42
  return path.isAbsolute(text) ? text : resolveEnvRelativePath(text, scope);
58
43
  }
59
44
  export function formatCliHomeScope(scope) {
60
- return scope === 'project' ? 'project' : 'global';
45
+ void scope;
46
+ return 'global';
61
47
  }
@@ -7,6 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { readFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
10
12
  export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
11
13
  export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
12
14
  export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
@@ -30,8 +32,20 @@ function loadLocaleMessages(locale) {
30
32
  if (localeCache[locale]) {
31
33
  return localeCache[locale];
32
34
  }
35
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
36
+ const fallbackPath = path.resolve(moduleDir, '..', 'locale', `${locale}.json`);
33
37
  const fileUrl = new URL(`../locale/${locale}.json`, import.meta.url);
34
- const parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
41
+ }
42
+ catch (error) {
43
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
44
+ if (code !== 'ENOENT') {
45
+ throw error;
46
+ }
47
+ parsed = JSON.parse(readFileSync(fallbackPath, 'utf8'));
48
+ }
35
49
  localeCache[locale] = parsed;
36
50
  return parsed;
37
51
  }
@@ -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
+ }