@nocobase/cli 2.1.0-beta.23 → 2.1.0-beta.25
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 +24 -0
- package/README.zh-CN.md +4 -0
- package/dist/commands/app/down.js +12 -6
- package/dist/commands/app/logs.js +2 -2
- package/dist/commands/app/start.js +2 -1
- package/dist/commands/app/stop.js +2 -1
- package/dist/commands/app/upgrade.js +116 -129
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +238 -0
- package/dist/commands/db/logs.js +2 -2
- package/dist/commands/db/shared.js +6 -5
- package/dist/commands/db/start.js +2 -1
- package/dist/commands/db/stop.js +2 -1
- package/dist/commands/env/info.js +6 -2
- package/dist/commands/env/shared.js +1 -1
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +87 -35
- package/dist/commands/license/activate.js +360 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +108 -0
- package/dist/commands/license/id.js +56 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +101 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +269 -0
- package/dist/commands/license/shared.js +414 -0
- package/dist/commands/license/status.js +50 -0
- package/dist/commands/plugin/disable.js +2 -0
- package/dist/commands/plugin/enable.js +2 -0
- package/dist/commands/source/dev.js +2 -1
- package/dist/lib/api-client.js +74 -3
- package/dist/lib/app-managed-resources.js +10 -6
- package/dist/lib/app-runtime.js +29 -11
- package/dist/lib/auth-store.js +36 -68
- package/dist/lib/bootstrap.js +0 -4
- package/dist/lib/build-config.js +8 -0
- package/dist/lib/builtin-db.js +86 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +6 -21
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-config.js +7 -0
- package/dist/lib/generated-command.js +24 -3
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/prompt-validators.js +4 -4
- package/dist/lib/run-npm.js +53 -0
- package/dist/lib/runtime-env-vars.js +32 -0
- package/dist/lib/runtime-generator.js +89 -10
- package/dist/lib/self-manager.js +57 -2
- package/dist/lib/skills-manager.js +2 -2
- package/dist/lib/startup-update.js +85 -7
- package/dist/lib/ui.js +3 -0
- package/dist/locale/en-US.json +16 -13
- package/dist/locale/zh-CN.json +16 -13
- package/nocobase-ctl.config.json +82 -0
- package/package.json +16 -4
|
@@ -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
|
+
}
|
package/dist/lib/env-config.js
CHANGED
|
@@ -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.
|
|
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) {
|
|
@@ -42,6 +53,7 @@ function buildParameterFlag(parameter, options) {
|
|
|
42
53
|
if (parameter.type === 'boolean') {
|
|
43
54
|
return Flags.boolean({
|
|
44
55
|
description,
|
|
56
|
+
allowNo: true,
|
|
45
57
|
...(helpGroup ? { helpGroup } : {}),
|
|
46
58
|
...(required ? { required: true } : {}),
|
|
47
59
|
});
|
|
@@ -67,10 +79,10 @@ export function createGeneratedFlags(operation) {
|
|
|
67
79
|
// Body flags are an alternative authoring path to --body/--body-file.
|
|
68
80
|
// Enforce required body semantics later in parseBody(), after we know
|
|
69
81
|
// which input mode the user chose.
|
|
70
|
-
required: parameter.in === 'body' ? false : parameter.required,
|
|
82
|
+
required: parameter.in === 'body' && !parameter.isFile ? false : parameter.required,
|
|
71
83
|
});
|
|
72
84
|
}
|
|
73
|
-
if (operation.hasBody) {
|
|
85
|
+
if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
|
|
74
86
|
flags.body = Flags.string({
|
|
75
87
|
description: 'Full JSON request body string. Do not combine with body field flags.',
|
|
76
88
|
helpGroup: 'Raw JSON Body',
|
|
@@ -82,6 +94,13 @@ export function createGeneratedFlags(operation) {
|
|
|
82
94
|
exclusive: ['body'],
|
|
83
95
|
});
|
|
84
96
|
}
|
|
97
|
+
if (operation.responseType === 'binary') {
|
|
98
|
+
flags.output = Flags.string({
|
|
99
|
+
description: 'Path where the downloaded response should be written.',
|
|
100
|
+
helpGroup: 'Output',
|
|
101
|
+
required: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
85
104
|
flags['api-base-url'] = Flags.string({
|
|
86
105
|
description: 'NocoBase API base URL, for example http://localhost:13000/api',
|
|
87
106
|
helpGroup: 'Global',
|
|
@@ -132,6 +151,8 @@ export class GeneratedApiCommand extends Command {
|
|
|
132
151
|
parameters: ctor.operation.parameters,
|
|
133
152
|
hasBody: ctor.operation.hasBody,
|
|
134
153
|
bodyRequired: ctor.operation.bodyRequired,
|
|
154
|
+
requestContentType: ctor.operation.requestContentType,
|
|
155
|
+
responseType: ctor.operation.responseType,
|
|
135
156
|
},
|
|
136
157
|
});
|
|
137
158
|
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
|
}
|
package/dist/lib/run-npm.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'node:fs';
|
|
18
|
+
import fsp from 'node:fs/promises';
|
|
19
|
+
import os from 'node:os';
|
|
18
20
|
import path from 'node:path';
|
|
19
21
|
import spawn from 'cross-spawn';
|
|
20
22
|
const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
@@ -178,6 +180,57 @@ export function commandOutput(name, args, options) {
|
|
|
178
180
|
});
|
|
179
181
|
});
|
|
180
182
|
}
|
|
183
|
+
async function readCommandOutputFile(filePath) {
|
|
184
|
+
try {
|
|
185
|
+
return await fsp.readFile(filePath, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function commandOutputViaFile(name, args, options) {
|
|
192
|
+
const cwd = resolveCwd(options?.cwd);
|
|
193
|
+
const label = options?.errorName ?? name;
|
|
194
|
+
const command = resolveCommandName(name);
|
|
195
|
+
const captureDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-output-'));
|
|
196
|
+
const stdoutPath = path.join(captureDir, 'stdout.log');
|
|
197
|
+
const stderrPath = path.join(captureDir, 'stderr.log');
|
|
198
|
+
const stdoutHandle = await fsp.open(stdoutPath, 'w');
|
|
199
|
+
const stderrHandle = await fsp.open(stderrPath, 'w');
|
|
200
|
+
try {
|
|
201
|
+
const result = await new Promise((resolve, reject) => {
|
|
202
|
+
const child = spawn(command, [...args], {
|
|
203
|
+
cwd,
|
|
204
|
+
env: {
|
|
205
|
+
...process.env,
|
|
206
|
+
...options?.env,
|
|
207
|
+
},
|
|
208
|
+
stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
|
|
209
|
+
windowsHide: process.platform === 'win32',
|
|
210
|
+
});
|
|
211
|
+
child.once('error', reject);
|
|
212
|
+
child.once('close', (code, signal) => {
|
|
213
|
+
resolve({ code, signal });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
await stdoutHandle.close();
|
|
217
|
+
await stderrHandle.close();
|
|
218
|
+
const stdout = await readCommandOutputFile(stdoutPath);
|
|
219
|
+
const stderr = await readCommandOutputFile(stderrPath);
|
|
220
|
+
if (result.code === 0) {
|
|
221
|
+
return stdout.trim();
|
|
222
|
+
}
|
|
223
|
+
if (result.signal) {
|
|
224
|
+
throw new Error(`${label} exited due to signal ${result.signal}`);
|
|
225
|
+
}
|
|
226
|
+
const details = stderr.trim() || stdout.trim();
|
|
227
|
+
throw new Error(details ? `${label} exited with code ${result.code}: ${details}` : `${label} exited with code ${result.code}`);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await Promise.allSettled([stdoutHandle.close(), stderrHandle.close()]);
|
|
231
|
+
await fsp.rm(captureDir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
181
234
|
/** Run `yarn` with the given argument list, inheriting stdio (errors label as `npm` for compatibility). */
|
|
182
235
|
export function runNpm(args, options) {
|
|
183
236
|
return run('yarn', [...args], { ...options, errorName: 'npm' });
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { resolveBuiltinDbConnection } from './builtin-db.js';
|
|
10
|
+
function put(out, key, value) {
|
|
11
|
+
if (value === undefined || value === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
out[key] = String(value);
|
|
15
|
+
}
|
|
16
|
+
export async function buildRuntimeEnvVars(runtime) {
|
|
17
|
+
const config = runtime.env.config ?? {};
|
|
18
|
+
const out = {
|
|
19
|
+
...runtime.env.envVars,
|
|
20
|
+
};
|
|
21
|
+
if (runtime.kind !== 'local' && runtime.kind !== 'docker') {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
if (!config.builtinDb) {
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const connection = await resolveBuiltinDbConnection(runtime);
|
|
28
|
+
put(out, 'DB_DIALECT', connection.dbDialect);
|
|
29
|
+
put(out, 'DB_HOST', connection.dbHost);
|
|
30
|
+
put(out, 'DB_PORT', connection.dbPort);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -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.
|
|
@@ -60,12 +68,59 @@ function toGeneratedParameter(parameter, usedFlagNames) {
|
|
|
60
68
|
required: parameter.required,
|
|
61
69
|
description: parameter.description,
|
|
62
70
|
type: inferParameterType(parameter.schema),
|
|
71
|
+
format: parameter.schema?.format,
|
|
63
72
|
isArray: parameter.schema?.type === 'array',
|
|
73
|
+
isFile: parameter.schema?.type === 'string' && parameter.schema?.format === 'binary',
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
76
|
function getJsonRequestSchema(requestBody) {
|
|
67
77
|
return requestBody?.content?.['application/json']?.schema;
|
|
68
78
|
}
|
|
79
|
+
function getMultipartRequestSchema(requestBody) {
|
|
80
|
+
return requestBody?.content?.['multipart/form-data']?.schema;
|
|
81
|
+
}
|
|
82
|
+
function getRequestContentType(requestBody) {
|
|
83
|
+
if (!requestBody || '$ref' in requestBody) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (requestBody.content?.['multipart/form-data']) {
|
|
87
|
+
return 'multipart/form-data';
|
|
88
|
+
}
|
|
89
|
+
if (requestBody.content?.['application/json']) {
|
|
90
|
+
return 'application/json';
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function getRequestSchema(requestBody) {
|
|
95
|
+
return getMultipartRequestSchema(requestBody) ?? getJsonRequestSchema(requestBody);
|
|
96
|
+
}
|
|
97
|
+
function isBinarySchema(schema) {
|
|
98
|
+
if (!schema || typeof schema !== 'object') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === 'string' && schema.format === 'binary') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return [...(schema.oneOf ?? []), ...(schema.anyOf ?? []), ...(schema.allOf ?? [])].some(isBinarySchema);
|
|
105
|
+
}
|
|
106
|
+
function getResponseType(operation) {
|
|
107
|
+
for (const response of Object.values(operation.responses ?? {})) {
|
|
108
|
+
const content = response?.content ?? {};
|
|
109
|
+
const mediaTypes = Object.keys(content);
|
|
110
|
+
const hasJson = mediaTypes.some((mediaType) => mediaType.includes('json'));
|
|
111
|
+
if (hasJson) {
|
|
112
|
+
return 'json';
|
|
113
|
+
}
|
|
114
|
+
const hasBinary = mediaTypes.some((mediaType) => {
|
|
115
|
+
const schema = content[mediaType]?.schema;
|
|
116
|
+
return mediaType === 'application/octet-stream' || mediaType.includes('zip') || isBinarySchema(schema);
|
|
117
|
+
});
|
|
118
|
+
if (hasBinary) {
|
|
119
|
+
return 'binary';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
69
124
|
function normalizeCompositeSchema(schema) {
|
|
70
125
|
if (!schema || typeof schema !== 'object') {
|
|
71
126
|
return schema;
|
|
@@ -139,7 +194,7 @@ function describeSchemaShape(schema, options = {}) {
|
|
|
139
194
|
return type;
|
|
140
195
|
}
|
|
141
196
|
function extractBodyParameters(requestBody, usedFlagNames) {
|
|
142
|
-
const schema =
|
|
197
|
+
const schema = getRequestSchema(requestBody);
|
|
143
198
|
const properties = normalizeCompositeSchema(schema)?.properties;
|
|
144
199
|
const required = new Set(normalizeCompositeSchema(schema)?.required ?? []);
|
|
145
200
|
return Object.entries(properties ?? {}).map(([name, propertySchema]) => ({
|
|
@@ -149,7 +204,9 @@ function extractBodyParameters(requestBody, usedFlagNames) {
|
|
|
149
204
|
required: required.has(name),
|
|
150
205
|
description: propertySchema.description,
|
|
151
206
|
type: inferParameterType(propertySchema),
|
|
207
|
+
format: propertySchema.format,
|
|
152
208
|
isArray: propertySchema.type === 'array',
|
|
209
|
+
isFile: propertySchema.type === 'string' && propertySchema.format === 'binary',
|
|
153
210
|
jsonEncoded: propertySchema.type === 'object' || propertySchema.type === 'array',
|
|
154
211
|
jsonShape: describeSchemaShape(propertySchema),
|
|
155
212
|
}));
|
|
@@ -179,6 +236,9 @@ function formatFlagExample(parameter) {
|
|
|
179
236
|
if (parameter.type === 'boolean') {
|
|
180
237
|
return `--${parameter.flagName}`;
|
|
181
238
|
}
|
|
239
|
+
if (parameter.isFile) {
|
|
240
|
+
return `--${parameter.flagName} <path>`;
|
|
241
|
+
}
|
|
182
242
|
if (parameter.type === 'object' || parameter.jsonEncoded) {
|
|
183
243
|
if (parameter.type === 'array' || parameter.isArray) {
|
|
184
244
|
return `--${parameter.flagName} '[]'`;
|
|
@@ -216,12 +276,13 @@ export function buildExamples(commandId, operation) {
|
|
|
216
276
|
const requiredParameters = operation.parameters.filter((parameter) => parameter.required);
|
|
217
277
|
const requiredFlags = requiredParameters.map(formatFlagExample);
|
|
218
278
|
const requiredNonBodyFlags = requiredParameters.filter((parameter) => parameter.in !== 'body').map(formatFlagExample);
|
|
219
|
-
const
|
|
279
|
+
const outputFlag = operation.responseType === 'binary' ? ' --output <path>' : '';
|
|
280
|
+
const examples = [`nb api ${commandId}${requiredFlags.length ? ` ${requiredFlags.join(' ')}` : ''}${outputFlag}`];
|
|
220
281
|
const firstOptional = operation.parameters.find((parameter) => !parameter.required);
|
|
221
282
|
if (firstOptional) {
|
|
222
|
-
examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}
|
|
283
|
+
examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}`.trim());
|
|
223
284
|
}
|
|
224
|
-
if (operation.hasBody) {
|
|
285
|
+
if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
|
|
225
286
|
const prefix = `nb api ${commandId}${requiredNonBodyFlags.length ? ` ${requiredNonBodyFlags.join(' ')}` : ''}`;
|
|
226
287
|
examples.push(`${prefix} --body '${buildSampleBody(operation.parameters)}'`);
|
|
227
288
|
}
|
|
@@ -248,9 +309,17 @@ function buildDescription(operation) {
|
|
|
248
309
|
}
|
|
249
310
|
if (operation.hasBody) {
|
|
250
311
|
const bodyFlags = operation.parameters.filter((parameter) => parameter.in === 'body').map((parameter) => `--${parameter.flagName}`);
|
|
251
|
-
|
|
252
|
-
? `Request body:
|
|
253
|
-
|
|
312
|
+
if (operation.requestContentType === 'multipart/form-data') {
|
|
313
|
+
sections.push(bodyFlags.length ? `Request body: multipart form fields (${bodyFlags.join(', ')}).` : 'Request body: multipart form data.');
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
sections.push(bodyFlags.length
|
|
317
|
+
? `Request body: use body field flags (${bodyFlags.join(', ')}) or pass raw JSON via \`--body\` / \`--body-file\`.`
|
|
318
|
+
: 'Request body: JSON via `--body` or `--body-file`.');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (operation.responseType === 'binary') {
|
|
322
|
+
sections.push('Response body: binary download written to `--output`.');
|
|
254
323
|
}
|
|
255
324
|
return sections.join('\n\n');
|
|
256
325
|
}
|
|
@@ -357,6 +426,8 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
357
426
|
const bodyParameters = extractBodyParameters(operation.requestBody, usedFlagNames);
|
|
358
427
|
const allParameters = [...parameters, ...bodyParameters];
|
|
359
428
|
const hasBody = Boolean(operation.requestBody && !('$ref' in operation.requestBody));
|
|
429
|
+
const requestContentType = getRequestContentType(operation.requestBody);
|
|
430
|
+
const responseType = getResponseType(operation);
|
|
360
431
|
const moduleDisplayName = moduleConfig.name ?? moduleKey;
|
|
361
432
|
const moduleDescription = moduleConfig.description;
|
|
362
433
|
const resourceDisplayName = resourceConfig?.name ?? resourceKey;
|
|
@@ -366,9 +437,11 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
366
437
|
description: operation.description,
|
|
367
438
|
});
|
|
368
439
|
const resourceSegments = toResourceSegments(pathTemplate);
|
|
369
|
-
const mappedResourceSegments = resourceSegments.length && resourceConfig?.
|
|
370
|
-
? [
|
|
371
|
-
: resourceSegments
|
|
440
|
+
const mappedResourceSegments = resourceSegments.length && resourceConfig?.segments?.length
|
|
441
|
+
? [...resourceConfig.segments.map(toKebabCase), ...resourceSegments.slice(1)]
|
|
442
|
+
: resourceSegments.length && resourceConfig?.name
|
|
443
|
+
? [toKebabCase(resourceConfig.name), ...resourceSegments.slice(1)]
|
|
444
|
+
: resourceSegments;
|
|
372
445
|
const segments = [
|
|
373
446
|
...(resourceConfig?.topLevel ? [] : [toKebabCase(moduleDisplayName)]),
|
|
374
447
|
...mappedResourceSegments,
|
|
@@ -397,15 +470,21 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
397
470
|
tags: operation.tags,
|
|
398
471
|
description: operationText.description,
|
|
399
472
|
hasBody,
|
|
473
|
+
requestContentType,
|
|
474
|
+
responseType,
|
|
400
475
|
parameters: allParameters,
|
|
401
476
|
}),
|
|
402
477
|
examples: buildExamples(segments.join(' '), {
|
|
403
478
|
parameters: allParameters,
|
|
404
479
|
hasBody,
|
|
480
|
+
requestContentType,
|
|
481
|
+
responseType,
|
|
405
482
|
}),
|
|
406
483
|
parameters: allParameters,
|
|
407
484
|
hasBody,
|
|
408
485
|
bodyRequired: operation.requestBody && !('$ref' in operation.requestBody) ? operation.requestBody.required : undefined,
|
|
486
|
+
requestContentType,
|
|
487
|
+
responseType,
|
|
409
488
|
});
|
|
410
489
|
}
|
|
411
490
|
const schemaHash = createHash('sha1').update(JSON.stringify(document)).digest('hex').slice(0, 8);
|