@nocobase/cli 2.1.0-alpha.26 → 2.1.0-alpha.28

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 (52) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +2 -3
  4. package/dist/commands/app/logs.js +2 -2
  5. package/dist/commands/app/upgrade.js +114 -128
  6. package/dist/commands/config/delete.js +30 -0
  7. package/dist/commands/config/get.js +29 -0
  8. package/dist/commands/config/index.js +20 -0
  9. package/dist/commands/config/list.js +29 -0
  10. package/dist/commands/config/set.js +35 -0
  11. package/dist/commands/db/check.js +238 -0
  12. package/dist/commands/db/logs.js +2 -2
  13. package/dist/commands/db/shared.js +6 -5
  14. package/dist/commands/env/info.js +6 -2
  15. package/dist/commands/env/shared.js +1 -1
  16. package/dist/commands/init.js +0 -1
  17. package/dist/commands/install.js +87 -35
  18. package/dist/commands/license/activate.js +357 -0
  19. package/dist/commands/license/env.js +94 -0
  20. package/dist/commands/license/generate-id.js +107 -0
  21. package/dist/commands/license/id.js +52 -0
  22. package/dist/commands/license/index.js +20 -0
  23. package/dist/commands/license/plugins/clean.js +98 -0
  24. package/dist/commands/license/plugins/index.js +20 -0
  25. package/dist/commands/license/plugins/list.js +50 -0
  26. package/dist/commands/license/plugins/shared.js +325 -0
  27. package/dist/commands/license/plugins/sync.js +267 -0
  28. package/dist/commands/license/shared.js +414 -0
  29. package/dist/commands/license/status.js +50 -0
  30. package/dist/lib/api-client.js +74 -3
  31. package/dist/lib/app-managed-resources.js +10 -6
  32. package/dist/lib/app-runtime.js +29 -11
  33. package/dist/lib/auth-store.js +36 -68
  34. package/dist/lib/build-config.js +8 -0
  35. package/dist/lib/builtin-db.js +86 -0
  36. package/dist/lib/cli-config.js +176 -0
  37. package/dist/lib/cli-home.js +6 -21
  38. package/dist/lib/db-connection-check.js +178 -0
  39. package/dist/lib/env-config.js +7 -0
  40. package/dist/lib/generated-command.js +23 -3
  41. package/dist/lib/plugin-storage.js +127 -0
  42. package/dist/lib/prompt-validators.js +4 -4
  43. package/dist/lib/run-npm.js +53 -0
  44. package/dist/lib/runtime-env-vars.js +32 -0
  45. package/dist/lib/runtime-generator.js +89 -10
  46. package/dist/lib/self-manager.js +57 -2
  47. package/dist/lib/skills-manager.js +2 -2
  48. package/dist/lib/startup-update.js +85 -7
  49. package/dist/locale/en-US.json +16 -13
  50. package/dist/locale/zh-CN.json +16 -13
  51. package/nocobase-ctl.config.json +82 -0
  52. package/package.json +16 -4
@@ -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,15 +6,12 @@
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
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';
18
15
  }
