@nocobase/cli 2.1.0-alpha.25 → 2.1.0-alpha.27

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 (84) hide show
  1. package/README.md +61 -49
  2. package/README.zh-CN.md +40 -47
  3. package/dist/commands/app/down.js +259 -0
  4. package/dist/commands/app/logs.js +98 -0
  5. package/dist/commands/app/restart.js +75 -0
  6. package/dist/commands/app/start.js +252 -0
  7. package/dist/commands/app/stop.js +98 -0
  8. package/dist/commands/app/upgrade.js +579 -0
  9. package/dist/commands/build.js +3 -48
  10. package/dist/commands/config/delete.js +30 -0
  11. package/dist/commands/config/get.js +29 -0
  12. package/dist/commands/config/index.js +20 -0
  13. package/dist/commands/config/list.js +29 -0
  14. package/dist/commands/config/set.js +35 -0
  15. package/dist/commands/db/check.js +230 -0
  16. package/dist/commands/db/shared.js +1 -1
  17. package/dist/commands/dev.js +3 -147
  18. package/dist/commands/down.js +3 -188
  19. package/dist/commands/download.js +4 -856
  20. package/dist/commands/env/add.js +28 -23
  21. package/dist/commands/env/info.js +152 -0
  22. package/dist/commands/env/list.js +23 -9
  23. package/dist/commands/env/shared.js +158 -0
  24. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  25. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  26. package/dist/commands/init.js +83 -6
  27. package/dist/commands/install.js +361 -82
  28. package/dist/commands/license/activate.js +357 -0
  29. package/dist/commands/license/env.js +94 -0
  30. package/dist/commands/license/generate-id.js +107 -0
  31. package/dist/commands/license/id.js +52 -0
  32. package/dist/commands/license/index.js +20 -0
  33. package/dist/commands/license/plugins/clean.js +98 -0
  34. package/dist/commands/license/plugins/index.js +20 -0
  35. package/dist/commands/license/plugins/list.js +50 -0
  36. package/dist/commands/license/plugins/shared.js +325 -0
  37. package/dist/commands/license/plugins/sync.js +267 -0
  38. package/dist/commands/license/shared.js +411 -0
  39. package/dist/commands/license/status.js +50 -0
  40. package/dist/commands/logs.js +3 -88
  41. package/dist/commands/plugin/disable.js +64 -0
  42. package/dist/commands/plugin/enable.js +64 -0
  43. package/dist/commands/plugin/list.js +62 -0
  44. package/dist/commands/pm/disable.js +3 -54
  45. package/dist/commands/pm/enable.js +3 -54
  46. package/dist/commands/pm/list.js +3 -52
  47. package/dist/commands/restart.js +3 -65
  48. package/dist/commands/scaffold/migration.js +1 -1
  49. package/dist/commands/scaffold/plugin.js +1 -1
  50. package/dist/commands/skills/remove.js +71 -0
  51. package/dist/commands/skills/update.js +7 -0
  52. package/dist/commands/source/build.js +58 -0
  53. package/dist/commands/source/dev.js +157 -0
  54. package/dist/commands/source/download.js +866 -0
  55. package/dist/commands/source/test.js +467 -0
  56. package/dist/commands/start.js +3 -209
  57. package/dist/commands/stop.js +3 -88
  58. package/dist/commands/test.js +3 -457
  59. package/dist/commands/upgrade.js +3 -585
  60. package/dist/help/runtime-help.js +3 -0
  61. package/dist/lib/api-client.js +94 -9
  62. package/dist/lib/app-health.js +126 -0
  63. package/dist/lib/app-managed-resources.js +264 -0
  64. package/dist/lib/app-runtime.js +26 -10
  65. package/dist/lib/auth-store.js +29 -63
  66. package/dist/lib/build-config.js +8 -0
  67. package/dist/lib/cli-config.js +176 -0
  68. package/dist/lib/cli-home.js +12 -26
  69. package/dist/lib/cli-locale.js +15 -1
  70. package/dist/lib/db-connection-check.js +178 -0
  71. package/dist/lib/env-config.js +80 -0
  72. package/dist/lib/generated-command.js +23 -3
  73. package/dist/lib/plugin-storage.js +127 -0
  74. package/dist/lib/prompt-validators.js +4 -4
  75. package/dist/lib/prompt-web-ui.js +13 -6
  76. package/dist/lib/runtime-generator.js +89 -10
  77. package/dist/lib/self-manager.js +57 -2
  78. package/dist/lib/skills-manager.js +34 -7
  79. package/dist/lib/startup-update.js +85 -7
  80. package/dist/locale/en-US.json +16 -13
  81. package/dist/locale/zh-CN.json +16 -13
  82. package/nocobase-ctl.config.json +82 -0
  83. package/package.json +41 -6
  84. package/dist/commands/ps.js +0 -119
