@nocobase/cli 2.3.0-alpha.1 → 2.3.0-beta.10
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/assets/env-proxy/nginx/snippets/maps-http.conf +5 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +13 -4
- package/dist/commands/api/resource/create.js +11 -2
- package/dist/lib/api-client.js +7 -0
- package/dist/lib/browser.js +29 -0
- package/dist/lib/env-proxy.js +74 -10
- package/dist/lib/generated-command.js +81 -0
- package/dist/lib/naming.js +9 -0
- package/dist/lib/plugin-import.js +30 -8
- package/dist/lib/resource-command.js +18 -2
- package/dist/lib/resource-request.js +8 -0
- package/dist/lib/runtime-generator.js +28 -1
- package/nocobase-ctl.config.json +111 -0
- package/package.json +2 -2
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
auth_request /_nocobase_legacy_file_auth;
|
|
2
|
+
auth_request_set $legacy_auth_set_cookie $upstream_http_set_cookie;
|
|
3
|
+
|
|
4
|
+
add_header Cache-Control "private, no-store" always;
|
|
5
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
6
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
2
7
|
add_header X-Content-Type-Options "nosniff" always;
|
|
3
8
|
|
|
4
9
|
access_log off;
|
|
5
10
|
autoindex off;
|
|
6
11
|
|
|
7
12
|
# Force potentially renderable uploaded files to download.
|
|
8
|
-
location ~* \.(?:htm|html|svg|svgz|xhtml)$ {
|
|
9
|
-
add_header Cache-Control "
|
|
13
|
+
location ~* \.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$ {
|
|
14
|
+
add_header Cache-Control "private, no-store" always;
|
|
15
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
16
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
10
17
|
add_header X-Content-Type-Options "nosniff" always;
|
|
11
18
|
add_header Content-Disposition "attachment" always;
|
|
12
19
|
}
|
|
@@ -15,7 +22,9 @@ location ~* \.(?:htm|html|svg|svgz|xhtml)$ {
|
|
|
15
22
|
location ~* \.md$ {
|
|
16
23
|
default_type text/markdown;
|
|
17
24
|
|
|
18
|
-
add_header Cache-Control "
|
|
25
|
+
add_header Cache-Control "private, no-store" always;
|
|
26
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
27
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
19
28
|
add_header X-Content-Type-Options "nosniff" always;
|
|
20
29
|
add_header Content-Disposition "inline" always;
|
|
21
30
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
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 { Command } from '@oclif/core';
|
|
2
10
|
import { buildCreateArgs, createFlags, runResourceCommand } from '../../../lib/resource-command.js';
|
|
3
11
|
export default class ResourceCreate extends Command {
|
|
4
|
-
static summary = 'Create
|
|
5
|
-
static description = 'Create
|
|
12
|
+
static summary = 'Create one or more records in a resource';
|
|
13
|
+
static description = 'Create records in a generic resource. Pass record content through --values as a JSON object, or as a JSON array of objects to create multiple records in a single request.';
|
|
6
14
|
static examples = [
|
|
7
15
|
`<%= config.bin %> <%= command.id %> --resource users --values '{"nickname":"Ada"}'`,
|
|
16
|
+
`<%= config.bin %> <%= command.id %> --resource users --values '[{"nickname":"Ada"},{"nickname":"Grace"}]'`,
|
|
8
17
|
`<%= config.bin %> <%= command.id %> --resource posts.comments --source-id 1 --values '{"content":"Hello"}'`,
|
|
9
18
|
];
|
|
10
19
|
static flags = createFlags;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -199,6 +199,13 @@ async function createMultipartBody(flags, operation) {
|
|
|
199
199
|
if (value === undefined) {
|
|
200
200
|
continue;
|
|
201
201
|
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
for (const item of value) {
|
|
204
|
+
formData.append(parameter.name, typeof item === 'object' ? JSON.stringify(item) : String(item));
|
|
205
|
+
}
|
|
206
|
+
hasValues = hasValues || value.length > 0;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
202
209
|
formData.append(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
203
210
|
hasValues = true;
|
|
204
211
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { spawn } from 'node:child_process';
|
|
10
|
+
export async function openUrlInDefaultBrowser(url) {
|
|
11
|
+
const [command, args, options] = process.platform === 'darwin'
|
|
12
|
+
? ['open', [url], { detached: true, stdio: 'ignore' }]
|
|
13
|
+
: process.platform === 'win32'
|
|
14
|
+
? ['cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }]
|
|
15
|
+
: ['xdg-open', [url], { detached: true, stdio: 'ignore' }];
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
try {
|
|
18
|
+
const child = spawn(command, args, options);
|
|
19
|
+
child.once('error', () => resolve(false));
|
|
20
|
+
child.once('spawn', () => {
|
|
21
|
+
child.unref();
|
|
22
|
+
resolve(true);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
resolve(false);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
package/dist/lib/env-proxy.js
CHANGED
|
@@ -510,6 +510,19 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
510
510
|
` include ${context.snippetsDir}/mime-types.conf;`,
|
|
511
511
|
` include ${context.snippetsDir}/gzip.conf;`,
|
|
512
512
|
'',
|
|
513
|
+
' location = /_nocobase_legacy_file_auth {',
|
|
514
|
+
' internal;',
|
|
515
|
+
` proxy_pass ${context.backendUrl}${context.apiBasePath}auth:checkLegacyFileAccess;`,
|
|
516
|
+
' proxy_pass_request_body off;',
|
|
517
|
+
' proxy_set_header Content-Length "";',
|
|
518
|
+
' proxy_set_header Cookie $http_cookie;',
|
|
519
|
+
' proxy_set_header Authorization $http_authorization;',
|
|
520
|
+
' proxy_set_header X-App $legacy_file_app;',
|
|
521
|
+
' proxy_set_header X-Original-URI $request_uri;',
|
|
522
|
+
' proxy_set_header Host $final_host;',
|
|
523
|
+
' proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;',
|
|
524
|
+
' }',
|
|
525
|
+
'',
|
|
513
526
|
` location ${context.appPublicPath}storage/uploads/ {`,
|
|
514
527
|
` alias ${context.uploadsDir}/;`,
|
|
515
528
|
` include ${context.snippetsDir}/uploads-location.conf;`,
|
|
@@ -1010,20 +1023,41 @@ function renderNginxLocationTemplate(context) {
|
|
|
1010
1023
|
const proxyPassBlock = buildNginxProxyPassBlock(context.proxyHost, context.apiPort);
|
|
1011
1024
|
const wsProxyPassTarget = `http://${context.proxyHost}:${context.apiPort}${context.wsPath}`;
|
|
1012
1025
|
const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
|
|
1013
|
-
return ` location
|
|
1026
|
+
return ` location = /_nocobase_legacy_file_auth {
|
|
1027
|
+
internal;
|
|
1028
|
+
proxy_pass http://${context.proxyHost}:${context.apiPort}${context.apiBasePath}auth:checkLegacyFileAccess;
|
|
1029
|
+
proxy_pass_request_body off;
|
|
1030
|
+
proxy_set_header Content-Length "";
|
|
1031
|
+
proxy_set_header Cookie $http_cookie;
|
|
1032
|
+
proxy_set_header Authorization $http_authorization;
|
|
1033
|
+
proxy_set_header X-App $legacy_file_app;
|
|
1034
|
+
proxy_set_header X-Original-URI $request_uri;
|
|
1035
|
+
proxy_set_header Host $final_host;
|
|
1036
|
+
proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
location ~* ^${context.appPublicPath}storage/uploads/(.*\\.md)$ {
|
|
1014
1040
|
alias ${context.uploadsPath}/$1;
|
|
1015
1041
|
default_type text/markdown;
|
|
1016
|
-
|
|
1042
|
+
auth_request /_nocobase_legacy_file_auth;
|
|
1043
|
+
auth_request_set $legacy_auth_set_cookie $upstream_http_set_cookie;
|
|
1044
|
+
add_header Cache-Control "private, no-store" always;
|
|
1045
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
1017
1046
|
add_header Content-Disposition "inline";
|
|
1047
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
1018
1048
|
add_header X-Content-Type-Options "nosniff" always;
|
|
1019
1049
|
access_log off;
|
|
1020
1050
|
autoindex off;
|
|
1021
1051
|
}
|
|
1022
1052
|
|
|
1023
|
-
location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|svg|svgz|xhtml|
|
|
1053
|
+
location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
|
|
1024
1054
|
alias ${context.uploadsPath}/$1;
|
|
1025
|
-
|
|
1055
|
+
auth_request /_nocobase_legacy_file_auth;
|
|
1056
|
+
auth_request_set $legacy_auth_set_cookie $upstream_http_set_cookie;
|
|
1057
|
+
add_header Cache-Control "private, no-store" always;
|
|
1058
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
1026
1059
|
add_header Content-Disposition "attachment" always;
|
|
1060
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
1027
1061
|
add_header X-Content-Type-Options "nosniff" always;
|
|
1028
1062
|
access_log off;
|
|
1029
1063
|
autoindex off;
|
|
@@ -1031,7 +1065,11 @@ function renderNginxLocationTemplate(context) {
|
|
|
1031
1065
|
|
|
1032
1066
|
location ${context.appPublicPath}storage/uploads/ {
|
|
1033
1067
|
alias ${context.uploadsPath}/;
|
|
1034
|
-
|
|
1068
|
+
auth_request /_nocobase_legacy_file_auth;
|
|
1069
|
+
auth_request_set $legacy_auth_set_cookie $upstream_http_set_cookie;
|
|
1070
|
+
add_header Cache-Control "private, no-store" always;
|
|
1071
|
+
add_header Set-Cookie $legacy_auth_set_cookie always;
|
|
1072
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
1035
1073
|
add_header X-Content-Type-Options "nosniff" always;
|
|
1036
1074
|
access_log off;
|
|
1037
1075
|
autoindex off;
|
|
@@ -1080,7 +1118,12 @@ function renderNginxLocationTemplate(context) {
|
|
|
1080
1118
|
`;
|
|
1081
1119
|
}
|
|
1082
1120
|
function renderLegacyEnvProxyAppTemplate(context) {
|
|
1083
|
-
return `
|
|
1121
|
+
return `map $request_uri $legacy_file_app {
|
|
1122
|
+
default "";
|
|
1123
|
+
~[?&]__appName=(?<legacy_file_app_name>[A-Za-z0-9_-]+)(?:&|$) $legacy_file_app_name;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
server {
|
|
1084
1127
|
listen 80;
|
|
1085
1128
|
server_name _;
|
|
1086
1129
|
client_max_body_size 0;
|
|
@@ -1153,10 +1196,31 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1153
1196
|
` encode zstd gzip${rootRedirectBlock}${appPublicPathRedirectBlock}${modernClientRedirectBlock}${shorthandModernClientRedirectBlock}`,
|
|
1154
1197
|
'',
|
|
1155
1198
|
` handle_path ${uploadsPathMatcher} {`,
|
|
1156
|
-
|
|
1157
|
-
'
|
|
1158
|
-
|
|
1159
|
-
|
|
1199
|
+
' route {',
|
|
1200
|
+
' request_header -X-NocoBase-Auth-Set-Cookie',
|
|
1201
|
+
` forward_auth ${context.proxyHost}:${context.apiPort} {`,
|
|
1202
|
+
` uri ${context.apiBasePath}auth:checkLegacyFileAccess`,
|
|
1203
|
+
' header_up X-App {query.__appName}',
|
|
1204
|
+
' copy_headers Set-Cookie>X-NocoBase-Auth-Set-Cookie',
|
|
1205
|
+
' }',
|
|
1206
|
+
'',
|
|
1207
|
+
' @refreshedAuth header X-NocoBase-Auth-Set-Cookie *',
|
|
1208
|
+
' header @refreshedAuth Set-Cookie {header.X-NocoBase-Auth-Set-Cookie}',
|
|
1209
|
+
' header Cache-Control "private, no-store"',
|
|
1210
|
+
' header Content-Security-Policy sandbox',
|
|
1211
|
+
' header X-Content-Type-Options nosniff',
|
|
1212
|
+
' header Content-Disposition inline',
|
|
1213
|
+
'',
|
|
1214
|
+
' @activeUploadedContent path_regexp activeUploadedContent (?i)\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$',
|
|
1215
|
+
' header @activeUploadedContent Content-Disposition attachment',
|
|
1216
|
+
' @download query download=1',
|
|
1217
|
+
' header @download Content-Disposition attachment',
|
|
1218
|
+
' @markdown path_regexp markdown (?i)\\.md$',
|
|
1219
|
+
' header @markdown Content-Type text/markdown',
|
|
1220
|
+
'',
|
|
1221
|
+
` root * ${context.uploadsPath}`,
|
|
1222
|
+
' file_server',
|
|
1223
|
+
' }',
|
|
1160
1224
|
' }',
|
|
1161
1225
|
'',
|
|
1162
1226
|
` handle_path ${distPathMatcher} {`,
|
|
@@ -15,12 +15,20 @@
|
|
|
15
15
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
16
|
*/
|
|
17
17
|
import { Command, Flags } from '@oclif/core';
|
|
18
|
+
import { getCurrentEnvName, getEnv } from './auth-store.js';
|
|
18
19
|
import { executeApiRequest } from './api-client.js';
|
|
20
|
+
import { resolveAppUrlFromApiBaseUrl } from '../commands/env/shared.js';
|
|
19
21
|
import { findApiCommandCompatViolation, formatApiCommandCompatViolation } from './api-command-compat.js';
|
|
22
|
+
import { openUrlInDefaultBrowser } from './browser.js';
|
|
20
23
|
import { ensureCrossEnvConfirmed } from './env-guard.js';
|
|
21
24
|
import { applyPostProcessor } from './post-processors.js';
|
|
22
25
|
import { readInstalledManagedSkillsVersion } from './skills-manager.js';
|
|
23
26
|
import { registerPostProcessors } from '../post-processors/index.js';
|
|
27
|
+
const UI_OPERATION_QUERY_KEY = '_operation_';
|
|
28
|
+
const UI_OPERATION_VERSION = 1;
|
|
29
|
+
function encodeUIOperation(operation) {
|
|
30
|
+
return Buffer.from(JSON.stringify(operation), 'utf8').toString('base64url');
|
|
31
|
+
}
|
|
24
32
|
function buildParameterFlag(parameter, options) {
|
|
25
33
|
const hints = [parameter.in];
|
|
26
34
|
if (parameter.isFile) {
|
|
@@ -104,6 +112,13 @@ export function createGeneratedFlags(operation) {
|
|
|
104
112
|
required: true,
|
|
105
113
|
});
|
|
106
114
|
}
|
|
115
|
+
if (operation.ui) {
|
|
116
|
+
flags.ui = Flags.boolean({
|
|
117
|
+
description: 'Open the corresponding page in the NocoBase UI',
|
|
118
|
+
default: false,
|
|
119
|
+
helpGroup: 'Global',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
107
122
|
flags['api-base-url'] = Flags.string({
|
|
108
123
|
description: 'NocoBase API base URL, for example http://localhost:13000/api',
|
|
109
124
|
helpGroup: 'Global',
|
|
@@ -142,6 +157,68 @@ export function createGeneratedFlags(operation) {
|
|
|
142
157
|
});
|
|
143
158
|
return flags;
|
|
144
159
|
}
|
|
160
|
+
function hasFlagValue(value) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.length > 0;
|
|
163
|
+
}
|
|
164
|
+
return value !== undefined && value !== '';
|
|
165
|
+
}
|
|
166
|
+
function listProvidedBodyFlags(flags, operation) {
|
|
167
|
+
const rawBodyFlags = ['body', 'body-file'].filter((flagName) => hasFlagValue(flags[flagName])).map((flagName) => `--${flagName}`);
|
|
168
|
+
const uiParameterNames = new Set(operation.ui?.parameters ?? []);
|
|
169
|
+
const bodyFieldFlags = operation.parameters
|
|
170
|
+
.filter((parameter) => parameter.in === 'body' && !uiParameterNames.has(parameter.name) && hasFlagValue(flags[parameter.flagName]))
|
|
171
|
+
.map((parameter) => `--${parameter.flagName}`);
|
|
172
|
+
return [...rawBodyFlags, ...bodyFieldFlags];
|
|
173
|
+
}
|
|
174
|
+
async function resolveUiAppUrl(flags) {
|
|
175
|
+
const apiBaseUrl = typeof flags['api-base-url'] === 'string' ? flags['api-base-url'] : undefined;
|
|
176
|
+
if (apiBaseUrl) {
|
|
177
|
+
return resolveAppUrlFromApiBaseUrl(apiBaseUrl);
|
|
178
|
+
}
|
|
179
|
+
const requestedEnv = typeof flags.env === 'string' ? flags.env : undefined;
|
|
180
|
+
const envName = requestedEnv ?? (await getCurrentEnvName());
|
|
181
|
+
const env = await getEnv(envName);
|
|
182
|
+
if (!env?.baseUrl) {
|
|
183
|
+
throw new Error(env
|
|
184
|
+
? `Env "${envName}" is missing a base URL. Use --api-base-url or update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\` first.`
|
|
185
|
+
: `Env "${envName}" is not configured. Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`);
|
|
186
|
+
}
|
|
187
|
+
return resolveAppUrlFromApiBaseUrl(env.baseUrl);
|
|
188
|
+
}
|
|
189
|
+
function buildUiOperationUrl(appUrl, path, encodedOperation) {
|
|
190
|
+
const url = new URL(appUrl);
|
|
191
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/${path}`;
|
|
192
|
+
url.searchParams.set(UI_OPERATION_QUERY_KEY, encodedOperation);
|
|
193
|
+
return url.toString();
|
|
194
|
+
}
|
|
195
|
+
async function openUiOperation(command, operation, flags) {
|
|
196
|
+
const { operationId, ui } = operation;
|
|
197
|
+
if (!ui || !operationId) {
|
|
198
|
+
command.error('This API operation does not support --ui.');
|
|
199
|
+
}
|
|
200
|
+
const bodyFlags = listProvidedBodyFlags(flags, operation);
|
|
201
|
+
if (bodyFlags.length) {
|
|
202
|
+
command.error('--ui cannot be combined with API request body flags. Remove --ui to submit through the API, or remove the body flags to open the UI.');
|
|
203
|
+
}
|
|
204
|
+
const uiParameterNames = new Set(ui.parameters ?? []);
|
|
205
|
+
const params = Object.fromEntries(operation.parameters
|
|
206
|
+
.filter((parameter) => uiParameterNames.has(parameter.name) && hasFlagValue(flags[parameter.flagName]))
|
|
207
|
+
.map((parameter) => [parameter.name, flags[parameter.flagName]]));
|
|
208
|
+
const uiOperation = {
|
|
209
|
+
v: UI_OPERATION_VERSION,
|
|
210
|
+
operationId,
|
|
211
|
+
...(Object.keys(params).length ? { params } : {}),
|
|
212
|
+
};
|
|
213
|
+
const encodedOperation = encodeUIOperation(uiOperation);
|
|
214
|
+
const appUrl = await resolveUiAppUrl(flags);
|
|
215
|
+
const targetUrl = buildUiOperationUrl(appUrl, ui.path, encodedOperation);
|
|
216
|
+
const opened = await openUrlInDefaultBrowser(targetUrl);
|
|
217
|
+
command.log(targetUrl);
|
|
218
|
+
if (!opened) {
|
|
219
|
+
command.warn('Could not open the default browser. Copy the URL above to continue.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
145
222
|
export class GeneratedApiCommand extends Command {
|
|
146
223
|
static operation;
|
|
147
224
|
static runtimeVersion;
|
|
@@ -169,6 +246,10 @@ export class GeneratedApiCommand extends Command {
|
|
|
169
246
|
if (compatViolation) {
|
|
170
247
|
this.error(formatApiCommandCompatViolation(compatViolation));
|
|
171
248
|
}
|
|
249
|
+
if (flags.ui) {
|
|
250
|
+
await openUiOperation(this, ctor.operation, flags);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
172
253
|
const response = await executeApiRequest({
|
|
173
254
|
cliVersion,
|
|
174
255
|
skillsVersion,
|
package/dist/lib/naming.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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 path from 'node:path';
|
|
2
10
|
export function toKebabCase(value) {
|
|
3
11
|
return value
|
|
12
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
4
13
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
5
14
|
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
6
15
|
.replace(/-+/g, '-')
|
|
@@ -207,6 +207,29 @@ async function openPluginSource(source, npmRegistry, runFn = run) {
|
|
|
207
207
|
}
|
|
208
208
|
return await packNpmPluginSource(source, npmRegistry, runFn);
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Locate the directory holding the plugin's `package.json` inside a freshly extracted archive.
|
|
212
|
+
*
|
|
213
|
+
* Archives reach us in two shapes: npm-style tarballs (`npm pack`, registry downloads) wrap everything in a single
|
|
214
|
+
* top-level directory — conventionally `package/` — while NocoBase's own `yarn build <plugin> --tar` writes entries at
|
|
215
|
+
* the archive root. Extracting with a fixed `strip: 1` would silently discard the root-level files of the latter, so we
|
|
216
|
+
* extract verbatim and pick the package root here instead. When neither shape matches, return the extract root and let
|
|
217
|
+
* `readPluginMetadata` report the missing `package.json`.
|
|
218
|
+
*/
|
|
219
|
+
async function resolveArchivePackageRoot(extractRoot) {
|
|
220
|
+
if (await pathExists(path.join(extractRoot, 'package.json'))) {
|
|
221
|
+
return extractRoot;
|
|
222
|
+
}
|
|
223
|
+
const entries = await fsp.readdir(extractRoot, { withFileTypes: true });
|
|
224
|
+
const directories = entries.filter((entry) => entry.isDirectory());
|
|
225
|
+
if (directories.length === 1) {
|
|
226
|
+
const nestedRoot = path.join(extractRoot, directories[0].name);
|
|
227
|
+
if (await pathExists(path.join(nestedRoot, 'package.json'))) {
|
|
228
|
+
return nestedRoot;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return extractRoot;
|
|
232
|
+
}
|
|
210
233
|
async function readPluginMetadata(extractRoot, sourceLabel) {
|
|
211
234
|
const packageJsonPath = path.join(extractRoot, 'package.json');
|
|
212
235
|
let content;
|
|
@@ -241,22 +264,21 @@ export async function importPluginSource(source, options = {}) {
|
|
|
241
264
|
await fsp.mkdir(storagePluginsPath, { recursive: true });
|
|
242
265
|
const archive = await openPluginSource(normalizedSource, options.npmRegistry, options.runFn);
|
|
243
266
|
const stageDir = await fsp.mkdtemp(path.join(storagePluginsPath, '.nb-plugin-import-'));
|
|
244
|
-
let stageMoved = false;
|
|
245
267
|
try {
|
|
246
268
|
try {
|
|
247
|
-
await pipeline(archive.stream, createGunzip(), tar.extract({ cwd: stageDir
|
|
269
|
+
await pipeline(archive.stream, createGunzip(), tar.extract({ cwd: stageDir }));
|
|
248
270
|
}
|
|
249
271
|
catch (error) {
|
|
250
272
|
const message = error instanceof Error ? error.message : String(error);
|
|
251
273
|
throw new Error(`Failed to extract plugin archive from ${archive.source}: ${message}`);
|
|
252
274
|
}
|
|
253
|
-
const
|
|
275
|
+
const packageRoot = await resolveArchivePackageRoot(stageDir);
|
|
276
|
+
const { packageName, packageVersion } = await readPluginMetadata(packageRoot, archive.source);
|
|
254
277
|
const outputDir = resolvePluginOutputDir(storagePluginsPath, packageName);
|
|
255
278
|
const action = (await pathExists(outputDir)) ? 'updated' : 'installed';
|
|
256
279
|
await fsp.mkdir(path.dirname(outputDir), { recursive: true });
|
|
257
280
|
await fsp.rm(outputDir, { recursive: true, force: true });
|
|
258
|
-
await fsp.rename(
|
|
259
|
-
stageMoved = true;
|
|
281
|
+
await fsp.rename(packageRoot, outputDir);
|
|
260
282
|
return {
|
|
261
283
|
action,
|
|
262
284
|
packageName,
|
|
@@ -268,9 +290,9 @@ export async function importPluginSource(source, options = {}) {
|
|
|
268
290
|
};
|
|
269
291
|
}
|
|
270
292
|
finally {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
293
|
+
// Always removes the staging directory: it is either untouched (failure), or an empty wrapper left behind after the
|
|
294
|
+
// nested package root was renamed out of it.
|
|
295
|
+
await fsp.rm(stageDir, { recursive: true, force: true });
|
|
274
296
|
await archive.cleanup();
|
|
275
297
|
}
|
|
276
298
|
}
|
|
@@ -41,6 +41,22 @@ function parseObjectFlag(value, flagName) {
|
|
|
41
41
|
}
|
|
42
42
|
return parsed;
|
|
43
43
|
}
|
|
44
|
+
function parseValuesFlag(value, flagName) {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseJson(value, flagName);
|
|
49
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
50
|
+
throw new Error(`--${flagName} must be a JSON object, or a JSON array of objects to create multiple records`);
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(parsed)) {
|
|
53
|
+
const invalidIndex = parsed.findIndex((item) => !item || Array.isArray(item) || typeof item !== 'object');
|
|
54
|
+
if (invalidIndex !== -1) {
|
|
55
|
+
throw new Error(`--${flagName} array items must all be JSON objects, but item ${invalidIndex} is not`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
44
60
|
function parseJsonArrayFlag(value, flagName) {
|
|
45
61
|
if (value === undefined) {
|
|
46
62
|
return undefined;
|
|
@@ -187,7 +203,7 @@ export const createFlags = {
|
|
|
187
203
|
...resourceBaseFlags,
|
|
188
204
|
...resourceAssociationFlags,
|
|
189
205
|
values: Flags.string({
|
|
190
|
-
description: 'Record values used by create as a JSON object.',
|
|
206
|
+
description: 'Record values used by create as a JSON object, or a JSON array of objects to create multiple records in one request.',
|
|
191
207
|
required: true,
|
|
192
208
|
}),
|
|
193
209
|
whitelist: Flags.string({
|
|
@@ -298,7 +314,7 @@ export function buildGetArgs(flags) {
|
|
|
298
314
|
export function buildCreateArgs(flags) {
|
|
299
315
|
return {
|
|
300
316
|
...pickSharedArgs(flags),
|
|
301
|
-
values:
|
|
317
|
+
values: parseValuesFlag(flags.values, 'values'),
|
|
302
318
|
whitelist: parseStringArrayFlags(flags.whitelist, 'whitelist'),
|
|
303
319
|
blacklist: parseStringArrayFlags(flags.blacklist, 'blacklist'),
|
|
304
320
|
};
|
|
@@ -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 { executeRawApiRequest } from './api-client.js';
|
|
2
10
|
function buildActionUrl(resource, action, sourceId) {
|
|
3
11
|
if (typeof sourceId === 'undefined' || sourceId === null || !resource.includes('.')) {
|
|
@@ -18,7 +18,31 @@ import { createHash } from 'node:crypto';
|
|
|
18
18
|
import { loadBuildConfig } from './build-config.js';
|
|
19
19
|
import { toKebabCase, toLogicalActionName, toLogicalResourceName, toResourceSegments } from './naming.js';
|
|
20
20
|
import { collectOperations } from './openapi.js';
|
|
21
|
-
const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file', 'yes']);
|
|
21
|
+
const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file', 'ui', 'yes']);
|
|
22
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
23
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string' && Boolean(item));
|
|
24
|
+
function getGeneratedUIOperation(operation, parameters) {
|
|
25
|
+
if (!operation.operationId) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const extension = operation['x-nocobase-cli-ui'];
|
|
29
|
+
if (!isRecord(extension) || typeof extension.path !== 'string') {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const path = extension.path.trim();
|
|
33
|
+
if (!path || path.startsWith('/') || /[?#]/.test(path) || /^[a-z][a-z\d+.-]*:/i.test(path)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const mappedParameters = extension.parameters === undefined ? [] : extension.parameters;
|
|
37
|
+
if (!isStringArray(mappedParameters) || new Set(mappedParameters).size !== mappedParameters.length) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const allowedParameters = new Set(parameters.map((parameter) => parameter.name));
|
|
41
|
+
if (!mappedParameters.every((parameter) => allowedParameters.has(parameter))) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return { path, parameters: mappedParameters };
|
|
45
|
+
}
|
|
22
46
|
function matchesPattern(value, pattern) {
|
|
23
47
|
if (!value) {
|
|
24
48
|
return false;
|
|
@@ -425,6 +449,7 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
425
449
|
const parameters = (operation.parameters ?? []).filter(isSupportedParameter).map((parameter) => toGeneratedParameter(parameter, usedFlagNames));
|
|
426
450
|
const bodyParameters = extractBodyParameters(operation.requestBody, usedFlagNames);
|
|
427
451
|
const allParameters = [...parameters, ...bodyParameters];
|
|
452
|
+
const ui = getGeneratedUIOperation(operation, allParameters);
|
|
428
453
|
const hasBody = Boolean(operation.requestBody && !('$ref' in operation.requestBody));
|
|
429
454
|
const requestContentType = getRequestContentType(operation.requestBody);
|
|
430
455
|
const responseType = getResponseType(operation);
|
|
@@ -456,6 +481,8 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
456
481
|
resourceDisplayName,
|
|
457
482
|
resourceDescription,
|
|
458
483
|
commandId: segments.join(' '),
|
|
484
|
+
operationId: operation.operationId,
|
|
485
|
+
ui,
|
|
459
486
|
method,
|
|
460
487
|
pathTemplate,
|
|
461
488
|
tags: operation.tags,
|
package/nocobase-ctl.config.json
CHANGED
|
@@ -202,6 +202,117 @@
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
},
|
|
205
|
+
"ai": {
|
|
206
|
+
"name": "ai",
|
|
207
|
+
"description": "Discover LLM providers and manage LLM services and AI employees.",
|
|
208
|
+
"include": true,
|
|
209
|
+
"resources": {
|
|
210
|
+
"includes": ["ai", "llmServices", "aiEmployees"],
|
|
211
|
+
"excludes": [],
|
|
212
|
+
"overrides": {
|
|
213
|
+
"ai": {
|
|
214
|
+
"name": "llm-providers",
|
|
215
|
+
"description": "Discover providers and models and test unsaved LLM settings.",
|
|
216
|
+
"topLevel": false,
|
|
217
|
+
"operations": {
|
|
218
|
+
"includes": [
|
|
219
|
+
"ai:listLLMProviders",
|
|
220
|
+
"ai:listProviderModels",
|
|
221
|
+
"ai:testFlight",
|
|
222
|
+
"ai:listModels",
|
|
223
|
+
"ai:listLLMServices"
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"llmServices": {
|
|
228
|
+
"name": "llm-services",
|
|
229
|
+
"description": "Manage saved LLM service configurations.",
|
|
230
|
+
"topLevel": false,
|
|
231
|
+
"operations": {
|
|
232
|
+
"includes": [
|
|
233
|
+
"llmServices:list",
|
|
234
|
+
"llmServices:get",
|
|
235
|
+
"llmServices:create",
|
|
236
|
+
"llmServices:update",
|
|
237
|
+
"llmServices:destroy"
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"aiEmployees": {
|
|
242
|
+
"name": "employees",
|
|
243
|
+
"description": "Manage AI employees.",
|
|
244
|
+
"topLevel": false,
|
|
245
|
+
"operations": {
|
|
246
|
+
"includes": [
|
|
247
|
+
"aiEmployees:list",
|
|
248
|
+
"aiEmployees:get",
|
|
249
|
+
"aiEmployees:create",
|
|
250
|
+
"aiEmployees:update",
|
|
251
|
+
"aiEmployees:destroy"
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"kb": {
|
|
259
|
+
"name": "kb",
|
|
260
|
+
"description": "Manage vector databases, knowledge bases, documents, vectorization, and retrieval tests.",
|
|
261
|
+
"include": true,
|
|
262
|
+
"resources": {
|
|
263
|
+
"includes": ["aiVectorDatabases", "aiKnowledgeBase", "aiKnowledgeBaseDocs"],
|
|
264
|
+
"excludes": [],
|
|
265
|
+
"overrides": {
|
|
266
|
+
"aiVectorDatabases": {
|
|
267
|
+
"name": "vector-databases",
|
|
268
|
+
"description": "Manage vector database connections.",
|
|
269
|
+
"topLevel": false,
|
|
270
|
+
"operations": {
|
|
271
|
+
"includes": [
|
|
272
|
+
"aiVectorDatabases:listProviders",
|
|
273
|
+
"aiVectorDatabases:testConnection",
|
|
274
|
+
"aiVectorDatabases:list",
|
|
275
|
+
"aiVectorDatabases:get",
|
|
276
|
+
"aiVectorDatabases:create",
|
|
277
|
+
"aiVectorDatabases:update",
|
|
278
|
+
"aiVectorDatabases:destroy"
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"aiKnowledgeBase": {
|
|
283
|
+
"name": "kb",
|
|
284
|
+
"segments": ["kb"],
|
|
285
|
+
"description": "Manage knowledge bases and retrieval tests.",
|
|
286
|
+
"topLevel": true,
|
|
287
|
+
"operations": {
|
|
288
|
+
"includes": [
|
|
289
|
+
"aiKnowledgeBase:list",
|
|
290
|
+
"aiKnowledgeBase:get",
|
|
291
|
+
"aiKnowledgeBase:create",
|
|
292
|
+
"aiKnowledgeBase:update",
|
|
293
|
+
"aiKnowledgeBase:destroy",
|
|
294
|
+
"aiKnowledgeBase:runHitTest",
|
|
295
|
+
"aiKnowledgeBase:listExternalVectorStoreProviders"
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"aiKnowledgeBaseDocs": {
|
|
300
|
+
"name": "documents",
|
|
301
|
+
"description": "Manage knowledge base documents and vectorization.",
|
|
302
|
+
"topLevel": false,
|
|
303
|
+
"operations": {
|
|
304
|
+
"includes": [
|
|
305
|
+
"aiKnowledgeBaseDocs:list",
|
|
306
|
+
"aiKnowledgeBaseDocs:get",
|
|
307
|
+
"aiKnowledgeBaseDocs:upload",
|
|
308
|
+
"aiKnowledgeBaseDocs:vectorization",
|
|
309
|
+
"aiKnowledgeBaseDocs:destroy"
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
205
316
|
"flow-engine": {
|
|
206
317
|
"name": "flow-engine",
|
|
207
318
|
"description": "Manage flow surface composition, configuration, layout, and mutation APIs.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.3.0-
|
|
3
|
+
"version": "2.3.0-beta.10",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
@@ -144,5 +144,5 @@
|
|
|
144
144
|
"type": "git",
|
|
145
145
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
146
146
|
},
|
|
147
|
-
"gitHead": "
|
|
147
|
+
"gitHead": "7aa3dce6cf3201752882f7012c72997898015d89"
|
|
148
148
|
}
|