19
16
  function readConfiguredPath(name) {
20
17
  const value = String(process.env[name] ?? '').trim();
@@ -24,28 +21,15 @@ function resolveGlobalCliHomeRoot() {
24
21
  return readConfiguredPath(NB_CLI_ROOT_ENV) ?? os.homedir();
25
22
  }
26
23
  export function resolveCliHomeRoot(scope = resolveDefaultConfigScope()) {
27
- const cwdRoot = process.cwd();
28
- if (scope === 'project') {
29
- return cwdRoot;
30
- }
31
- if (scope === 'global') {
32
- return resolveGlobalCliHomeRoot();
33
- }
34
- const cwdCliHome = path.join(cwdRoot, CLI_HOME_DIRNAME);
35
- if (fs.existsSync(cwdCliHome)) {
36
- return cwdRoot;
37
- }
24
+ void scope;
38
25
  return resolveGlobalCliHomeRoot();
39
26
  }
40
27
  export function resolveCliHomeDir(scope = resolveDefaultConfigScope()) {
41
28
  return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
42
29
  }
43
30
  export function resolveEnvRoot(scope = resolveDefaultConfigScope()) {
44
- const envRoot = readConfiguredPath(NB_CLI_ROOT_ENV);
45
- if (envRoot) {
46
- return path.resolve(envRoot);
47
- }
48
- return resolveCliHomeRoot(scope);
31
+ void scope;
32
+ return resolveCliHomeRoot();
49
33
  }
50
34
  export function resolveEnvRelativePath(relativePath, scope = resolveDefaultConfigScope()) {
51
35
  return path.resolve(resolveEnvRoot(scope), relativePath);
@@ -58,5 +42,6 @@ export function resolveConfiguredEnvPath(value, scope = resolveDefaultConfigScop
58
42
  return path.isAbsolute(text) ? text : resolveEnvRelativePath(text, scope);
59
43
  }
60
44
  export function formatCliHomeScope(scope) {
61
- return scope === 'project' ? 'project' : 'global';
45
+ void scope;
46
+ return 'global';
62
47
  }
@@ -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
+ }
@@ -71,6 +71,13 @@ export function buildStoredEnvConfig(input) {
71
71
  if (input.builtinDb === false) {
72
72
  envConfig.builtinDbImage = undefined;
73
73
  }
74
+ if (input.builtinDb === true) {
75
+ delete envConfig.dbHost;
76
+ const source = trimConfigValue(input.source);
77
+ if (source === 'docker') {
78
+ delete envConfig.dbPort;
79
+ }
80
+ }
74
81
  const authType = trimConfigValue(input.authType);
75
82
  const accessToken = trimConfigValue(input.accessToken);
76
83
  if (authType === 'token' && accessToken) {
@@ -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
  /**
2
10
  * This file is part of the NocoBase (R) project.
3
11
  * Copyright (c) 2020-2024 NocoBase Co., Ltd.
@@ -12,7 +20,10 @@ import { applyPostProcessor } from './post-processors.js';
12
20
  import { registerPostProcessors } from '../post-processors/index.js';
13
21
  function buildParameterFlag(parameter, options) {
14
22
  const hints = [parameter.in];
15
- if (parameter.type === 'object' || parameter.type === 'array' || parameter.jsonEncoded) {
23
+ if (parameter.isFile) {
24
+ hints.push('file path');
25
+ }
26
+ else if (parameter.type === 'object' || parameter.type === 'array' || parameter.jsonEncoded) {
16
27
  hints.push('JSON');
17
28
  }
18
29
  else if (parameter.isArray) {
@@ -67,10 +78,10 @@ export function createGeneratedFlags(operation) {
67
78
  // Body flags are an alternative authoring path to --body/--body-file.
68
79
  // Enforce required body semantics later in parseBody(), after we know
69
80
  // which input mode the user chose.
70
- required: parameter.in === 'body' ? false : parameter.required,
81
+ required: parameter.in === 'body' && !parameter.isFile ? false : parameter.required,
71
82
  });
72
83
  }
73
- if (operation.hasBody) {
84
+ if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
74
85
  flags.body = Flags.string({
75
86
  description: 'Full JSON request body string. Do not combine with body field flags.',
76
87
  helpGroup: 'Raw JSON Body',
@@ -82,6 +93,13 @@ export function createGeneratedFlags(operation) {
82
93
  exclusive: ['body'],
83
94
  });
84
95
  }
96
+ if (operation.responseType === 'binary') {
97
+ flags.output = Flags.string({
98
+ description: 'Path where the downloaded response should be written.',
99
+ helpGroup: 'Output',
100
+ required: true,
101
+ });
102
+ }
85
103
  flags['api-base-url'] = Flags.string({
86
104
  description: 'NocoBase API base URL, for example http://localhost:13000/api',
87
105
  helpGroup: 'Global',
@@ -132,6 +150,8 @@ export class GeneratedApiCommand extends Command {
132
150
  parameters: ctor.operation.parameters,
133
151
  hasBody: ctor.operation.hasBody,
134
152
  bodyRequired: ctor.operation.bodyRequired,
153
+ requestContentType: ctor.operation.requestContentType,
154
+ responseType: ctor.operation.responseType,
135
155
  },
136
156
  });
137
157
  if (!response.ok) {
@@ -0,0 +1,127 @@
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 path from 'node:path';
10
+ import { access, lstat, mkdir, readdir, readlink, realpath, rm, stat, symlink } from 'node:fs/promises';
11
+ async function pathExists(target) {
12
+ try {
13
+ await access(target);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function resolvePluginStoragePath(storagePath) {
21
+ const root = String(storagePath ?? process.env.STORAGE_PATH ?? '').trim();
22
+ if (root) {
23
+ return path.join(path.isAbsolute(root) ? root : path.resolve(process.cwd(), root), 'plugins');
24
+ }
25
+ const configured = String(process.env.PLUGIN_STORAGE_PATH ?? '').trim();
26
+ if (configured) {
27
+ return path.isAbsolute(configured) ? configured : path.resolve(process.cwd(), configured);
28
+ }
29
+ return path.resolve(process.cwd(), 'storage', 'plugins');
30
+ }
31
+ async function getStoragePluginNames(target) {
32
+ const plugins = [];
33
+ const items = await readdir(target);
34
+ for (const item of items) {
35
+ const itemPath = path.resolve(target, item);
36
+ if (item.startsWith('@')) {
37
+ const statResult = await stat(itemPath);
38
+ if (!statResult.isDirectory()) {
39
+ continue;
40
+ }
41
+ const children = await getStoragePluginNames(itemPath);
42
+ plugins.push(...children.map((child) => `${item}/${child}`));
43
+ continue;
44
+ }
45
+ if (await pathExists(path.resolve(itemPath, 'package.json'))) {
46
+ plugins.push(item);
47
+ }
48
+ }
49
+ return plugins;
50
+ }
51
+ async function ensureOrgDirectory(nodeModulesPath, pluginName) {
52
+ if (!pluginName.startsWith('@')) {
53
+ return;
54
+ }
55
+ const [orgName] = pluginName.split('/');
56
+ await mkdir(path.resolve(nodeModulesPath, orgName), { recursive: true });
57
+ }
58
+ async function isSymlinkValid(linkPath, targetPath) {
59
+ try {
60
+ if (await pathExists(linkPath)) {
61
+ const realPath = await realpath(linkPath);
62
+ return realPath === targetPath;
63
+ }
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ return false;
69
+ }
70
+ async function createStoragePluginSymlink(storagePluginsPath, nodeModulesPath, pluginName) {
71
+ const targetPath = path.resolve(storagePluginsPath, pluginName);
72
+ if (!(await pathExists(targetPath))) {
73
+ return;
74
+ }
75
+ await ensureOrgDirectory(nodeModulesPath, pluginName);
76
+ const linkPath = path.resolve(nodeModulesPath, pluginName);
77
+ if (await isSymlinkValid(linkPath, targetPath)) {
78
+ return;
79
+ }
80
+ await rm(linkPath, { recursive: true, force: true });
81
+ await symlink(targetPath, linkPath, 'dir');
82
+ }
83
+ export async function createStoragePluginsSymlink(storagePath, nodeModulesPath = String(process.env.NODE_MODULES_PATH ?? '').trim()) {
84
+ if (!nodeModulesPath) {
85
+ return;
86
+ }
87
+ const storagePluginsPath = resolvePluginStoragePath(storagePath);
88
+ if (!(await pathExists(storagePluginsPath))) {
89
+ return;
90
+ }
91
+ const pluginNames = await getStoragePluginNames(storagePluginsPath);
92
+ await Promise.all(pluginNames.map(async (pluginName) => await createStoragePluginSymlink(storagePluginsPath, nodeModulesPath, pluginName)));
93
+ }
94
+ export async function removeStoragePluginSymlink(pluginName, storagePath, nodeModulesPath = String(process.env.NODE_MODULES_PATH ?? '').trim()) {
95
+ if (!nodeModulesPath) {
96
+ return false;
97
+ }
98
+ const storagePluginsPath = resolvePluginStoragePath(storagePath);
99
+ const targetPath = path.resolve(storagePluginsPath, pluginName);
100
+ const linkPath = path.resolve(nodeModulesPath, pluginName);
101
+ if (!(await pathExists(linkPath))) {
102
+ return false;
103
+ }
104
+ let statResult;
105
+ try {
106
+ statResult = await lstat(linkPath);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ if (!statResult.isSymbolicLink()) {
112
+ return false;
113
+ }
114
+ let resolvedLinkTarget = '';
115
+ try {
116
+ const linkTarget = await readlink(linkPath);
117
+ resolvedLinkTarget = path.resolve(path.dirname(linkPath), linkTarget);
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ if (resolvedLinkTarget !== targetPath) {
123
+ return false;
124
+ }
125
+ await rm(linkPath, { recursive: true, force: true });
126
+ return true;
127
+ }
@@ -173,13 +173,13 @@ export async function validateAvailableTcpPort(value) {
173
173
  return formatError;
174
174
  }
175
175
  const port = parseTcpPort(raw);
176
- const available = await canListenOnTcpPort(port);
177
- if (!available) {
178
- return translateCli('validators.tcpPort.alreadyInUse', { port });
179
- }
180
176
  const dockerPorts = await getDockerPublishedTcpPorts();
181
177
  if (dockerPorts.has(port)) {
182
178
  return translateCli('validators.tcpPort.alreadyInUseByDocker', { port });
183
179
  }
180
+ const available = await canListenOnTcpPort(port);
181
+ if (!available) {
182
+ return translateCli('validators.tcpPort.alreadyInUse', { port });
183
+ }
184
184
  return undefined;
185
185
  }