@@ -0,0 +1,80 @@
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
+ const STRING_ENV_CONFIG_KEYS = [
10
+ 'source',
11
+ 'downloadVersion',
12
+ 'dockerRegistry',
13
+ 'dockerPlatform',
14
+ 'gitUrl',
15
+ 'npmRegistry',
16
+ 'appRootPath',
17
+ 'storagePath',
18
+ 'appPort',
19
+ 'appKey',
20
+ 'timezone',
21
+ 'dbDialect',
22
+ 'builtinDbImage',
23
+ 'dbHost',
24
+ 'dbPort',
25
+ 'dbDatabase',
26
+ 'dbUser',
27
+ 'dbPassword',
28
+ 'rootUsername',
29
+ 'rootEmail',
30
+ 'rootPassword',
31
+ 'rootNickname',
32
+ ];
33
+ const BOOLEAN_ENV_CONFIG_KEYS = [
34
+ 'builtinDb',
35
+ 'devDependencies',
36
+ 'build',
37
+ 'buildDts',
38
+ ];
39
+ function trimConfigValue(value) {
40
+ const text = String(value ?? '').trim();
41
+ return text || undefined;
42
+ }
43
+ function resolveEnvKind(input) {
44
+ const source = trimConfigValue(input.source);
45
+ const appRootPath = trimConfigValue(input.appRootPath);
46
+ if (source === 'docker') {
47
+ return 'docker';
48
+ }
49
+ if (source === 'npm' || source === 'git' || source === 'local' || appRootPath) {
50
+ return 'local';
51
+ }
52
+ return 'http';
53
+ }
54
+ export function buildStoredEnvConfig(input) {
55
+ const envConfig = {
56
+ kind: resolveEnvKind(input),
57
+ apiBaseUrl: trimConfigValue(input.apiBaseUrl) ?? '',
58
+ };
59
+ for (const key of STRING_ENV_CONFIG_KEYS) {
60
+ const value = trimConfigValue(input[key]);
61
+ if (value) {
62
+ envConfig[key] = value;
63
+ }
64
+ }
65
+ for (const key of BOOLEAN_ENV_CONFIG_KEYS) {
66
+ const value = input[key];
67
+ if (typeof value === 'boolean') {
68
+ envConfig[key] = value;
69
+ }
70
+ }
71
+ if (input.builtinDb === false) {
72
+ envConfig.builtinDbImage = undefined;
73
+ }
74
+ const authType = trimConfigValue(input.authType);
75
+ const accessToken = trimConfigValue(input.accessToken);
76
+ if (authType === 'token' && accessToken) {
77
+ envConfig.accessToken = accessToken;
78
+ }
79
+ return envConfig;
80
+ }
@@ -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
  /**
2
10
  * This file is part of the NocoBase (R) project.
3
11
  * Copyright (c) 2020-2024 NocoBase Co., Ltd.
@@ -12,7 +20,10 @@ import { applyPostProcessor } from './post-processors.js';
12
20
  import { registerPostProcessors } from '../post-processors/index.js';
13
21
  function buildParameterFlag(parameter, options) {
14
22
  const hints = [parameter.in];
15
- if (parameter.type === 'object' || parameter.type === 'array' || parameter.jsonEncoded) {
23
+ if (parameter.isFile) {
24
+ hints.push('file path');
25
+ }
26
+ else if (parameter.type === 'object' || parameter.type === 'array' || parameter.jsonEncoded) {
16
27
  hints.push('JSON');
17
28
  }
18
29
  else if (parameter.isArray) {
@@ -67,10 +78,10 @@ export function createGeneratedFlags(operation) {
67
78
  // Body flags are an alternative authoring path to --body/--body-file.
68
79
  // Enforce required body semantics later in parseBody(), after we know
69
80
  // which input mode the user chose.
70
- required: parameter.in === 'body' ? false : parameter.required,
81
+ required: parameter.in === 'body' && !parameter.isFile ? false : parameter.required,
71
82
  });
72
83
  }
73
- if (operation.hasBody) {
84
+ if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
74
85
  flags.body = Flags.string({
75
86
  description: 'Full JSON request body string. Do not combine with body field flags.',
76
87
  helpGroup: 'Raw JSON Body',
@@ -82,6 +93,13 @@ export function createGeneratedFlags(operation) {
82
93
  exclusive: ['body'],
83
94
  });
84
95
  }
96
+ if (operation.responseType === 'binary') {
97
+ flags.output = Flags.string({
98
+ description: 'Path where the downloaded response should be written.',
99
+ helpGroup: 'Output',
100
+ required: true,
101
+ });
102
+ }
85
103
  flags['api-base-url'] = Flags.string({
86
104
  description: 'NocoBase API base URL, for example http://localhost:13000/api',
87
105
  helpGroup: 'Global',
@@ -132,6 +150,8 @@ export class GeneratedApiCommand extends Command {
132
150
  parameters: ctor.operation.parameters,
133
151
  hasBody: ctor.operation.hasBody,
134
152
  bodyRequired: ctor.operation.bodyRequired,
153
+ requestContentType: ctor.operation.requestContentType,
154
+ responseType: ctor.operation.responseType,
135
155
  },
136
156
  });
137
157
  if (!response.ok) {
@@ -0,0 +1,127 @@
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 path from 'node:path';
10
+ import { access, lstat, mkdir, readdir, readlink, realpath, rm, stat, symlink } from 'node:fs/promises';
11
+ async function pathExists(target) {
12
+ try {
13
+ await access(target);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function resolvePluginStoragePath(storagePath) {
21
+ const root = String(storagePath ?? process.env.STORAGE_PATH ?? '').trim();
22
+ if (root) {
23
+ return path.join(path.isAbsolute(root) ? root : path.resolve(process.cwd(), root), 'plugins');
24
+ }
25
+ const configured = String(process.env.PLUGIN_STORAGE_PATH ?? '').trim();
26
+ if (configured) {
27
+ return path.isAbsolute(configured) ? configured : path.resolve(process.cwd(), configured);
28
+ }
29
+ return path.resolve(process.cwd(), 'storage', 'plugins');
30
+ }
31
+ async function getStoragePluginNames(target) {
32
+ const plugins = [];
33
+ const items = await readdir(target);
34
+ for (const item of items) {
35
+ const itemPath = path.resolve(target, item);
36
+ if (item.startsWith('@')) {
37
+ const statResult = await stat(itemPath);
38
+ if (!statResult.isDirectory()) {
39
+ continue;
40
+ }
41
+ const children = await getStoragePluginNames(itemPath);
42
+ plugins.push(...children.map((child) => `${item}/${child}`));
43
+ continue;
44
+ }
45
+ if (await pathExists(path.resolve(itemPath, 'package.json'))) {
46
+ plugins.push(item);
47
+ }
48
+ }
49
+ return plugins;
50
+ }
51
+ async function ensureOrgDirectory(nodeModulesPath, pluginName) {
52
+ if (!pluginName.startsWith('@')) {
53
+ return;
54
+ }
55
+ const [orgName] = pluginName.split('/');
56
+ await mkdir(path.resolve(nodeModulesPath, orgName), { recursive: true });
57
+ }
58
+ async function isSymlinkValid(linkPath, targetPath) {
59
+ try {
60
+ if (await pathExists(linkPath)) {
61
+ const realPath = await realpath(linkPath);
62
+ return realPath === targetPath;
63
+ }
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ return false;
69
+ }
70
+ async function createStoragePluginSymlink(storagePluginsPath, nodeModulesPath, pluginName) {
71
+ const targetPath = path.resolve(storagePluginsPath, pluginName);
72
+ if (!(await pathExists(targetPath))) {
73
+ return;
74
+ }
75
+ await ensureOrgDirectory(nodeModulesPath, pluginName);
76
+ const linkPath = path.resolve(nodeModulesPath, pluginName);
77
+ if (await isSymlinkValid(linkPath, targetPath)) {
78
+ return;
79
+ }
80
+ await rm(linkPath, { recursive: true, force: true });
81
+ await symlink(targetPath, linkPath, 'dir');
82
+ }
83
+ export async function createStoragePluginsSymlink(storagePath, nodeModulesPath = String(process.env.NODE_MODULES_PATH ?? '').trim()) {
84
+ if (!nodeModulesPath) {
85
+ return;
86
+ }
87
+ const storagePluginsPath = resolvePluginStoragePath(storagePath);
88
+ if (!(await pathExists(storagePluginsPath))) {
89
+ return;
90
+ }
91
+ const pluginNames = await getStoragePluginNames(storagePluginsPath);
92
+ await Promise.all(pluginNames.map(async (pluginName) => await createStoragePluginSymlink(storagePluginsPath, nodeModulesPath, pluginName)));
93
+ }
94
+ export async function removeStoragePluginSymlink(pluginName, storagePath, nodeModulesPath = String(process.env.NODE_MODULES_PATH ?? '').trim()) {
95
+ if (!nodeModulesPath) {
96
+ return false;
97
+ }
98
+ const storagePluginsPath = resolvePluginStoragePath(storagePath);
99
+ const targetPath = path.resolve(storagePluginsPath, pluginName);
100
+ const linkPath = path.resolve(nodeModulesPath, pluginName);
101
+ if (!(await pathExists(linkPath))) {
102
+ return false;
103
+ }
104
+ let statResult;
105
+ try {
106
+ statResult = await lstat(linkPath);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ if (!statResult.isSymbolicLink()) {
112
+ return false;
113
+ }
114
+ let resolvedLinkTarget = '';
115
+ try {
116
+ const linkTarget = await readlink(linkPath);
117
+ resolvedLinkTarget = path.resolve(path.dirname(linkPath), linkTarget);
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ if (resolvedLinkTarget !== targetPath) {
123
+ return false;
124
+ }
125
+ await rm(linkPath, { recursive: true, force: true });
126
+ return true;
127
+ }
@@ -173,13 +173,13 @@ export async function validateAvailableTcpPort(value) {
173
173
  return formatError;
174
174
  }
175
175
  const port = parseTcpPort(raw);
176
- const available = await canListenOnTcpPort(port);
177
- if (!available) {
178
- return translateCli('validators.tcpPort.alreadyInUse', { port });
179
- }
180
176
  const dockerPorts = await getDockerPublishedTcpPorts();
181
177
  if (dockerPorts.has(port)) {
182
178
  return translateCli('validators.tcpPort.alreadyInUseByDocker', { port });
183
179
  }
180
+ const available = await canListenOnTcpPort(port);
181
+ if (!available) {
182
+ return translateCli('validators.tcpPort.alreadyInUse', { port });
183
+ }
184
184
  return undefined;
185
185
  }
@@ -559,17 +559,23 @@ function readFormFromClientStrippingPwcMeta(o) {
559
559
  const { [PWC_FORM_META_STEP]: _meta, ...rest } = o;
560
560
  return rest;
561
561
  }
562
- function openUrlInDefaultBrowser(url) {
562
+ function openUrlInDefaultBrowser(url, onError) {
563
+ const reportError = (error) => {
564
+ onError?.(url, error);
565
+ };
563
566
  const platform = process.platform;
567
+ let child;
564
568
  if (platform === 'darwin') {
565
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
569
+ child = spawn('open', [url], { stdio: 'ignore', detached: true });
566
570
  }
567
571
  else if (platform === 'win32') {
568
- spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
572
+ child = spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true });
569
573
  }
570
574
  else {
571
- spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
575
+ child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
572
576
  }
577
+ child.once('error', reportError);
578
+ child.unref();
573
579
  }
574
580
  function closePromptWebUiServer(server, done) {
575
581
  server.close(done);
@@ -2084,11 +2090,12 @@ function runPromptCatalogWebUIImpl(options) {
2084
2090
  const port = addr.port;
2085
2091
  const startUrl = `http://${host}:${port}/`;
2086
2092
  options.onServerStart?.({ host, port, url: startUrl });
2093
+ const onOpenBrowserError = options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u));
2087
2094
  try {
2088
- openUrlInDefaultBrowser(startUrl);
2095
+ openUrlInDefaultBrowser(startUrl, onOpenBrowserError);
2089
2096
  }
2090
2097
  catch (e) {
2091
- (options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u)))(startUrl, e);
2098
+ onOpenBrowserError(startUrl, e);
2092
2099
  }
2093
2100
  timeoutId = setTimeout(() => rejectAndClose(new Error('Local UI timeout — close the tab and try again, or resubmit within the time limit.')), timeoutMs);
2094
2101
  });
@@ -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
  /**
2
10
  * This file is part of the NocoBase (R) project.
3
11
  * Copyright (c) 2020-2024 NocoBase Co., Ltd.
@@ -60,12 +68,59 @@ function toGeneratedParameter(parameter, usedFlagNames) {
60
68
  required: parameter.required,
61
69
  description: parameter.description,
62
70
  type: inferParameterType(parameter.schema),
71
+ format: parameter.schema?.format,
63
72
  isArray: parameter.schema?.type === 'array',
73
+ isFile: parameter.schema?.type === 'string' && parameter.schema?.format === 'binary',
64
74
  };
65
75
  }
66
76
  function getJsonRequestSchema(requestBody) {
67
77
  return requestBody?.content?.['application/json']?.schema;
68
78
  }
79
+ function getMultipartRequestSchema(requestBody) {
80
+ return requestBody?.content?.['multipart/form-data']?.schema;
81
+ }
82
+ function getRequestContentType(requestBody) {
83
+ if (!requestBody || '$ref' in requestBody) {
84
+ return undefined;
85
+ }
86
+ if (requestBody.content?.['multipart/form-data']) {
87
+ return 'multipart/form-data';
88
+ }
89
+ if (requestBody.content?.['application/json']) {
90
+ return 'application/json';
91
+ }
92
+ return undefined;
93
+ }
94
+ function getRequestSchema(requestBody) {
95
+ return getMultipartRequestSchema(requestBody) ?? getJsonRequestSchema(requestBody);
96
+ }
97
+ function isBinarySchema(schema) {
98
+ if (!schema || typeof schema !== 'object') {
99
+ return false;
100
+ }
101
+ if (schema.type === 'string' && schema.format === 'binary') {
102
+ return true;
103
+ }
104
+ return [...(schema.oneOf ?? []), ...(schema.anyOf ?? []), ...(schema.allOf ?? [])].some(isBinarySchema);
105
+ }
106
+ function getResponseType(operation) {
107
+ for (const response of Object.values(operation.responses ?? {})) {
108
+ const content = response?.content ?? {};
109
+ const mediaTypes = Object.keys(content);
110
+ const hasJson = mediaTypes.some((mediaType) => mediaType.includes('json'));
111
+ if (hasJson) {
112
+ return 'json';
113
+ }
114
+ const hasBinary = mediaTypes.some((mediaType) => {
115
+ const schema = content[mediaType]?.schema;
116
+ return mediaType === 'application/octet-stream' || mediaType.includes('zip') || isBinarySchema(schema);
117
+ });
118
+ if (hasBinary) {
119
+ return 'binary';
120
+ }
121
+ }
122
+ return undefined;
123
+ }
69
124
  function normalizeCompositeSchema(schema) {
70
125
  if (!schema || typeof schema !== 'object') {
71
126
  return schema;
@@ -139,7 +194,7 @@ function describeSchemaShape(schema, options = {}) {
139
194
  return type;
140
195
  }
141
196
  function extractBodyParameters(requestBody, usedFlagNames) {
142
- const schema = getJsonRequestSchema(requestBody);
197
+ const schema = getRequestSchema(requestBody);
143
198
  const properties = normalizeCompositeSchema(schema)?.properties;
144
199
  const required = new Set(normalizeCompositeSchema(schema)?.required ?? []);
145
200
  return Object.entries(properties ?? {}).map(([name, propertySchema]) => ({
@@ -149,7 +204,9 @@ function extractBodyParameters(requestBody, usedFlagNames) {
149
204
  required: required.has(name),
150
205
  description: propertySchema.description,
151
206
  type: inferParameterType(propertySchema),
207
+ format: propertySchema.format,
152
208
  isArray: propertySchema.type === 'array',
209
+ isFile: propertySchema.type === 'string' && propertySchema.format === 'binary',
153
210
  jsonEncoded: propertySchema.type === 'object' || propertySchema.type === 'array',
154
211
  jsonShape: describeSchemaShape(propertySchema),
155
212
  }));
@@ -179,6 +236,9 @@ function formatFlagExample(parameter) {
179
236
  if (parameter.type === 'boolean') {
180
237
  return `--${parameter.flagName}`;
181
238
  }
239
+ if (parameter.isFile) {
240
+ return `--${parameter.flagName} <path>`;
241
+ }
182
242
  if (parameter.type === 'object' || parameter.jsonEncoded) {
183
243
  if (parameter.type === 'array' || parameter.isArray) {
184
244
  return `--${parameter.flagName} '[]'`;
@@ -216,12 +276,13 @@ export function buildExamples(commandId, operation) {
216
276
  const requiredParameters = operation.parameters.filter((parameter) => parameter.required);
217
277
  const requiredFlags = requiredParameters.map(formatFlagExample);
218
278
  const requiredNonBodyFlags = requiredParameters.filter((parameter) => parameter.in !== 'body').map(formatFlagExample);
219
- const examples = [`nb api ${commandId}${requiredFlags.length ? ` ${requiredFlags.join(' ')}` : ''}`];
279
+ const outputFlag = operation.responseType === 'binary' ? ' --output <path>' : '';
280
+ const examples = [`nb api ${commandId}${requiredFlags.length ? ` ${requiredFlags.join(' ')}` : ''}${outputFlag}`];
220
281
  const firstOptional = operation.parameters.find((parameter) => !parameter.required);
221
282
  if (firstOptional) {
222
- examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}`);
283
+ examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}`.trim());
223
284
  }
224
- if (operation.hasBody) {
285
+ if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
225
286
  const prefix = `nb api ${commandId}${requiredNonBodyFlags.length ? ` ${requiredNonBodyFlags.join(' ')}` : ''}`;
226
287
  examples.push(`${prefix} --body '${buildSampleBody(operation.parameters)}'`);
227
288
  }
@@ -248,9 +309,17 @@ function buildDescription(operation) {
248
309
  }
249
310
  if (operation.hasBody) {
250
311
  const bodyFlags = operation.parameters.filter((parameter) => parameter.in === 'body').map((parameter) => `--${parameter.flagName}`);
251
- sections.push(bodyFlags.length
252
- ? `Request body: use body field flags (${bodyFlags.join(', ')}) or pass raw JSON via \`--body\` / \`--body-file\`.`
253
- : 'Request body: JSON via `--body` or `--body-file`.');
312
+ if (operation.requestContentType === 'multipart/form-data') {
313
+ sections.push(bodyFlags.length ? `Request body: multipart form fields (${bodyFlags.join(', ')}).` : 'Request body: multipart form data.');
314
+ }
315
+ else {
316
+ sections.push(bodyFlags.length
317
+ ? `Request body: use body field flags (${bodyFlags.join(', ')}) or pass raw JSON via \`--body\` / \`--body-file\`.`
318
+ : 'Request body: JSON via `--body` or `--body-file`.');
319
+ }
320
+ }
321
+ if (operation.responseType === 'binary') {
322
+ sections.push('Response body: binary download written to `--output`.');
254
323
  }
255
324
  return sections.join('\n\n');
256
325
  }
@@ -357,6 +426,8 @@ export async function generateRuntime(document, configFile, baseUrl) {
357
426
  const bodyParameters = extractBodyParameters(operation.requestBody, usedFlagNames);
358
427
  const allParameters = [...parameters, ...bodyParameters];
359
428
  const hasBody = Boolean(operation.requestBody && !('$ref' in operation.requestBody));
429
+ const requestContentType = getRequestContentType(operation.requestBody);
430
+ const responseType = getResponseType(operation);
360
431
  const moduleDisplayName = moduleConfig.name ?? moduleKey;
361
432
  const moduleDescription = moduleConfig.description;
362
433
  const resourceDisplayName = resourceConfig?.name ?? resourceKey;
@@ -366,9 +437,11 @@ export async function generateRuntime(document, configFile, baseUrl) {
366
437
  description: operation.description,
367
438
  });
368
439
  const resourceSegments = toResourceSegments(pathTemplate);
369
- const mappedResourceSegments = resourceSegments.length && resourceConfig?.name
370
- ? [toKebabCase(resourceConfig.name), ...resourceSegments.slice(1)]
371
- : resourceSegments;
440
+ const mappedResourceSegments = resourceSegments.length && resourceConfig?.segments?.length
441
+ ? [...resourceConfig.segments.map(toKebabCase), ...resourceSegments.slice(1)]
442
+ : resourceSegments.length && resourceConfig?.name
443
+ ? [toKebabCase(resourceConfig.name), ...resourceSegments.slice(1)]
444
+ : resourceSegments;
372
445
  const segments = [
373
446
  ...(resourceConfig?.topLevel ? [] : [toKebabCase(moduleDisplayName)]),
374
447
  ...mappedResourceSegments,
@@ -397,15 +470,21 @@ export async function generateRuntime(document, configFile, baseUrl) {
397
470
  tags: operation.tags,
398
471
  description: operationText.description,
399
472
  hasBody,
473
+ requestContentType,
474
+ responseType,
400
475
  parameters: allParameters,
401
476
  }),
402
477
  examples: buildExamples(segments.join(' '), {
403
478
  parameters: allParameters,
404
479
  hasBody,
480
+ requestContentType,
481
+ responseType,
405
482
  }),
406
483
  parameters: allParameters,
407
484
  hasBody,
408
485
  bodyRequired: operation.requestBody && !('$ref' in operation.requestBody) ? operation.requestBody.required : undefined,
486
+ requestContentType,
487
+ responseType,
409
488
  });
410
489
  }
411
490
  const schemaHash = createHash('sha1').update(JSON.stringify(document)).digest('hex').slice(0, 8);
@@ -7,11 +7,14 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
10
11
  import path from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
13
+ import { resolveCliHomeDir } from './cli-home.js';
12
14
  import { commandOutput, run } from './run-npm.js';
13
15
  const DEFAULT_PACKAGE_NAME = '@nocobase/cli';
14
16
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+ const INSTALL_METHOD_CACHE_FILE = 'self-install-methods.json';
15
18
  function normalizePath(value) {
16
19
  return path.resolve(value);
17
20
  }
@@ -115,6 +118,38 @@ function detectInstallMethod(packageRoot, globalPrefix) {
115
118
  }
116
119
  return 'unknown';
117
120
  }
121
+ function getInstallMethodCacheFile() {
122
+ return path.join(resolveCliHomeDir('global'), INSTALL_METHOD_CACHE_FILE);
123
+ }
124
+ function getInstallMethodCacheKey(packageRoot) {
125
+ return normalizePath(path.join(packageRoot, 'bin', 'run.js'));
126
+ }
127
+ async function readInstallMethodCache() {
128
+ try {
129
+ const raw = await fsp.readFile(getInstallMethodCacheFile(), 'utf8');
130
+ return JSON.parse(raw);
131
+ }
132
+ catch {
133
+ return {};
134
+ }
135
+ }
136
+ async function writeInstallMethodCache(state) {
137
+ const filePath = getInstallMethodCacheFile();
138
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
139
+ await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
140
+ }
141
+ async function readCachedInstallMethod(packageRoot) {
142
+ const state = await readInstallMethodCache();
143
+ return state.entries?.[getInstallMethodCacheKey(packageRoot)];
144
+ }
145
+ async function writeCachedInstallMethod(packageRoot, entry) {
146
+ const state = await readInstallMethodCache();
147
+ const entries = {
148
+ ...(state.entries ?? {}),
149
+ [getInstallMethodCacheKey(packageRoot)]: entry,
150
+ };
151
+ await writeInstallMethodCache({ entries });
152
+ }
118
153
  async function readGlobalPrefix(commandOutputFn) {
119
154
  try {
120
155
  return (await commandOutputFn('npm', ['prefix', '-g'], {
@@ -177,14 +212,34 @@ export function formatSelfUpdateUnavailableMessage(status) {
177
212
  export function getSelfUpdatePackageSpec(status) {
178
213
  return `${status.packageName}@${status.channel}`;
179
214
  }
215
+ export async function inspectSelfInstall(options = {}) {
216
+ const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
217
+ const commandOutputFn = options.commandOutputFn ?? commandOutput;
218
+ const cachedInstallMethod = await readCachedInstallMethod(packageRoot);
219
+ const globalPrefix = cachedInstallMethod?.globalPrefix ?? await readGlobalPrefix(commandOutputFn);
220
+ const installMethod = cachedInstallMethod?.installMethod ?? detectInstallMethod(packageRoot, globalPrefix);
221
+ if (!cachedInstallMethod) {
222
+ await writeCachedInstallMethod(packageRoot, {
223
+ installMethod,
224
+ globalPrefix,
225
+ });
226
+ }
227
+ return {
228
+ packageRoot,
229
+ installMethod,
230
+ globalPrefix,
231
+ };
232
+ }
180
233
  export async function inspectSelfStatus(options = {}) {
181
234
  const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
182
235
  const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
183
236
  const currentVersion = options.currentVersion ?? readCurrentVersion(packageRoot);
184
237
  const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
185
238
  const commandOutputFn = options.commandOutputFn ?? commandOutput;
186
- const globalPrefix = await readGlobalPrefix(commandOutputFn);
187
- const installMethod = detectInstallMethod(packageRoot, globalPrefix);
239
+ const { installMethod, globalPrefix } = await inspectSelfInstall({
240
+ packageRoot,
241
+ commandOutputFn,
242
+ });
188
243
  let latestVersion;
189
244
  let registryError;
190
245
  try {
@@ -180,12 +180,6 @@ export async function inspectSkillsStatus(options = {}) {
180
180
  registryError,
181
181
  };
182
182
  }
183
- function formatSkillsNotInstalledMessage() {
184
- return [
185
- 'NocoBase AI coding skills are not installed globally.',
186
- 'Run `nb skills install` first.',
187
- ].join('\n');
188
- }
189
183
  async function persistManagedSkillsState(globalRoot, options = {}) {
190
184
  const installedSkills = await listGlobalSkills({
191
185
  globalRoot,
@@ -250,7 +244,11 @@ export async function updateNocoBaseSkills(options = {}) {
250
244
  commandOutputFn: options.commandOutputFn,
251
245
  });
252
246
  if (!status.installed) {
253
- throw new Error(formatSkillsNotInstalledMessage());
247
+ return {
248
+ action: 'noop',
249
+ reason: 'not-installed',
250
+ status,
251
+ };
254
252
  }
255
253
  if (status.managedByNb
256
254
  && status.latestVersion
@@ -258,6 +256,7 @@ export async function updateNocoBaseSkills(options = {}) {
258
256
  && compareVersions(status.latestVersion, status.installedVersion) <= 0) {
259
257
  return {
260
258
  action: 'noop',
259
+ reason: 'up-to-date',
261
260
  status,
262
261
  };
263
262
  }
@@ -267,3 +266,31 @@ export async function updateNocoBaseSkills(options = {}) {
267
266
  status: await persistManagedSkillsState(globalRoot, options),
268
267
  };
269
268
  }
269
+ export async function removeNocoBaseSkills(options = {}) {
270
+ const globalRoot = resolveSkillsRoot(options);
271
+ const status = await inspectSkillsStatus({
272
+ globalRoot,
273
+ commandOutputFn: options.commandOutputFn,
274
+ });
275
+ if (!status.installed || status.installedSkillNames.length === 0) {
276
+ return {
277
+ action: 'noop',
278
+ status,
279
+ };
280
+ }
281
+ for (const skillName of status.installedSkillNames) {
282
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'remove', skillName, '-g', '-y'], {
283
+ cwd: globalRoot,
284
+ stdio: options.verbose ? 'inherit' : 'ignore',
285
+ errorName: 'skills remove',
286
+ });
287
+ }
288
+ await fsp.rm(getManagedSkillsStateFile(globalRoot), { force: true });
289
+ return {
290
+ action: 'removed',
291
+ status: await inspectSkillsStatus({
292
+ globalRoot,
293
+ commandOutputFn: options.commandOutputFn,
294
+ }),
295
+ };
296
+ }