@nocobase/cli 2.1.0-beta.23 → 2.1.0-beta.25

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 (61) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +12 -6
  4. package/dist/commands/app/logs.js +2 -2
  5. package/dist/commands/app/start.js +2 -1
  6. package/dist/commands/app/stop.js +2 -1
  7. package/dist/commands/app/upgrade.js +116 -129
  8. package/dist/commands/config/delete.js +30 -0
  9. package/dist/commands/config/get.js +29 -0
  10. package/dist/commands/config/index.js +20 -0
  11. package/dist/commands/config/list.js +29 -0
  12. package/dist/commands/config/set.js +35 -0
  13. package/dist/commands/db/check.js +238 -0
  14. package/dist/commands/db/logs.js +2 -2
  15. package/dist/commands/db/shared.js +6 -5
  16. package/dist/commands/db/start.js +2 -1
  17. package/dist/commands/db/stop.js +2 -1
  18. package/dist/commands/env/info.js +6 -2
  19. package/dist/commands/env/shared.js +1 -1
  20. package/dist/commands/init.js +0 -1
  21. package/dist/commands/install.js +87 -35
  22. package/dist/commands/license/activate.js +360 -0
  23. package/dist/commands/license/env.js +94 -0
  24. package/dist/commands/license/generate-id.js +108 -0
  25. package/dist/commands/license/id.js +56 -0
  26. package/dist/commands/license/index.js +20 -0
  27. package/dist/commands/license/plugins/clean.js +101 -0
  28. package/dist/commands/license/plugins/index.js +20 -0
  29. package/dist/commands/license/plugins/list.js +50 -0
  30. package/dist/commands/license/plugins/shared.js +325 -0
  31. package/dist/commands/license/plugins/sync.js +269 -0
  32. package/dist/commands/license/shared.js +414 -0
  33. package/dist/commands/license/status.js +50 -0
  34. package/dist/commands/plugin/disable.js +2 -0
  35. package/dist/commands/plugin/enable.js +2 -0
  36. package/dist/commands/source/dev.js +2 -1
  37. package/dist/lib/api-client.js +74 -3
  38. package/dist/lib/app-managed-resources.js +10 -6
  39. package/dist/lib/app-runtime.js +29 -11
  40. package/dist/lib/auth-store.js +36 -68
  41. package/dist/lib/bootstrap.js +0 -4
  42. package/dist/lib/build-config.js +8 -0
  43. package/dist/lib/builtin-db.js +86 -0
  44. package/dist/lib/cli-config.js +176 -0
  45. package/dist/lib/cli-home.js +6 -21
  46. package/dist/lib/db-connection-check.js +178 -0
  47. package/dist/lib/env-config.js +7 -0
  48. package/dist/lib/generated-command.js +24 -3
  49. package/dist/lib/plugin-storage.js +127 -0
  50. package/dist/lib/prompt-validators.js +4 -4
  51. package/dist/lib/run-npm.js +53 -0
  52. package/dist/lib/runtime-env-vars.js +32 -0
  53. package/dist/lib/runtime-generator.js +89 -10
  54. package/dist/lib/self-manager.js +57 -2
  55. package/dist/lib/skills-manager.js +2 -2
  56. package/dist/lib/startup-update.js +85 -7
  57. package/dist/lib/ui.js +3 -0
  58. package/dist/locale/en-US.json +16 -13
  59. package/dist/locale/zh-CN.json +16 -13
  60. package/nocobase-ctl.config.json +82 -0
  61. package/package.json +16 -4
