@nocobase/cli 2.2.0-test.15 → 2.2.0

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.
Files changed (44) hide show
  1. package/dist/commands/api/resource/create.js +11 -2
  2. package/dist/commands/app/start.js +1 -21
  3. package/dist/commands/config/set.js +0 -1
  4. package/dist/commands/init.js +3 -120
  5. package/dist/commands/install.js +4 -121
  6. package/dist/commands/source/dev.js +1 -1
  7. package/dist/lib/api-client.js +7 -0
  8. package/dist/lib/auth-store.js +1 -3
  9. package/dist/lib/browser.js +29 -0
  10. package/dist/lib/cli-config.js +1 -20
  11. package/dist/lib/env-config.js +0 -3
  12. package/dist/lib/env-proxy.js +1 -65
  13. package/dist/lib/generated-command.js +81 -0
  14. package/dist/lib/managed-init-env.js +1 -6
  15. package/dist/lib/naming.js +9 -0
  16. package/dist/lib/plugin-import.js +30 -8
  17. package/dist/lib/resource-command.js +18 -2
  18. package/dist/lib/resource-request.js +8 -0
  19. package/dist/lib/run-npm.js +16 -17
  20. package/dist/lib/runtime-generator.js +28 -1
  21. package/dist/lib/ui.js +1 -28
  22. package/dist/locale/en-US.json +0 -154
  23. package/dist/locale/zh-CN.json +0 -154
  24. package/nocobase-ctl.config.json +111 -0
  25. package/package.json +3 -5
  26. package/dist/commands/portal/create.js +0 -105
  27. package/dist/commands/portal/deploy.js +0 -81
  28. package/dist/commands/portal/destroy.js +0 -104
  29. package/dist/commands/portal/dev.js +0 -71
  30. package/dist/commands/portal/index.js +0 -20
  31. package/dist/commands/portal/info.js +0 -82
  32. package/dist/commands/portal/list.js +0 -98
  33. package/dist/commands/portal/pull.js +0 -77
  34. package/dist/commands/portal/push.js +0 -79
  35. package/dist/lib/portal-command-env.js +0 -31
  36. package/dist/lib/portal-create.js +0 -488
  37. package/dist/lib/portal-deploy.js +0 -275
  38. package/dist/lib/portal-destroy.js +0 -100
  39. package/dist/lib/portal-dev.js +0 -79
  40. package/dist/lib/portal-env-files.js +0 -53
  41. package/dist/lib/portal-info.js +0 -31
  42. package/dist/lib/portal-list.js +0 -197
  43. package/dist/lib/portal-source.js +0 -416
  44. package/dist/lib/portal-template.js +0 -190
