@nocobase/cli 2.2.0-beta.16 → 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.
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
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": "
|
|
147
|
+
"gitHead": "32a74d4bdc538580581df7ac45f6ba9191fa2195"
|
|
148
148
|
}
|