@nocobase/cli 3.0.0-alpha.3 → 3.0.0-alpha.4

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
+ }
@@ -605,17 +605,30 @@ function buildNginxManagedConfigBlock(context) {
605
605
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
606
606
  ].join('\n');
607
607
  }
608
- function buildNginxPortalRootPublicPath(appPublicPath) {
608
+ function buildPortalRootPublicPath(appPublicPath) {
609
609
  return appPublicPath === DEFAULT_APP_PUBLIC_PATH
610
610
  ? `/${PORTAL_CLIENT_PREFIX}/`
611
611
  : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
612
612
  }
613
613
  function buildNginxPortalLocationBlock(context) {
614
- const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
614
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
615
615
  const portalBasePathPattern = escapeRegExp(portalBasePath);
616
616
  return [
617
+ ` location = ${portalBasePath} {`,
618
+ ' absolute_redirect off;',
619
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
620
+ ' }',
621
+ '',
622
+ ` location = ${portalBasePath}/ {`,
623
+ ' absolute_redirect off;',
624
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
625
+ ' }',
626
+ '',
617
627
  ` location ^~ ${portalBasePath}/apps/ {`,
618
628
  ' absolute_redirect off;',
629
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {`,
630
+ ` return 302 ${context.v2PublicPath}apps/$subapp/$is_args$args;`,
631
+ ' }',
619
632
  '',
620
633
  ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
621
634
  ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
@@ -640,7 +653,6 @@ function buildNginxPortalLocationBlock(context) {
640
653
  '',
641
654
  ` location ^~ ${portalBasePath}/ {`,
642
655
  ' absolute_redirect off;',
643
- '',
644
656
  ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
645
657
  ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
646
658
  ' }',
@@ -1229,6 +1241,7 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1229
1241
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
1230
1242
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
1231
1243
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
1244
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
1232
1245
  const rootRedirectBlock = context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1233
1246
  ? ''
1234
1247
  : `
@@ -1321,6 +1334,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1321
1334
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1322
1335
  ' }',
1323
1336
  '',
1337
+ ` handle ${portalBasePath} {`,
1338
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1339
+ ' }',
1340
+ '',
1341
+ ` handle ${portalBasePath}/* {`,
1342
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1343
+ ' }',
1344
+ '',
1324
1345
  ` @settingsRoute path_regexp settingsRoute ${settingsRoutePattern}`,
1325
1346
  ' handle @settingsRoute {',
1326
1347
  ` root * ${publicDir}`,
@@ -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,
@@ -21,9 +21,6 @@ function trimValue(value) {
21
21
  const text = String(value ?? '').trim();
22
22
  return text || undefined;
23
23
  }
24
- function normalizeEnvFilePath(value) {
25
- return value.replace(/\\/g, '/');
26
- }
27
24
  function stripWrappingQuotes(value) {
28
25
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
29
26
  return value
@@ -63,38 +60,39 @@ export function resolveManagedLocalEnvFilePath(runtime) {
63
60
  const config = runtime.env.config ?? {};
64
61
  const explicitEnvFile = trimValue(config.envFile);
65
62
  if (explicitEnvFile) {
66
- return normalizeEnvFilePath(resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile);
63
+ return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
67
64
  }
68
65
  const configuredAppPath = resolveConfiguredAppPath(config);
69
66
  if (configuredAppPath) {
70
- return normalizeEnvFilePath(path.join(configuredAppPath, '.env'));
67
+ return path.join(configuredAppPath, '.env');
71
68
  }
72
69
  if (path.basename(runtime.projectRoot) === 'source') {
73
- return normalizeEnvFilePath(path.resolve(runtime.projectRoot, '..', '.env'));
70
+ return path.resolve(runtime.projectRoot, '..', '.env');
74
71
  }
75
- return normalizeEnvFilePath(path.join(runtime.projectRoot, '.env'));
72
+ return path.join(runtime.projectRoot, '.env');
76
73
  }
77
74
  export function resolveManagedEnvFilePathFromConfig(envName, config) {
78
75
  const kind = config?.kind ?? resolveEnvKind(config);
79
76
  if (kind === 'docker') {
80
- const filePath = resolveDockerEnvFilePath(envName, config);
81
- return filePath ? normalizeEnvFilePath(filePath) : undefined;
77
+ return resolveDockerEnvFilePath(envName, config);
82
78
  }
83
79
  if (kind !== 'local') {
84
80
  return undefined;
85
81
  }
86
82
  const explicitEnvFile = trimValue(config?.envFile);
87
83
  if (explicitEnvFile) {
88
- return normalizeEnvFilePath(resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile);
84
+ return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
89
85
  }
90
86
  const configuredAppPath = resolveConfiguredAppPath(config);
91
87
  if (configuredAppPath) {
92
- return normalizeEnvFilePath(path.join(configuredAppPath, '.env'));
88
+ return path.join(configuredAppPath, '.env');
93
89
  }
94
90
  const configuredAppRootPath = trimValue(config?.appRootPath);
95
91
  if (configuredAppRootPath) {
96
92
  const appRootPath = resolveConfiguredEnvPath(configuredAppRootPath) ?? configuredAppRootPath;
97
- return normalizeEnvFilePath(path.basename(appRootPath) === 'source' ? path.resolve(appRootPath, '..', '.env') : path.join(appRootPath, '.env'));
93
+ return path.basename(appRootPath) === 'source'
94
+ ? path.resolve(appRootPath, '..', '.env')
95
+ : path.join(appRootPath, '.env');
98
96
  }
99
97
  return undefined;
100
98
  }
@@ -765,11 +765,10 @@ function runPromptCatalogWebUIImpl(options) {
765
765
  }
766
766
  };
767
767
  const servePage = (port) => {
768
- const base = `http://${publicHost}:${port}`;
769
768
  const formInner = buildPwcFormHtml(catalog, formDefaults, initialShow, pwcStepDefs, 0, pwcNSteps, locale, uiText);
770
769
  const wizardClientJson = JSON.stringify({ n: pwcNSteps, stepDefs: pwcStepDefs });
771
- const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(base + resolveValidateStepPath) : 'null';
772
- const pwcValFieldUrl = JSON.stringify(base + resolveValidateFieldPath);
770
+ const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(resolveValidateStepPath) : 'null';
771
+ const pwcValFieldUrl = JSON.stringify(resolveValidateFieldPath);
773
772
  const uiTextJson = JSON.stringify(uiText);
774
773
  const pwcShellClass = options.stages && options.stages.length > 0
775
774
  ? 'pwc-shell pwc-shell--stages'
@@ -1493,8 +1492,8 @@ function runPromptCatalogWebUIImpl(options) {
1493
1492
  </div>
1494
1493
  <script>
1495
1494
  (function () {
1496
- var sub = ${JSON.stringify(base + submitPath)};
1497
- var ref = ${JSON.stringify(base + reflowPath)};
1495
+ var sub = ${JSON.stringify(submitPath)};
1496
+ var ref = ${JSON.stringify(reflowPath)};
1498
1497
  var pwcValStep = ${pwcValStepUrl};
1499
1498
  var pwcValField = ${pwcValFieldUrl};
1500
1499
  var pwcStepMeta = ${JSON.stringify(PWC_FORM_META_STEP)};
@@ -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": "3.0.0-alpha.3",
3
+ "version": "3.0.0-alpha.4",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -147,5 +147,5 @@
147
147
  "type": "git",
148
148
  "url": "git+https://github.com/nocobase/nocobase.git"
149
149
  },
150
- "gitHead": "9899cd799c070c7bd85140148ea5b100b046bdc7"
150
+ "gitHead": "6658768567378382de758c4e814ac510d688400c"
151
151
  }