@@ -34,7 +34,6 @@ export const SUPPORTED_CLI_CONFIG_KEYS = [
34
34
  'locale',
35
35
  'default-ui-host',
36
36
  'default-api-host',
37
- 'default-portal-template',
38
37
  'update.policy',
39
38
  'license.pkg-url',
40
39
  'docker.network',
@@ -121,10 +120,7 @@ function pruneSettings(config) {
121
120
  delete config.settings.locale;
122
121
  }
123
122
  const init = config.settings?.init;
124
- if (init &&
125
- !trimValue(init.defaultUiHost) &&
126
- !trimValue(init.defaultApiHost) &&
127
- !trimValue(init.defaultPortalTemplate)) {
123
+ if (init && !trimValue(init.defaultUiHost) && !trimValue(init.defaultApiHost)) {
128
124
  delete config.settings?.init;
129
125
  }
130
126
  const update = config.settings?.update;
@@ -185,8 +181,6 @@ export function getExplicitCliConfigValue(config, key) {
185
181
  return trimValue(config.settings?.init?.defaultUiHost);
186
182
  case 'default-api-host':
187
183
  return trimValue(config.settings?.init?.defaultApiHost);
188
- case 'default-portal-template':
189
- return trimValue(config.settings?.init?.defaultPortalTemplate);
190
184
  case 'update.policy':
191
185
  return normalizeCliUpdatePolicy(config.settings?.update?.policy);
192
186
  case 'license.pkg-url':
@@ -239,8 +233,6 @@ export function getEffectiveCliConfigValue(config, key) {
239
233
  return '127.0.0.1';
240
234
  case 'default-api-host':
241
235
  return '127.0.0.1';
242
- case 'default-portal-template':
243
- return explicit ?? '';
244
236
  case 'update.policy':
245
237
  return explicit ?? DEFAULT_UPDATE_POLICY;
246
238
  case 'license.pkg-url':
@@ -385,12 +377,6 @@ export async function setCliConfigValue(key, value, options = {}) {
385
377
  defaultApiHost: normalized,
386
378
  };
387
379
  break;
388
- case 'default-portal-template':
389
- config.settings.init = {
390
- ...(config.settings.init ?? {}),
391
- defaultPortalTemplate: normalized,
392
- };
393
- break;
394
380
  case 'update.policy':
395
381
  config.settings.update = {
396
382
  ...(config.settings.update ?? {}),
@@ -526,11 +512,6 @@ export async function deleteCliConfigValue(key, options = {}) {
526
512
  delete config.settings.init.defaultApiHost;
527
513
  }
528
514
  break;
529
- case 'default-portal-template':
530
- if (config.settings.init) {
531
- delete config.settings.init.defaultPortalTemplate;
532
- }
533
- break;
534
515
  case 'update.policy':
535
516
  if (config.settings.update) {
536
517
  delete config.settings.update.policy;
@@ -36,9 +36,6 @@ const STRING_ENV_CONFIG_KEYS = [
36
36
  'dbSchema',
37
37
  'dbTablePrefix',
38
38
  'lang',
39
- 'developmentMode',
40
- 'portalName',
41
- 'portalTemplate',
42
39
  'rootUsername',
43
40
  'rootEmail',
44
41
  'rootPassword',
@@ -23,7 +23,6 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
23
23
  const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
24
24
  const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
25
25
  const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
26
- const PORTAL_CLIENT_PREFIX = 'x';
27
26
  const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
28
27
  const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
29
28
  const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
@@ -542,8 +541,6 @@ function buildNginxManagedConfigBlock(context) {
542
541
  ]
543
542
  : []),
544
543
  '',
545
- buildNginxPortalLocationBlock(context),
546
- '',
547
544
  ` location = ${apiBasePathNoTrailingSlash} {`,
548
545
  ` return 308 ${context.apiBasePath}$is_args$args;`,
549
546
  ' }',
@@ -569,7 +566,7 @@ function buildNginxManagedConfigBlock(context) {
569
566
  ` include ${context.snippetsDir}/spa-location.conf;`,
570
567
  ' }',
571
568
  '',
572
- ` location ${context.appPublicPath} {`,
569
+ ` location ^~ ${context.appPublicPath} {`,
573
570
  ` alias ${context.publicDir}/;`,
574
571
  ` try_files $uri /index-v1.html =404;`,
575
572
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -578,65 +575,6 @@ function buildNginxManagedConfigBlock(context) {
578
575
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
579
576
  ].join('\n');
580
577
  }
581
- function buildNginxPortalRootPublicPath(appPublicPath) {
582
- return appPublicPath === DEFAULT_APP_PUBLIC_PATH
583
- ? `/${PORTAL_CLIENT_PREFIX}/`
584
- : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
585
- }
586
- function buildNginxPortalLocationBlock(context) {
587
- const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
588
- const portalBasePathPattern = escapeRegExp(portalBasePath);
589
- return [
590
- ` location ^~ ${portalBasePath}/apps/ {`,
591
- ' absolute_redirect off;',
592
- '',
593
- ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
594
- ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
595
- ' }',
596
- '',
597
- ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
598
- ' return 404;',
599
- ' }',
600
- '',
601
- ` root ${context.storageDir};`,
602
- '',
603
- ' if ($portal_path = "") {',
604
- ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
605
- ' }',
606
- '',
607
- ' try_files',
608
- ' /portals/$subapp/$portal/dist/$portal_path',
609
- ' /portals/$subapp/$portal/dist/$portal_path/',
610
- ' /portals/$subapp/$portal/dist/index.html',
611
- ' =404;',
612
- ' }',
613
- '',
614
- ` location ^~ ${portalBasePath}/ {`,
615
- ' absolute_redirect off;',
616
- '',
617
- ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
618
- ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
619
- ' }',
620
- '',
621
- ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
622
- ' return 404;',
623
- ' }',
624
- '',
625
- ` root ${context.storageDir};`,
626
- '',
627
- ' if ($portal_path = "") {',
628
- ' rewrite ^ /portals/main/$portal/dist/index.html break;',
629
- ' }',
630
- '',
631
- ' try_files',
632
- ' /portals/main/$portal/dist/$portal_path',
633
- ' /portals/main/$portal/dist/$portal_path/',
634
- ' /portals/main/$portal/dist/index.html',
635
- ' =404;',
636
- ' }',
637
- '',
638
- ].join('\n');
639
- }
640
578
  function buildNginxRuntimeConfig(context, variant) {
641
579
  return {
642
580
  __webpack_public_path__: context.cdnBaseUrl,
@@ -670,7 +608,6 @@ async function buildEnvProxyNginxRenderContext(source, options) {
670
608
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
671
609
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
672
610
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
673
- const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
674
611
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
675
612
  const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
676
613
  return {
@@ -695,7 +632,6 @@ async function buildEnvProxyNginxRenderContext(source, options) {
695
632
  modernClientPrefix: source.settings.modernClientPrefix,
696
633
  proxyHost,
697
634
  snippetsDir: mappedSnippetsDir,
698
- storageDir: mappedStorageDir,
699
635
  uploadsDir: mappedUploadsDir,
700
636
  v2PublicPath,
701
637
  wsPath: source.settings.wsPath,
@@ -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,
@@ -15,7 +15,7 @@ export function resolveManagedSetupState(value) {
15
15
  export function isPreparedSetupState(value) {
16
16
  return resolveManagedSetupState(value) === 'prepared';
17
17
  }
18
- export function buildInitAppEnvVarsFromConfig(config, options = {}) {
18
+ export function buildInitAppEnvVarsFromConfig(config) {
19
19
  const out = {};
20
20
  const put = (key, value) => {
21
21
  const text = trimValue(value);
@@ -28,10 +28,5 @@ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
28
28
  put('INIT_ROOT_EMAIL', config?.rootEmail);
29
29
  put('INIT_ROOT_PASSWORD', config?.rootPassword);
30
30
  put('INIT_ROOT_NICKNAME', config?.rootNickname);
31
- if (options.includePortal !== false) {
32
- put('INIT_DEVELOPMENT_MODE', config?.developmentMode);
33
- put('INIT_PORTAL_NAME', config?.portalName);
34
- put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
35
- }
36
31
  return out;
37
32
  }
@@ -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, strip: 1 }));
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 { packageName, packageVersion } = await readPluginMetadata(stageDir, archive.source);
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(stageDir, outputDir);
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
- if (!stageMoved) {
272
- await fsp.rm(stageDir, { recursive: true, force: true });
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: parseObjectFlag(flags.values, '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('.')) {
@@ -48,10 +48,6 @@ const MISSING_COMMAND_SPECS = {
48
48
  displayName: 'pnpm',
49
49
  configKey: 'bin.pnpm',
50
50
  },
51
- npm: {
52
- displayName: 'npm',
53
- configKey: 'bin.npm',
54
- },
55
51
  };
56
52
  const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
57
53
  /cannot connect to the docker daemon/i,
@@ -65,15 +61,6 @@ async function resolveCommandName(name) {
65
61
  function shouldTeeInheritedOutput(options) {
66
62
  return options?.stdio === 'inherit' && Boolean(String(process.env.NB_CLI_ACTIVE_LOG_FILE ?? '').trim());
67
63
  }
68
- function buildProcessEnv(options) {
69
- if (options?.envMode === 'replace') {
70
- return options.env ?? {};
71
- }
72
- return {
73
- ...process.env,
74
- ...options?.env,
75
- };
76
- }
77
64
  function createMissingCommandError(name, label, error) {
78
65
  const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
79
66
  if (code !== 'ENOENT') {
@@ -159,7 +146,10 @@ export async function run(name, args, options) {
159
146
  const child = spawn(command, [...args], {
160
147
  stdio,
161
148
  cwd,
162
- env: buildProcessEnv(options),
149
+ env: {
150
+ ...process.env,
151
+ ...options?.env,
152
+ },
163
153
  windowsHide: process.platform === 'win32',
164
154
  });
165
155
  if (options?.stdio === 'pipe' || shouldTeeInheritedOutput(options)) {
@@ -278,7 +268,10 @@ export async function commandSucceeds(name, args, options) {
278
268
  return await new Promise((resolve, reject) => {
279
269
  const child = spawn(command, [...args], {
280
270
  cwd,
281
- env: buildProcessEnv(options),
271
+ env: {
272
+ ...process.env,
273
+ ...options?.env,
274
+ },
282
275
  stdio: 'ignore',
283
276
  windowsHide: process.platform === 'win32',
284
277
  });
@@ -309,7 +302,10 @@ export async function commandOutput(name, args, options) {
309
302
  return await new Promise((resolve, reject) => {
310
303
  const child = spawn(command, [...args], {
311
304
  cwd,
312
- env: buildProcessEnv(options),
305
+ env: {
306
+ ...process.env,
307
+ ...options?.env,
308
+ },
313
309
  stdio: ['ignore', 'pipe', 'pipe'],
314
310
  windowsHide: process.platform === 'win32',
315
311
  });
@@ -368,7 +364,10 @@ export async function commandOutputViaFile(name, args, options) {
368
364
  const result = await new Promise((resolve, reject) => {
369
365
  const child = spawn(command, [...args], {
370
366
  cwd,
371
- env: buildProcessEnv(options),
367
+ env: {
368
+ ...process.env,
369
+ ...options?.env,
370
+ },
372
371
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
373
372
  windowsHide: process.platform === 'win32',
374
373
  });
@@ -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/dist/lib/ui.js CHANGED
@@ -15,35 +15,8 @@ let verboseMode = false;
15
15
  let lastStaticTaskMessage;
16
16
  let lastStaticTaskAt = 0;
17
17
  const STATIC_TASK_UPDATE_THROTTLE_MS = 3_000;
18
- function isCombiningCodePoint(codePoint) {
19
- return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
20
- (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
21
- (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
22
- (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
23
- (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
24
- }
25
- function isFullWidthCodePoint(codePoint) {
26
- return (codePoint >= 0x1100 &&
27
- (codePoint <= 0x115f ||
28
- codePoint === 0x2329 ||
29
- codePoint === 0x232a ||
30
- (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
31
- (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
32
- (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
33
- (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
34
- (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
35
- (codePoint >= 0xff00 && codePoint <= 0xff60) ||
36
- (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
37
- (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
38
- }
39
18
  function stringWidth(value) {
40
- return Array.from(value).reduce((width, character) => {
41
- const codePoint = character.codePointAt(0);
42
- if (!codePoint || codePoint === 0 || codePoint < 32 || isCombiningCodePoint(codePoint)) {
43
- return width;
44
- }
45
- return width + (isFullWidthCodePoint(codePoint) ? 2 : 1);
46
- }, 0);
19
+ return Array.from(value).length;
47
20
  }
48
21
  function pad(value, width) {
49
22
  const padding = Math.max(0, width - stringWidth(value));