@@ -0,0 +1,414 @@
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 { Flags } from '@oclif/core';
10
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { getEnvAsync, getInstanceIdAsync, keyDecrypt } from '@nocobase/license-kit';
13
+ import _ from 'lodash';
14
+ import { checkExternalDbConnection, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
15
+ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
16
+ import { buildRuntimeEnvVars } from '../../lib/runtime-env-vars.js';
17
+ import { resolveLicensePkgUrlFromConfig } from '../../lib/cli-config.js';
18
+ import { commandOutput } from '../../lib/run-npm.js';
19
+ import { appUrl } from '../env/shared.js';
20
+ export const licenseEnvFlag = Flags.string({
21
+ char: 'e',
22
+ description: 'CLI env name (from `nb env` / `nb init`). Defaults to the current env when omitted',
23
+ });
24
+ export const licenseJsonFlag = Flags.boolean({
25
+ description: 'Output the result as JSON',
26
+ default: false,
27
+ });
28
+ const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
29
+ const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
30
+ const DEFAULT_DOCKER_VERSION = 'alpha';
31
+ export const licensePkgUrlFlag = Flags.string({
32
+ description: 'Commercial package service base URL',
33
+ hidden: true,
34
+ });
35
+ export async function requireLicenseRuntime(envName) {
36
+ const runtime = await resolveManagedAppRuntime(envName);
37
+ if (!runtime) {
38
+ throw new Error(formatMissingManagedAppEnvMessage(envName));
39
+ }
40
+ return runtime;
41
+ }
42
+ export function resolveLicenseDir(runtime) {
43
+ return path.resolve(runtime.env.storagePath, '.license');
44
+ }
45
+ export function resolveInstanceIdFile(runtime) {
46
+ return path.resolve(resolveLicenseDir(runtime), 'instance-id');
47
+ }
48
+ export function resolveLicenseKeyFile(runtime) {
49
+ return path.resolve(resolveLicenseDir(runtime), 'license-key');
50
+ }
51
+ export async function readSavedInstanceId(runtime) {
52
+ try {
53
+ const value = await readFile(resolveInstanceIdFile(runtime), 'utf8');
54
+ const normalized = value.trim();
55
+ return normalized || undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function trimValue(value) {
62
+ const text = String(value ?? '').trim();
63
+ return text || undefined;
64
+ }
65
+ function normalizeDockerPlatform(value) {
66
+ const text = trimValue(value);
67
+ if (!text || text === 'auto') {
68
+ return undefined;
69
+ }
70
+ if (text === 'linux/amd64' || text === 'linux/arm64') {
71
+ return text;
72
+ }
73
+ return undefined;
74
+ }
75
+ function resolveDockerLicenseImageRef(runtime) {
76
+ const config = runtime.env.config ?? {};
77
+ return `${trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY}:${trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION}`;
78
+ }
79
+ function buildDockerLicenseDbFlagArgs(envVars) {
80
+ return [
81
+ '--db-dialect',
82
+ String(envVars.DB_DIALECT ?? ''),
83
+ '--db-host',
84
+ String(envVars.DB_HOST ?? ''),
85
+ '--db-port',
86
+ String(envVars.DB_PORT ?? ''),
87
+ '--db-database',
88
+ String(envVars.DB_DATABASE ?? ''),
89
+ '--db-user',
90
+ String(envVars.DB_USER ?? ''),
91
+ '--db-password',
92
+ String(envVars.DB_PASSWORD ?? ''),
93
+ ];
94
+ }
95
+ async function runDockerLicenseJsonCommand(runtime, commandArgs) {
96
+ const args = [
97
+ 'run',
98
+ '--rm',
99
+ '--network',
100
+ runtime.dockerNetworkName || runtime.workspaceName,
101
+ ];
102
+ const dockerPlatform = normalizeDockerPlatform(runtime.env.config?.dockerPlatform);
103
+ if (dockerPlatform) {
104
+ args.push('--platform', dockerPlatform);
105
+ }
106
+ args.push('--entrypoint', 'nb', resolveDockerLicenseImageRef(runtime), ...commandArgs, '--json');
107
+ const output = await commandOutput('docker', args, {
108
+ errorName: 'docker run',
109
+ });
110
+ try {
111
+ return JSON.parse(output);
112
+ }
113
+ catch {
114
+ throw new Error(`Failed to parse Docker license command response: ${output}`);
115
+ }
116
+ }
117
+ function buildLicenseDbConfigFromEnvVars(envVars) {
118
+ return {
119
+ builtinDb: false,
120
+ dbDialect: trimValue(envVars.DB_DIALECT),
121
+ dbHost: trimValue(envVars.DB_HOST),
122
+ dbPort: trimValue(envVars.DB_PORT),
123
+ dbDatabase: trimValue(envVars.DB_DATABASE),
124
+ dbUser: trimValue(envVars.DB_USER),
125
+ dbPassword: envVars.DB_PASSWORD,
126
+ };
127
+ }
128
+ export async function validateLicenseDbConnectionFromEnvVars(envVars) {
129
+ const connectionConfig = readExternalDbConnectionConfig(buildLicenseDbConfigFromEnvVars(envVars));
130
+ if (!connectionConfig) {
131
+ throw new Error('Unsupported or incomplete database settings for instance ID generation.');
132
+ }
133
+ const validationError = await checkExternalDbConnection(connectionConfig);
134
+ if (validationError) {
135
+ throw new Error(validationError);
136
+ }
137
+ }
138
+ export async function withLicenseEnvVars(nextEnv, task) {
139
+ const previous = {};
140
+ for (const [key, value] of Object.entries(nextEnv)) {
141
+ previous[key] = process.env[key];
142
+ process.env[key] = value;
143
+ }
144
+ try {
145
+ return await task();
146
+ }
147
+ finally {
148
+ for (const key of Object.keys(nextEnv)) {
149
+ const value = previous[key];
150
+ if (value === undefined) {
151
+ delete process.env[key];
152
+ }
153
+ else {
154
+ process.env[key] = value;
155
+ }
156
+ }
157
+ }
158
+ }
159
+ async function withLicenseEnv(runtime, task) {
160
+ return await withLicenseEnvVars(await buildRuntimeEnvVars(runtime), task);
161
+ }
162
+ export async function getCurrentLicenseEnv(runtime) {
163
+ if (runtime.kind === 'docker') {
164
+ const envVars = await buildRuntimeEnvVars(runtime);
165
+ const payload = await runDockerLicenseJsonCommand(runtime, [
166
+ 'license',
167
+ 'env',
168
+ ...buildDockerLicenseDbFlagArgs(envVars),
169
+ ]);
170
+ return payload?.env;
171
+ }
172
+ return await withLicenseEnv(runtime, async () => await getEnvAsync());
173
+ }
174
+ export async function generateInstanceIdFromEnvVars(envVars) {
175
+ const instanceId = String(await withLicenseEnvVars(envVars, async () => await getInstanceIdAsync())).trim();
176
+ if (!instanceId) {
177
+ throw new Error('Generated instance ID is empty.');
178
+ }
179
+ return instanceId;
180
+ }
181
+ export async function generateValidatedInstanceIdFromEnvVars(envVars) {
182
+ await validateLicenseDbConnectionFromEnvVars(envVars);
183
+ return await generateInstanceIdFromEnvVars(envVars);
184
+ }
185
+ async function generateInstanceIdForDockerRuntime(runtime) {
186
+ const envVars = await buildRuntimeEnvVars(runtime);
187
+ const payload = await runDockerLicenseJsonCommand(runtime, [
188
+ 'license',
189
+ 'generate-id',
190
+ ...buildDockerLicenseDbFlagArgs(envVars),
191
+ ]);
192
+ const instanceId = trimValue(payload.instanceId);
193
+ if (!instanceId) {
194
+ throw new Error('Docker instance ID generation did not return an instance ID.');
195
+ }
196
+ return instanceId;
197
+ }
198
+ export async function generateInstanceIdForRuntime(runtime) {
199
+ if (runtime.kind === 'docker') {
200
+ return await generateInstanceIdForDockerRuntime(runtime);
201
+ }
202
+ if (runtime.kind === 'local') {
203
+ return await generateValidatedInstanceIdFromEnvVars(await buildRuntimeEnvVars(runtime));
204
+ }
205
+ throw new Error(`Env "${runtime.envName}" does not support automatic instance ID generation.`);
206
+ }
207
+ export async function saveInstanceId(runtime, instanceId) {
208
+ const normalized = String(instanceId ?? '').trim();
209
+ if (!normalized) {
210
+ throw new Error('Generated instance ID is empty.');
211
+ }
212
+ await mkdir(resolveLicenseDir(runtime), { recursive: true });
213
+ await writeFile(resolveInstanceIdFile(runtime), `${normalized}\n`);
214
+ return normalized;
215
+ }
216
+ export async function generateAndSaveInstanceId(runtime) {
217
+ const instanceId = await generateInstanceIdForRuntime(runtime);
218
+ return await saveInstanceId(runtime, instanceId);
219
+ }
220
+ export async function ensureInstanceId(runtime, options = {}) {
221
+ if (!options.force) {
222
+ const saved = await readSavedInstanceId(runtime);
223
+ if (saved) {
224
+ return saved;
225
+ }
226
+ }
227
+ return await generateAndSaveInstanceId(runtime);
228
+ }
229
+ export function parseLicenseKey(key) {
230
+ try {
231
+ return JSON.parse(keyDecrypt(key));
232
+ }
233
+ catch {
234
+ throw new Error('invalid');
235
+ }
236
+ }
237
+ export async function saveLicenseKey(runtime, key) {
238
+ await mkdir(resolveLicenseDir(runtime), { recursive: true });
239
+ const filePath = resolveLicenseKeyFile(runtime);
240
+ await writeFile(filePath, key.trim());
241
+ return filePath;
242
+ }
243
+ export async function readSavedLicenseKey(runtime) {
244
+ try {
245
+ const value = await readFile(resolveLicenseKeyFile(runtime), 'utf8');
246
+ const normalized = value.trim();
247
+ return normalized || undefined;
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ }
253
+ function matchSingleDomain(licenseDomain, currentDomain) {
254
+ let hostname = '';
255
+ let port = '';
256
+ try {
257
+ const url = new URL(currentDomain);
258
+ hostname = url.hostname;
259
+ port = url.port ? `:${url.port}` : '';
260
+ }
261
+ catch {
262
+ return false;
263
+ }
264
+ const fullDomain = hostname + port;
265
+ if (!licenseDomain.includes('*')) {
266
+ return fullDomain === licenseDomain;
267
+ }
268
+ const base = licenseDomain.replace('*', '');
269
+ return fullDomain.endsWith(base);
270
+ }
271
+ export function isDomainMatch(currentDomain, keyData) {
272
+ if (!keyData?.licenseKey?.domain || !currentDomain) {
273
+ return false;
274
+ }
275
+ const licenseDomains = String(keyData.licenseKey.domain)
276
+ .split(',')
277
+ .map((value) => value.trim())
278
+ .filter(Boolean);
279
+ return licenseDomains.some((licenseDomain) => matchSingleDomain(licenseDomain, currentDomain));
280
+ }
281
+ export function isDbMatch(env, keyData) {
282
+ const currentDb = env?.db;
283
+ const licenseDb = keyData?.instanceData?.db;
284
+ if (!currentDb || !licenseDb) {
285
+ return false;
286
+ }
287
+ if (currentDb?.id && licenseDb?.id) {
288
+ return currentDb.id === licenseDb.id;
289
+ }
290
+ return _.isEqual(_.omit(currentDb, ['id']), _.omit(licenseDb, ['id']));
291
+ }
292
+ export function isSysMatch(env, keyData) {
293
+ const instance = keyData?.instanceData;
294
+ if (!env || !instance) {
295
+ return false;
296
+ }
297
+ const normalize = (item) => ({
298
+ sys: item?.sys ?? null,
299
+ osVer: item?.osVer ?? null,
300
+ });
301
+ return _.isEqual(normalize(env), normalize(instance));
302
+ }
303
+ export async function getLicenseStatus(keyData) {
304
+ if (!keyData) {
305
+ return 'invalid';
306
+ }
307
+ if (keyData.licenseKey?.licenseStatus === 'invalid') {
308
+ return 'invalid';
309
+ }
310
+ const domain = String(keyData.service?.domain ?? '').trim();
311
+ const accessKeyId = String(keyData.accessKeyId ?? '').trim();
312
+ const accessKeySecret = String(keyData.accessKeySecret ?? '').trim();
313
+ if (!domain || !accessKeyId || !accessKeySecret) {
314
+ return 'active';
315
+ }
316
+ const controller = new AbortController();
317
+ const timeout = setTimeout(() => controller.abort(), 5000);
318
+ try {
319
+ const response = await fetch(`${domain.replace(/\/$/, '')}/api/license_keys:getKeyStatus`, {
320
+ method: 'POST',
321
+ headers: {
322
+ 'Content-Type': 'application/json',
323
+ ...(keyData.service?.headers ?? {}),
324
+ },
325
+ body: JSON.stringify({
326
+ access_key_id: accessKeyId,
327
+ access_key_secret: accessKeySecret,
328
+ }),
329
+ signal: controller.signal,
330
+ });
331
+ const payload = await response.json();
332
+ return payload?.data?.status === 'active' ? 'active' : 'invalid';
333
+ }
334
+ catch {
335
+ return 'active';
336
+ }
337
+ finally {
338
+ clearTimeout(timeout);
339
+ }
340
+ }
341
+ export async function validateLicenseKey(runtime, key) {
342
+ let keyData;
343
+ let keyStatus;
344
+ try {
345
+ keyData = parseLicenseKey(key);
346
+ }
347
+ catch {
348
+ keyStatus = 'invalid';
349
+ }
350
+ const currentEnv = await getCurrentLicenseEnv(runtime);
351
+ const currentDomain = appUrl(runtime);
352
+ const dbMatch = isDbMatch(currentEnv, keyData);
353
+ const sysMatch = isSysMatch(currentEnv, keyData);
354
+ const envMatch = dbMatch && sysMatch;
355
+ const domainMatch = isDomainMatch(currentDomain, keyData);
356
+ const licenseStatus = await getLicenseStatus(keyData);
357
+ return {
358
+ current: {
359
+ env: currentEnv,
360
+ domain: currentDomain ? new URL(currentDomain).host : '',
361
+ },
362
+ keyData,
363
+ keyStatus,
364
+ dbMatch,
365
+ sysMatch,
366
+ envMatch,
367
+ domainMatch,
368
+ licenseStatus,
369
+ };
370
+ }
371
+ export function redactLicenseKey(value) {
372
+ const text = String(value ?? '').trim();
373
+ if (!text) {
374
+ return '';
375
+ }
376
+ if (text.length <= 8) {
377
+ return '*'.repeat(text.length);
378
+ }
379
+ return `${text.slice(0, 4)}...${text.slice(-4)}`;
380
+ }
381
+ export async function resolveLicenseServiceUrl(value) {
382
+ return (await resolveLicensePkgUrl(value)).replace(/\/+$/, '');
383
+ }
384
+ export async function resolveLicensePkgUrl(value) {
385
+ const normalized = String(value ?? '').trim() || await resolveLicensePkgUrlFromConfig();
386
+ return normalized.replace(/\/+$/, '') + '/';
387
+ }
388
+ function shouldRedactOutputKey(key) {
389
+ return /accesskeyid|accesskeysecret|secret|token|password|authorization/i.test(key);
390
+ }
391
+ function redactOutputValue(value) {
392
+ const text = String(value ?? '').trim();
393
+ if (!text) {
394
+ return '';
395
+ }
396
+ if (text.length <= 8) {
397
+ return '*'.repeat(text.length);
398
+ }
399
+ return `${text.slice(0, 2)}***${text.slice(-2)}`;
400
+ }
401
+ export function sanitizeLicenseOutput(value) {
402
+ if (Array.isArray(value)) {
403
+ return value.map((item) => sanitizeLicenseOutput(item));
404
+ }
405
+ if (value && typeof value === 'object') {
406
+ return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
407
+ key,
408
+ shouldRedactOutputKey(key)
409
+ ? redactOutputValue(String(nestedValue ?? ''))
410
+ : sanitizeLicenseOutput(nestedValue),
411
+ ]));
412
+ }
413
+ return value;
414
+ }
@@ -0,0 +1,50 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { ensureInstanceId, licenseEnvFlag, licenseJsonFlag, requireLicenseRuntime } from './shared.js';
11
+ export default class LicenseStatus extends Command {
12
+ static summary = 'Show commercial license status for the selected env';
13
+ static description = 'Inspect the selected env and show the current commercial licensing status. Use `--doctor` for extra diagnostic checks once the license backend wiring is implemented.';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %>',
16
+ '<%= config.bin %> <%= command.id %> --env app1',
17
+ '<%= config.bin %> <%= command.id %> --env app1 --doctor',
18
+ '<%= config.bin %> <%= command.id %> --env app1 --json',
19
+ ];
20
+ static flags = {
21
+ env: licenseEnvFlag,
22
+ json: licenseJsonFlag,
23
+ doctor: Flags.boolean({
24
+ description: 'Run extra diagnostic checks and suggestions',
25
+ default: false,
26
+ }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(LicenseStatus);
30
+ const runtime = await requireLicenseRuntime(flags.env);
31
+ const payload = {
32
+ ok: true,
33
+ env: runtime.envName,
34
+ kind: runtime.kind,
35
+ instanceId: await ensureInstanceId(runtime),
36
+ licensed: false,
37
+ doctor: Boolean(flags.doctor),
38
+ implemented: false,
39
+ message: 'Commercial license status is not implemented yet in the new CLI.',
40
+ };
41
+ if (flags.json) {
42
+ this.log(JSON.stringify(payload, null, 2));
43
+ return;
44
+ }
45
+ this.log(`License status for env "${runtime.envName}": not implemented yet`);
46
+ if (flags.doctor) {
47
+ this.log('Diagnostic checks for commercial licensing are not implemented yet in the new CLI.');
48
+ }
49
+ }
50
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { announceTargetEnv } from '../../lib/ui.js';
11
12
  export default class PluginDisable extends Command {
12
13
  static hidden = false;
13
14
  static args = {
@@ -39,6 +40,7 @@ export default class PluginDisable extends Command {
39
40
  if (!runtime) {
40
41
  this.error(formatMissingManagedAppEnvMessage(flags.env));
41
42
  }
43
+ announceTargetEnv(runtime.envName);
42
44
  if (runtime.kind === 'local') {
43
45
  try {
44
46
  await runLocalNocoBaseCommand(runtime, ['pm', 'disable', ...packages]);
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { announceTargetEnv } from '../../lib/ui.js';
11
12
  export default class PluginEnable extends Command {
12
13
  static hidden = false;
13
14
  static args = {
@@ -39,6 +40,7 @@ export default class PluginEnable extends Command {
39
40
  if (!runtime) {
40
41
  this.error(formatMissingManagedAppEnvMessage(flags.env));
41
42
  }
43
+ announceTargetEnv(runtime.envName);
42
44
  if (runtime.kind === 'local') {
43
45
  try {
44
46
  await runLocalNocoBaseCommand(runtime, ['pm', 'enable', ...packages]);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
- import { printInfo } from '../../lib/ui.js';
11
+ import { announceTargetEnv, printInfo } from '../../lib/ui.js';
12
12
  function formatUnsupportedRuntimeMessage(kind, envName) {
13
13
  if (kind === 'docker') {
14
14
  return [
@@ -110,6 +110,7 @@ export default class SourceDev extends Command {
110
110
  if (runtime.kind === 'docker' || runtime.kind === 'http' || runtime.kind === 'ssh') {
111
111
  this.error(formatUnsupportedRuntimeMessage(runtime.kind, runtime.envName));
112
112
  }
113
+ announceTargetEnv(runtime.envName);
113
114
  const devPort = flags.port
114
115
  || (runtime.env.appPort !== undefined && runtime.env.appPort !== null
115
116
  ? String(runtime.env.appPort).trim()
@@ -6,7 +6,19 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ /**
10
+ * This file is part of the NocoBase (R) project.
11
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
12
+ * Authors: NocoBase Team.
13
+ *
14
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
15
+ * For more information, please refer to: https://www.nocobase.com/agreement.
16
+ */
17
+ import { createWriteStream } from 'node:fs';
9
18
  import { promises as fs } from 'node:fs';
19
+ import { basename, dirname } from 'node:path';
20
+ import { Readable } from 'node:stream';
21
+ import { pipeline } from 'node:stream/promises';
10
22
  import { resolveServerRequestTarget } from './env-auth.js';
11
23
  import { fetchWithPreservedAuthRedirect } from './http-request.js';
12
24
  const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
@@ -43,6 +55,20 @@ async function parseResponse(response) {
43
55
  data,
44
56
  };
45
57
  }
58
+ async function parseBinaryResponse(response, outputPath) {
59
+ if (response.ok && response.body) {
60
+ await fs.mkdir(dirname(outputPath), { recursive: true }).catch(() => undefined);
61
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(outputPath));
62
+ return {
63
+ ok: response.ok,
64
+ status: response.status,
65
+ data: {
66
+ output: outputPath,
67
+ },
68
+ };
69
+ }
70
+ return parseResponse(response);
71
+ }
46
72
  function parseScalarValue(value, type) {
47
73
  if (value === undefined) {
48
74
  return undefined;
@@ -105,6 +131,9 @@ function parseBodyFieldValue(rawValue, parameter) {
105
131
  return parseScalarValue(rawValue, parameter.type);
106
132
  }
107
133
  export async function parseBody(flags, operation) {
134
+ if (operation.requestContentType === 'multipart/form-data') {
135
+ return undefined;
136
+ }
108
137
  const inlineBody = flags.body;
109
138
  const bodyFile = flags['body-file'];
110
139
  const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
@@ -143,6 +172,39 @@ export async function parseBody(flags, operation) {
143
172
  }
144
173
  return undefined;
145
174
  }
175
+ async function createMultipartBody(flags, operation) {
176
+ const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
177
+ const formData = new FormData();
178
+ let hasValues = false;
179
+ for (const parameter of bodyParameters) {
180
+ const rawValue = flags[parameter.flagName];
181
+ const hasValue = hasParameterValue(flags, parameter);
182
+ if (parameter.required && !hasValue) {
183
+ throw new Error(`Missing required body field --${parameter.flagName}`);
184
+ }
185
+ if (!hasValue) {
186
+ continue;
187
+ }
188
+ if (parameter.isFile) {
189
+ const filePath = String(rawValue);
190
+ const content = await fs.readFile(filePath);
191
+ const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
192
+ formData.append(parameter.name, new Blob([arrayBuffer]), basename(filePath));
193
+ hasValues = true;
194
+ continue;
195
+ }
196
+ const value = parseBodyFieldValue(rawValue, parameter);
197
+ if (value === undefined) {
198
+ continue;
199
+ }
200
+ formData.append(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
201
+ hasValues = true;
202
+ }
203
+ if (!hasValues && operation.bodyRequired) {
204
+ throw new Error('Missing multipart request body.');
205
+ }
206
+ return hasValues ? formData : undefined;
207
+ }
146
208
  export async function executeApiRequest(options) {
147
209
  const { baseUrl, token } = await resolveServerRequestTarget(options);
148
210
  const headers = new Headers();
@@ -190,8 +252,10 @@ export async function executeApiRequest(options) {
190
252
  continue;
191
253
  }
192
254
  }
193
- const body = await parseBody(options.flags, options.operation);
194
- if (body !== undefined) {
255
+ const body = options.operation.requestContentType === 'multipart/form-data'
256
+ ? await createMultipartBody(options.flags, options.operation)
257
+ : await parseBody(options.flags, options.operation);
258
+ if (body !== undefined && options.operation.requestContentType !== 'multipart/form-data') {
195
259
  headers.set('content-type', 'application/json');
196
260
  }
197
261
  const url = new URL(`${normalizeBaseUrl(baseUrl)}${requestPath}`);
@@ -199,8 +263,15 @@ export async function executeApiRequest(options) {
199
263
  const response = await fetchWithPreservedAuthRedirect(url.toString(), {
200
264
  method: options.operation.method.toUpperCase(),
201
265
  headers,
202
- body: body === undefined ? undefined : JSON.stringify(body),
266
+ body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
203
267
  });
268
+ if (options.operation.responseType === 'binary') {
269
+ const outputPath = options.flags.output;
270
+ if (!outputPath) {
271
+ throw new Error('Missing required output path --output');
272
+ }
273
+ return parseBinaryResponse(response, outputPath);
274
+ }
204
275
  return parseResponse(response);
205
276
  }
206
277
  export async function executeRawApiRequest(options) {
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { mkdir, readdir } from 'node:fs/promises';
10
10
  import { dockerContainerExists, startDockerContainer } from './app-runtime.js';
11
+ import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
11
12
  import { resolveConfiguredEnvPath } from './cli-home.js';
12
13
  import { commandSucceeds, run } from './run-npm.js';
13
14
  import Install from '../commands/install.js';
@@ -92,9 +93,10 @@ export function buildSavedDockerRunArgs(runtime) {
92
93
  : trimValue(runtime.env.appPort);
93
94
  const appKey = trimValue(config.appKey);
94
95
  const timeZone = trimValue(config.timezone) || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
95
- const dbDialect = trimValue(config.dbDialect);
96
- const dbHost = trimValue(config.dbHost);
97
- const dbPort = trimValue(config.dbPort);
96
+ const builtinDbConnection = config.builtinDb ? deriveBuiltinDbConnection(runtime) : undefined;
97
+ const dbDialect = builtinDbConnection?.dbDialect || trimValue(config.dbDialect);
98
+ const dbHost = builtinDbConnection?.dbHost || trimValue(config.dbHost);
99
+ const dbPort = builtinDbConnection?.dbPort || trimValue(config.dbPort);
98
100
  const dbDatabase = trimValue(config.dbDatabase);
99
101
  const dbUser = trimValue(config.dbUser);
100
102
  const dbPassword = trimValue(config.dbPassword);
@@ -178,14 +180,16 @@ export async function ensureBuiltinDbReady(runtime, options) {
178
180
  if (!config.builtinDb) {
179
181
  return;
180
182
  }
183
+ const builtinDbConnection = await resolveBuiltinDbConnection(runtime);
181
184
  const plan = Install.buildBuiltinDbPlan({
182
185
  envName: runtime.envName,
183
186
  workspaceName: runtime.workspaceName,
187
+ dockerContainerPrefix: runtime.dockerContainerPrefix,
184
188
  storagePath: config.storagePath,
185
189
  source: runtime.source,
186
- dbDialect: config.dbDialect,
187
- dbHost: config.dbHost,
188
- dbPort: config.dbPort,
190
+ dbDialect: builtinDbConnection.dbDialect,
191
+ dbHost: builtinDbConnection.dbHost,
192
+ dbPort: builtinDbConnection.dbPort,
189
193
  dbDatabase: config.dbDatabase,
190
194
  dbUser: config.dbUser,
191
195
  dbPassword: config.dbPassword,