@nocobase/cli 2.2.0-beta.15 → 2.2.0-beta.17

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.
@@ -1,12 +1,14 @@
1
1
  add_header Cache-Control "public" always;
2
+ add_header Content-Security-Policy "sandbox" always;
2
3
  add_header X-Content-Type-Options "nosniff" always;
3
4
 
4
5
  access_log off;
5
6
  autoindex off;
6
7
 
7
8
  # Force potentially renderable uploaded files to download.
8
- location ~* \.(?:htm|html|svg|svgz|xhtml)$ {
9
+ location ~* \.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$ {
9
10
  add_header Cache-Control "public" always;
11
+ add_header Content-Security-Policy "sandbox" always;
10
12
  add_header X-Content-Type-Options "nosniff" always;
11
13
  add_header Content-Disposition "attachment" always;
12
14
  }
@@ -16,6 +18,7 @@ location ~* \.md$ {
16
18
  default_type text/markdown;
17
19
 
18
20
  add_header Cache-Control "public" always;
21
+ add_header Content-Security-Policy "sandbox" always;
19
22
  add_header X-Content-Type-Options "nosniff" always;
20
23
  add_header Content-Disposition "inline" always;
21
24
  }
@@ -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
+ }
@@ -1015,15 +1015,17 @@ function renderNginxLocationTemplate(context) {
1015
1015
  default_type text/markdown;
1016
1016
  add_header Cache-Control "public";
1017
1017
  add_header Content-Disposition "inline";
1018
+ add_header Content-Security-Policy "sandbox" always;
1018
1019
  add_header X-Content-Type-Options "nosniff" always;
1019
1020
  access_log off;
1020
1021
  autoindex off;
1021
1022
  }
1022
1023
 
1023
- location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|svg|svgz|xhtml|pdf))$ {
1024
+ location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
1024
1025
  alias ${context.uploadsPath}/$1;
1025
1026
  add_header Cache-Control "public";
1026
1027
  add_header Content-Disposition "attachment" always;
1028
+ add_header Content-Security-Policy "sandbox" always;
1027
1029
  add_header X-Content-Type-Options "nosniff" always;
1028
1030
  access_log off;
1029
1031
  autoindex off;
@@ -1032,6 +1034,7 @@ function renderNginxLocationTemplate(context) {
1032
1034
  location ${context.appPublicPath}storage/uploads/ {
1033
1035
  alias ${context.uploadsPath}/;
1034
1036
  add_header Cache-Control "public";
1037
+ add_header Content-Security-Policy "sandbox" always;
1035
1038
  add_header X-Content-Type-Options "nosniff" always;
1036
1039
  access_log off;
1037
1040
  autoindex off;
@@ -1152,10 +1155,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1152
1155
  `${siteAddress} {`,
1153
1156
  ` encode zstd gzip${rootRedirectBlock}${appPublicPathRedirectBlock}${modernClientRedirectBlock}${shorthandModernClientRedirectBlock}`,
1154
1157
  '',
1158
+ ' @activeUploadedContent path_regexp activeUploadedContent (?i)\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$',
1159
+ '',
1155
1160
  ` handle_path ${uploadsPathMatcher} {`,
1156
1161
  ` root * ${context.uploadsPath}`,
1157
1162
  ' header Cache-Control public',
1163
+ ' header Content-Security-Policy sandbox',
1158
1164
  ' header X-Content-Type-Options nosniff',
1165
+ ' header @activeUploadedContent Content-Disposition attachment',
1159
1166
  ' file_server',
1160
1167
  ' }',
1161
1168
  '',
@@ -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,
@@ -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, '-')
@@ -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,
@@ -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.2.0-beta.15",
3
+ "version": "2.2.0-beta.17",
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": "4c471f6b7d26adc412b2cc1d91264572296ca873"
147
+ "gitHead": "32a74d4bdc538580581df7ac45f6ba9191fa2195"
148
148
  }