@nocobase/cli 2.1.0-beta.43 → 2.1.0-beta.44.test.1

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 (101) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/dist/commands/app/autostart/disable.js +55 -0
  14. package/dist/commands/app/autostart/enable.js +55 -0
  15. package/dist/commands/app/autostart/list.js +37 -0
  16. package/dist/commands/app/autostart/run.js +84 -0
  17. package/dist/commands/app/autostart/shared.js +49 -0
  18. package/dist/commands/app/destroy.js +8 -6
  19. package/dist/commands/app/down.js +2 -2
  20. package/dist/commands/app/logs.js +2 -1
  21. package/dist/commands/app/restart.js +79 -23
  22. package/dist/commands/app/shared.js +1 -1
  23. package/dist/commands/app/start.js +134 -38
  24. package/dist/commands/app/stop.js +31 -2
  25. package/dist/commands/app/upgrade.js +3 -1
  26. package/dist/commands/config/delete.js +4 -1
  27. package/dist/commands/config/get.js +4 -1
  28. package/dist/commands/config/set.js +5 -2
  29. package/dist/commands/env/add.js +19 -39
  30. package/dist/commands/env/info.js +3 -2
  31. package/dist/commands/env/proxy/caddy.js +28 -0
  32. package/dist/commands/env/proxy/index.js +353 -0
  33. package/dist/commands/env/proxy/nginx.js +28 -0
  34. package/dist/commands/env/remove.js +112 -22
  35. package/dist/commands/env/shared.js +17 -9
  36. package/dist/commands/env/update.js +385 -21
  37. package/dist/commands/init.js +233 -91
  38. package/dist/commands/install.js +174 -68
  39. package/dist/commands/license/activate.js +63 -244
  40. package/dist/commands/license/plugins/shared.js +64 -13
  41. package/dist/commands/plugin/import.js +108 -0
  42. package/dist/commands/revision/create.js +89 -0
  43. package/dist/locale/en-US.json +105 -19
  44. package/dist/locale/zh-CN.json +102 -16
  45. package/package.json +5 -8
  46. package/scripts/build.mjs +34 -0
  47. package/scripts/clean.mjs +9 -0
  48. package/tsconfig.json +19 -0
  49. package/LICENSE.txt +0 -107
  50. package/README.zh-CN.md +0 -355
  51. package/dist/lib/api-client.js +0 -335
  52. package/dist/lib/api-command-compat.js +0 -641
  53. package/dist/lib/app-health.js +0 -139
  54. package/dist/lib/app-managed-resources.js +0 -316
  55. package/dist/lib/app-runtime.js +0 -180
  56. package/dist/lib/auth-store.js +0 -405
  57. package/dist/lib/backup.js +0 -171
  58. package/dist/lib/bootstrap.js +0 -409
  59. package/dist/lib/build-config.js +0 -18
  60. package/dist/lib/builtin-db.js +0 -86
  61. package/dist/lib/cli-config.js +0 -309
  62. package/dist/lib/cli-entry-error.js +0 -44
  63. package/dist/lib/cli-home.js +0 -47
  64. package/dist/lib/cli-locale.js +0 -141
  65. package/dist/lib/command-discovery.js +0 -39
  66. package/dist/lib/db-connection-check.js +0 -219
  67. package/dist/lib/docker-env-file.js +0 -52
  68. package/dist/lib/docker-image.js +0 -37
  69. package/dist/lib/docker-log-stream.js +0 -45
  70. package/dist/lib/env-auth.js +0 -960
  71. package/dist/lib/env-config.js +0 -95
  72. package/dist/lib/env-guard.js +0 -62
  73. package/dist/lib/generated-command.js +0 -203
  74. package/dist/lib/http-request.js +0 -49
  75. package/dist/lib/inquirer-theme.js +0 -17
  76. package/dist/lib/inquirer.js +0 -244
  77. package/dist/lib/naming.js +0 -70
  78. package/dist/lib/object-utils.js +0 -76
  79. package/dist/lib/openapi.js +0 -62
  80. package/dist/lib/plugin-storage.js +0 -64
  81. package/dist/lib/post-processors.js +0 -23
  82. package/dist/lib/prompt-catalog-core.js +0 -185
  83. package/dist/lib/prompt-catalog-terminal.js +0 -375
  84. package/dist/lib/prompt-catalog.js +0 -10
  85. package/dist/lib/prompt-validators.js +0 -258
  86. package/dist/lib/prompt-web-ui.js +0 -2227
  87. package/dist/lib/resource-command.js +0 -357
  88. package/dist/lib/resource-request.js +0 -104
  89. package/dist/lib/run-npm.js +0 -385
  90. package/dist/lib/runtime-env-vars.js +0 -32
  91. package/dist/lib/runtime-generator.js +0 -498
  92. package/dist/lib/runtime-store.js +0 -56
  93. package/dist/lib/self-manager.js +0 -301
  94. package/dist/lib/session-id.js +0 -17
  95. package/dist/lib/session-integration.js +0 -703
  96. package/dist/lib/session-store.js +0 -118
  97. package/dist/lib/skills-manager.js +0 -436
  98. package/dist/lib/source-publish.js +0 -309
  99. package/dist/lib/source-registry.js +0 -188
  100. package/dist/lib/startup-update.js +0 -309
  101. package/dist/lib/ui.js +0 -158
@@ -7,199 +7,98 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
+ import pc from 'picocolors';
10
11
  import { readFile } from 'node:fs/promises';
12
+ import { translateCli } from '../../lib/cli-locale.js';
11
13
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
12
14
  import { input, password as promptPassword, select } from "../../lib/inquirer.js";
13
- import { createLicenseEnvFlag, ensureInstanceId, licenseJsonFlag, licensePkgUrlFlag, licenseYesFlag, redactLicenseKey, requireLicenseRuntime, resolveLicenseKeyFile, resolveLicenseServiceUrl, saveLicenseKey, sanitizeLicenseOutput, validateLicenseKey, } from './shared.js';
15
+ import { createLicenseEnvFlag, ensureInstanceId, licenseJsonFlag, licenseYesFlag, redactLicenseKey, requireLicenseRuntime, resolveLicenseKeyFile, saveLicenseKey, sanitizeLicenseOutput, validateLicenseKey, } from './shared.js';
14
16
  import { announceTargetEnv, isInteractiveTerminal } from '../../lib/ui.js';
15
17
  import { appUrl } from '../env/shared.js';
16
- function resolveOnlineInputValue(value) {
17
- return String(value ?? '').trim();
18
- }
19
- async function promptActivationMode() {
18
+ const licenseActivateText = (key, values, fallback) => translateCli(`commands.license.activate.${key}`, values, { fallback });
19
+ function resolveHostnameNoticeValue(runtime) {
20
+ const currentAppUrl = String(appUrl(runtime) ?? '').trim();
21
+ if (!currentAppUrl) {
22
+ return;
23
+ }
20
24
  try {
21
- return await select({
22
- message: 'How do you want to activate the license?',
23
- choices: [
24
- { value: 'online', name: 'Request and activate a license online' },
25
- { value: 'key', name: 'Use an existing license key' },
26
- { value: 'cancel', name: 'Cancel' },
27
- ],
28
- default: 'online',
29
- });
25
+ const url = new URL(currentAppUrl);
26
+ return url.host || undefined;
30
27
  }
31
28
  catch {
32
- return 'cancel';
29
+ return currentAppUrl;
33
30
  }
34
31
  }
32
+ function formatInstanceIdNotice(instanceId, hostname) {
33
+ return [
34
+ '',
35
+ ...(hostname
36
+ ? [pc.cyan(pc.bold(licenseActivateText('interactive.notice.hostnameLabel'))), ` ${pc.bold(hostname)}`, '']
37
+ : []),
38
+ pc.cyan(pc.bold(licenseActivateText('interactive.notice.instanceIdLabel'))),
39
+ ` ${pc.bold(instanceId)}`,
40
+ pc.dim(` ${licenseActivateText(hostname ? 'interactive.notice.copyHintWithHostname' : 'interactive.notice.copyHintWithoutHostname')}`),
41
+ '',
42
+ ].join('\n');
43
+ }
35
44
  async function promptLicenseKeyInput() {
36
45
  let answer;
37
46
  try {
38
47
  answer = await select({
39
- message: 'How do you want to provide the license key?',
48
+ message: licenseActivateText('interactive.prompts.provideMethod.message'),
40
49
  choices: [
41
- { value: 'key', name: 'Paste the license key' },
42
- { value: 'file', name: 'Read the key from a file' },
50
+ { value: 'key', name: licenseActivateText('interactive.prompts.provideMethod.keyOption') },
51
+ { value: 'file', name: licenseActivateText('interactive.prompts.provideMethod.fileOption') },
43
52
  ],
44
53
  default: 'key',
45
54
  });
46
55
  }
47
56
  catch {
48
- return {};
57
+ return;
49
58
  }
50
59
  if (answer === 'key') {
51
60
  try {
52
- const key = await input({
53
- message: 'License key',
54
- validate: (value) => String(value ?? '').trim() ? true : 'License key is required.',
61
+ const key = await promptPassword({
62
+ message: licenseActivateText('interactive.prompts.key.message'),
63
+ mask: false,
64
+ transformer: (value) => licenseActivateText('interactive.prompts.key.transformer', { count: value.length }),
65
+ validate: (value) => String(value ?? '').trim() ? true : licenseActivateText('interactive.prompts.key.required'),
55
66
  });
56
67
  return { key: String(key ?? '').trim() || undefined };
57
68
  }
58
69
  catch {
59
- return {};
70
+ return;
60
71
  }
61
72
  }
62
73
  try {
63
74
  const keyFile = await input({
64
- message: 'Path to the license key file',
65
- validate: (value) => String(value ?? '').trim() ? true : 'License key file path is required.',
75
+ message: licenseActivateText('interactive.prompts.keyFile.message'),
76
+ validate: (value) => String(value ?? '').trim() ? true : licenseActivateText('interactive.prompts.keyFile.required'),
66
77
  });
67
78
  return { keyFile: String(keyFile ?? '').trim() || undefined };
68
79
  }
69
80
  catch {
70
- return {};
71
- }
72
- }
73
- async function promptOnlineActivationInput(initial, defaultAppName) {
74
- let account = String(initial.account ?? '').trim();
75
- if (!account) {
76
- try {
77
- const answer = await input({
78
- message: 'Service account',
79
- validate: (value) => String(value ?? '').trim() ? true : 'Service account is required.',
80
- });
81
- account = String(answer ?? '').trim();
82
- }
83
- catch {
84
- return;
85
- }
86
- }
87
- if (!account) {
88
- return;
89
- }
90
- let password = String(initial.password ?? '').trim();
91
- if (!password) {
92
- try {
93
- const answer = await promptPassword({
94
- message: 'Service password',
95
- mask: '•',
96
- validate: (value) => String(value ?? '').trim() ? true : 'Service password is required.',
97
- });
98
- password = String(answer ?? '').trim();
99
- }
100
- catch {
101
- return;
102
- }
103
- }
104
- if (!password) {
105
81
  return;
106
82
  }
107
- let appName = String(initial.appName ?? '').trim();
108
- if (!appName) {
109
- try {
110
- const resolvedDefaultAppName = String(defaultAppName ?? '').trim();
111
- const answer = await input({
112
- message: 'Application name',
113
- default: resolvedDefaultAppName || undefined,
114
- validate: (value) => String(value ?? '').trim() ? true : 'Application name is required.',
115
- });
116
- appName = String(answer ?? '').trim();
117
- }
118
- catch {
119
- return;
120
- }
121
- }
122
- if (!appName) {
123
- return;
124
- }
125
- return {
126
- account,
127
- password,
128
- appName,
129
- serviceUrl: await resolveLicenseServiceUrl(initial.serviceUrl),
130
- };
131
- }
132
- function resolveAppUrlOrThrow(runtime) {
133
- const currentAppUrl = appUrl(runtime);
134
- if (!currentAppUrl) {
135
- throw new Error(`Env "${runtime.envName}" does not have an app URL or app port configured.`);
136
- }
137
- try {
138
- return new URL(currentAppUrl).toString();
139
- }
140
- catch {
141
- throw new Error(`Env "${runtime.envName}" has an invalid app URL: ${currentAppUrl}`);
142
- }
143
- }
144
- async function requestOnlineLicenseKey(serviceUrl, account, password, payload) {
145
- const response = await fetch(`${serviceUrl}/license-key`, {
146
- method: 'POST',
147
- headers: {
148
- 'content-type': 'application/json',
149
- },
150
- body: JSON.stringify({
151
- account,
152
- password,
153
- appUrl: payload.appUrl,
154
- appName: payload.appName,
155
- instanceId: payload.instanceId,
156
- type: payload.type,
157
- }),
158
- });
159
- if (!response.ok) {
160
- throw new Error(`License service request failed with status ${response.status}.`);
161
- }
162
- const data = await response.json();
163
- const key = String(data?.data?.key ?? '').trim();
164
- if (!key) {
165
- throw new Error('License service did not return a license key.');
166
- }
167
- return key;
168
83
  }
169
84
  export default class LicenseActivate extends Command {
170
- static summary = 'Activate commercial licensing for the selected env';
171
- static description = 'Activate a commercial license for the selected env. Provide an existing license key directly, or use `--online` to request and activate one from the online license service.';
85
+ static summary = 'Activate an existing commercial license key for the selected env';
86
+ static description = 'Activate an existing commercial license key for the selected env.';
172
87
  static examples = [
88
+ '<%= config.bin %> <%= command.id %>',
173
89
  '<%= config.bin %> <%= command.id %> --env app1 --key <licenseKey>',
174
90
  '<%= config.bin %> <%= command.id %> --env app1 --key-file ./license.txt',
175
- '<%= config.bin %> <%= command.id %> --env app1 --online',
176
- '<%= config.bin %> <%= command.id %> --env app1 --online --account aa --password bb --desc test24',
177
- '<%= config.bin %> <%= command.id %> --env app1 --online --account aa --password bb --desc test24 --yes',
178
91
  '<%= config.bin %> <%= command.id %> --env app1 --json --key-file ./license.txt',
179
92
  ];
180
93
  static flags = {
181
94
  env: createLicenseEnvFlag('CLI env name to activate a license for. Defaults to the current env when omitted'),
182
95
  json: licenseJsonFlag,
183
96
  key: Flags.string({
184
- description: 'Existing license key to activate',
97
+ description: 'Existing commercial license key to activate',
185
98
  }),
186
99
  'key-file': Flags.string({
187
- description: 'Path to a file containing the license key to activate',
188
- }),
189
- online: Flags.boolean({
190
- description: 'Request a license online and activate it',
191
- default: false,
192
- }),
193
- account: Flags.string({
194
- description: 'License service account for online activation',
195
- }),
196
- password: Flags.string({
197
- description: 'License service password for online activation',
100
+ description: 'Path to a file containing the existing commercial license key to activate',
198
101
  }),
199
- desc: Flags.string({
200
- description: 'Application name for online activation',
201
- }),
202
- 'pkg-url': licensePkgUrlFlag,
203
102
  yes: licenseYesFlag,
204
103
  };
205
104
  async run() {
@@ -222,113 +121,33 @@ export default class LicenseActivate extends Command {
222
121
  }
223
122
  let key = String(flags.key ?? '').trim();
224
123
  let keyFile = String(flags['key-file'] ?? '').trim();
225
- let online = Boolean(flags.online);
226
- if (!key && !keyFile && !online) {
124
+ let interactiveKeyFlowInstanceId;
125
+ if (!key && !keyFile) {
227
126
  if (!isInteractiveTerminal()) {
228
- this.error('Provide --key, --key-file, or --online to continue.');
127
+ this.error(licenseActivateText('errors.provideKeyOrKeyFile'));
229
128
  }
230
- const mode = await promptActivationMode();
231
- if (mode === 'cancel') {
129
+ interactiveKeyFlowInstanceId = await ensureInstanceId(runtime);
130
+ const hostname = resolveHostnameNoticeValue(runtime);
131
+ this.log(formatInstanceIdNotice(interactiveKeyFlowInstanceId, hostname));
132
+ const prompted = await promptLicenseKeyInput();
133
+ if (!prompted) {
232
134
  return;
233
135
  }
234
- if (mode === 'online') {
235
- online = true;
236
- }
237
- else {
238
- const prompted = await promptLicenseKeyInput();
239
- key = String(prompted.key ?? '').trim();
240
- keyFile = String(prompted.keyFile ?? '').trim();
241
- if (!key && !keyFile) {
242
- this.error('License key input was empty.');
243
- }
136
+ key = String(prompted.key ?? '').trim();
137
+ keyFile = String(prompted.keyFile ?? '').trim();
138
+ if (!key && !keyFile) {
139
+ this.error(licenseActivateText('errors.emptyInput'));
244
140
  }
245
141
  }
246
- if ((key || keyFile) && online) {
247
- this.error('Use either an existing key (--key / --key-file) or --online, not both.');
248
- }
249
- if (online) {
250
- const resolvedServiceUrl = await resolveLicenseServiceUrl(flags['pkg-url']);
251
- const initialOnline = {
252
- account: resolveOnlineInputValue(flags.account),
253
- password: resolveOnlineInputValue(flags.password),
254
- appName: resolveOnlineInputValue(flags.desc),
255
- serviceUrl: resolvedServiceUrl,
256
- };
257
- let onlineInput = initialOnline;
258
- if (!onlineInput.account
259
- || !onlineInput.password
260
- || !onlineInput.appName) {
261
- if (!isInteractiveTerminal()) {
262
- this.error('Online activation requires --account, --password, and --desc when not using a TTY.');
263
- }
264
- const prompted = await promptOnlineActivationInput(initialOnline, runtime.envName);
265
- if (!prompted) {
266
- return;
267
- }
268
- onlineInput = prompted;
269
- }
270
- const instanceId = await ensureInstanceId(runtime);
271
- const resolvedAppUrl = resolveAppUrlOrThrow(runtime);
272
- const resolvedKey = await requestOnlineLicenseKey(onlineInput.serviceUrl, onlineInput.account, onlineInput.password, {
273
- appUrl: resolvedAppUrl,
274
- appName: onlineInput.appName,
275
- instanceId,
276
- type: 'internal',
277
- });
278
- const validation = await validateLicenseKey(runtime, resolvedKey);
279
- const ok = !validation.keyStatus
280
- && validation.envMatch
281
- && validation.domainMatch
282
- && validation.licenseStatus === 'active';
283
- const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
284
- const payload = {
285
- ok,
286
- env: runtime.envName,
287
- kind: runtime.kind,
288
- instanceId,
289
- mode: 'online',
290
- serviceUrl: onlineInput.serviceUrl,
291
- appUrl: resolvedAppUrl,
292
- appName: onlineInput.appName,
293
- key: redactLicenseKey(resolvedKey),
294
- licenseKeyPath,
295
- validation: sanitizeLicenseOutput(validation),
296
- };
297
- if (flags.json) {
298
- this.log(JSON.stringify(payload, null, 2));
299
- if (!ok) {
300
- this.exit(1);
301
- }
302
- return;
303
- }
304
- if (!ok) {
305
- const reason = validation.keyStatus
306
- ? `license key is ${validation.keyStatus}`
307
- : !validation.envMatch
308
- ? 'license key does not match the current instance environment'
309
- : !validation.domainMatch
310
- ? 'license key does not match the current app domain'
311
- : validation.licenseStatus !== 'active'
312
- ? `license status is ${validation.licenseStatus}`
313
- : 'license validation failed';
314
- this.error(`Failed to activate the online license for env "${runtime.envName}": ${reason}.`);
315
- }
316
- this.log(`Activated the online license for env "${runtime.envName}".`);
317
- this.log(`Saved license key at ${licenseKeyPath}`);
318
- return;
319
- }
320
142
  const resolvedKey = key || String(await readFile(keyFile, 'utf8')).trim();
321
143
  const validation = await validateLicenseKey(runtime, resolvedKey);
322
- const ok = !validation.keyStatus
323
- && validation.envMatch
324
- && validation.domainMatch
325
- && validation.licenseStatus === 'active';
144
+ const ok = !validation.keyStatus && validation.envMatch && validation.domainMatch && validation.licenseStatus === 'active';
326
145
  const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
327
146
  const payload = {
328
147
  ok,
329
148
  env: runtime.envName,
330
149
  kind: runtime.kind,
331
- instanceId: await ensureInstanceId(runtime),
150
+ instanceId: interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime)),
332
151
  mode: 'key',
333
152
  key: redactLicenseKey(resolvedKey),
334
153
  keyFile: keyFile || undefined,
@@ -344,17 +163,17 @@ export default class LicenseActivate extends Command {
344
163
  }
345
164
  if (!ok) {
346
165
  const reason = validation.keyStatus
347
- ? `license key is ${validation.keyStatus}`
166
+ ? licenseActivateText('errors.reasons.keyStatus', { status: validation.keyStatus })
348
167
  : !validation.envMatch
349
- ? 'license key does not match the current instance environment'
168
+ ? licenseActivateText('errors.reasons.envMismatch')
350
169
  : !validation.domainMatch
351
- ? 'license key does not match the current app domain'
170
+ ? licenseActivateText('errors.reasons.domainMismatch')
352
171
  : validation.licenseStatus !== 'active'
353
- ? `license status is ${validation.licenseStatus}`
354
- : 'license validation failed';
355
- this.error(`Failed to activate the license for env "${runtime.envName}": ${reason}.`);
172
+ ? licenseActivateText('errors.reasons.licenseStatus', { status: validation.licenseStatus })
173
+ : licenseActivateText('errors.reasons.validationFailed');
174
+ this.error(licenseActivateText('errors.activationFailed', { envName: runtime.envName, reason }));
356
175
  }
357
- this.log(`Activated the license for env "${runtime.envName}".`);
358
- this.log(`Saved license key at ${licenseKeyPath}`);
176
+ this.log(licenseActivateText('messages.activated', { envName: runtime.envName }));
177
+ this.log(licenseActivateText('messages.savedLicenseKey', { licenseKeyPath }));
359
178
  }
360
179
  }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import path from 'node:path';
10
- import { access, mkdir, rm } from 'node:fs/promises';
10
+ import { access, mkdir, readFile, rm } from 'node:fs/promises';
11
11
  import { Readable } from 'node:stream';
12
12
  import { createGunzip } from 'node:zlib';
13
13
  import * as tar from 'tar';
@@ -34,6 +34,10 @@ async function pathExists(target) {
34
34
  return false;
35
35
  }
36
36
  }
37
+ function trimString(value) {
38
+ const text = String(value ?? '').trim();
39
+ return text || undefined;
40
+ }
37
41
  async function loginPkg(baseURL, keyData) {
38
42
  const username = String(keyData.accessKeyId ?? '').trim();
39
43
  const password = String(keyData.accessKeySecret ?? '').trim();
@@ -98,15 +102,16 @@ async function packageMetadata(baseURL, token, pluginName) {
98
102
  if (!response.ok) {
99
103
  return undefined;
100
104
  }
101
- return await response.json();
105
+ return (await response.json());
102
106
  }
103
107
  catch {
104
108
  return undefined;
105
109
  }
106
110
  }
107
111
  function resolveTarball(metadata, requestedVersion) {
108
- if (metadata.versions?.[requestedVersion]) {
109
- return [requestedVersion, metadata.versions[requestedVersion].dist.tarball];
112
+ const requestedTarball = trimString(metadata.versions?.[requestedVersion]?.dist?.tarball);
113
+ if (requestedTarball) {
114
+ return [requestedVersion, requestedTarball];
110
115
  }
111
116
  let version = requestedVersion;
112
117
  if (version.includes('rc')) {
@@ -129,16 +134,36 @@ function resolveTarball(metadata, requestedVersion) {
129
134
  else if (requestedVersion.includes('alpha')) {
130
135
  version = metadata['dist-tags']?.alpha || metadata['dist-tags']?.next;
131
136
  }
132
- if (!metadata.versions?.[version]) {
137
+ const tarball = trimString(metadata.versions?.[version]?.dist?.tarball);
138
+ if (!tarball) {
133
139
  return undefined;
134
140
  }
135
- return [version, metadata.versions[version].dist.tarball];
141
+ return [version, tarball];
136
142
  }
137
- async function downloadPlugin(baseURL, token, pluginName, requestedVersion, storagePath) {
143
+ async function readDownloadedPluginVersion(outputDir) {
144
+ const packageJsonPath = path.resolve(outputDir, 'package.json');
145
+ let content;
146
+ try {
147
+ content = await readFile(packageJsonPath, 'utf8');
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ try {
153
+ const parsed = JSON.parse(content);
154
+ return trimString(parsed.version);
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ }
160
+ async function planPluginDownload(baseURL, token, pluginName, requestedVersion, storagePath) {
138
161
  const metadata = await packageMetadata(baseURL, token, pluginName);
162
+ const outputDir = path.resolve(storagePath, pluginName);
139
163
  if (!metadata) {
140
164
  return {
141
165
  action: 'skipped',
166
+ outputDir,
142
167
  warning: `Commercial plugin package "${pluginName}" does not exist in the package registry.`,
143
168
  };
144
169
  }
@@ -146,12 +171,36 @@ async function downloadPlugin(baseURL, token, pluginName, requestedVersion, stor
146
171
  if (!tarball) {
147
172
  return {
148
173
  action: 'skipped',
174
+ outputDir,
149
175
  warning: `Package ${pluginName} does not have a downloadable version for "${requestedVersion}".`,
150
176
  };
151
177
  }
152
178
  const [resolvedVersion, tarballUrl] = tarball;
153
- const outputDir = path.resolve(storagePath, pluginName);
154
- const existedBefore = await pathExists(path.resolve(storagePath, pluginName, 'package.json'));
179
+ const packageJsonPath = path.resolve(outputDir, 'package.json');
180
+ const localVersion = await readDownloadedPluginVersion(outputDir);
181
+ if (localVersion === resolvedVersion) {
182
+ return {
183
+ action: 'skipped',
184
+ outputDir,
185
+ };
186
+ }
187
+ const existedBefore = await pathExists(packageJsonPath);
188
+ return {
189
+ action: existedBefore ? 'updated' : 'installed',
190
+ outputDir,
191
+ resolvedVersion,
192
+ tarballUrl,
193
+ };
194
+ }
195
+ async function downloadPlugin(baseURL, token, pluginName, requestedVersion, storagePath) {
196
+ const plan = await planPluginDownload(baseURL, token, pluginName, requestedVersion, storagePath);
197
+ if (plan.action === 'skipped') {
198
+ return {
199
+ action: 'skipped',
200
+ ...(plan.warning ? { warning: plan.warning } : {}),
201
+ };
202
+ }
203
+ const { action, outputDir, resolvedVersion, tarballUrl } = plan;
155
204
  try {
156
205
  await rm(outputDir, { recursive: true, force: true });
157
206
  await mkdir(outputDir, { recursive: true });
@@ -174,7 +223,7 @@ async function downloadPlugin(baseURL, token, pluginName, requestedVersion, stor
174
223
  .on('error', reject);
175
224
  });
176
225
  return {
177
- action: existedBefore ? 'updated' : 'installed',
226
+ action,
178
227
  };
179
228
  }
180
229
  catch (error) {
@@ -246,14 +295,16 @@ export async function syncLicensedPlugins(runtime, options) {
246
295
  }
247
296
  for (const pluginName of licensedPlugins) {
248
297
  if (options.dryRun) {
249
- const outputDir = path.resolve(storagePath, pluginName);
250
- const existedBefore = await pathExists(path.resolve(outputDir, 'package.json'));
251
- const action = existedBefore ? 'updated' : 'installed';
298
+ const { action, outputDir, warning } = await planPluginDownload(baseURL, token, pluginName, options.version, storagePath);
252
299
  result[action].push(pluginName);
300
+ if (warning) {
301
+ result.warnings.push(warning);
302
+ }
253
303
  await emitDetail({
254
304
  packageName: pluginName,
255
305
  action,
256
306
  outputDir,
307
+ ...(warning ? { warning } : {}),
257
308
  });
258
309
  continue;
259
310
  }
@@ -0,0 +1,108 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
12
+ import { importPluginSource } from '../../lib/plugin-import.js';
13
+ import { announceTargetEnv } from '../../lib/ui.js';
14
+ export default class PluginImport extends Command {
15
+ static hidden = false;
16
+ static args = {
17
+ archive: Args.string({
18
+ required: true,
19
+ description: 'Plugin source to import. Accepts a local .tgz path, a remote http(s) URL, or an npm package spec.',
20
+ }),
21
+ };
22
+ static summary = 'Import a packaged plugin into storage/plugins for the selected env';
23
+ static description = 'Download or read a packaged plugin source and extract it into the selected env storage/plugins directory without enabling it.';
24
+ static examples = [
25
+ '<%= config.bin %> <%= command.id %> https://github.com/nocobase/plugin-auth-cas/releases/download/v1.4.0/plugin-auth-cas-1.4.0.tgz',
26
+ '<%= config.bin %> <%= command.id %> /your/path/plugin-auth-cas-1.4.0.tgz',
27
+ '<%= config.bin %> <%= command.id %> @nocobase/plugin-acl@beta',
28
+ '<%= config.bin %> <%= command.id %> @nocobase/plugin-acl@beta --npm-registry=https://registry.npmjs.org',
29
+ '<%= config.bin %> <%= command.id %> --env app1 ./plugin-auth-cas-1.4.0.tgz',
30
+ '<%= config.bin %> <%= command.id %> --storage-path ./storage ./plugin-auth-cas-1.4.0.tgz',
31
+ ];
32
+ static flags = {
33
+ env: Flags.string({
34
+ char: 'e',
35
+ description: 'CLI env name to import the plugin into. Defaults to the current env when omitted',
36
+ }),
37
+ yes: Flags.boolean({
38
+ char: 'y',
39
+ description: 'Confirm using --env when it targets a different env than the current env',
40
+ default: false,
41
+ }),
42
+ 'storage-path': Flags.string({
43
+ description: 'Override the env storage root path. Imported plugins are written into <storage-path>/plugins',
44
+ }),
45
+ 'npm-registry': Flags.string({
46
+ description: 'npm registry to use when the import source is an npm package spec.',
47
+ }),
48
+ };
49
+ async run() {
50
+ const { args, flags } = await this.parse(PluginImport);
51
+ const requestedEnv = flags.env?.trim() || undefined;
52
+ const storagePathOverride = flags['storage-path']?.trim() || undefined;
53
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
54
+ if (explicitEnvSelection) {
55
+ const confirmed = await ensureCrossEnvConfirmed({
56
+ command: this,
57
+ requestedEnv,
58
+ yes: flags.yes,
59
+ });
60
+ if (!confirmed) {
61
+ return;
62
+ }
63
+ }
64
+ const archiveSource = args.archive?.trim();
65
+ if (!archiveSource) {
66
+ this.error('Pass a plugin archive path, URL, or npm package spec.');
67
+ }
68
+ const shouldResolveTargetEnv = Boolean(requestedEnv || !storagePathOverride);
69
+ const runtime = await resolveManagedAppRuntime(shouldResolveTargetEnv ? requestedEnv : undefined);
70
+ if (shouldResolveTargetEnv && !runtime) {
71
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
72
+ }
73
+ if (runtime && shouldResolveTargetEnv && runtime.kind === 'http') {
74
+ this.error([
75
+ `Can't import plugins for "${runtime.envName}" yet.`,
76
+ 'HTTP envs do not expose a writable storage/plugins path to the CLI.',
77
+ 'Use a local or Docker env for plugin imports right now.',
78
+ ].join('\n'));
79
+ }
80
+ if (runtime && shouldResolveTargetEnv && runtime.kind === 'ssh') {
81
+ this.error([
82
+ `Can't import plugins for "${runtime.envName}" yet.`,
83
+ 'SSH env support is reserved but not implemented yet.',
84
+ 'Use a local or Docker env for plugin imports right now.',
85
+ ].join('\n'));
86
+ }
87
+ if (runtime && shouldResolveTargetEnv) {
88
+ announceTargetEnv(runtime.envName);
89
+ }
90
+ const runtimeForDefaults = runtime && runtime.kind !== 'http' && runtime.kind !== 'ssh' ? runtime : undefined;
91
+ const npmRegistry = flags['npm-registry']?.trim() || String(runtimeForDefaults?.env.config.npmRegistry ?? '').trim() || undefined;
92
+ const storagePath = storagePathOverride || runtimeForDefaults?.env.storagePath;
93
+ const result = await importPluginSource(archiveSource, {
94
+ storagePath,
95
+ npmRegistry,
96
+ });
97
+ const label = result.action === 'updated' ? 'Updated' : 'Imported';
98
+ const versionSuffix = result.packageVersion ? `@${result.packageVersion}` : '';
99
+ this.log(`${label} ${result.packageName}${versionSuffix} into ${result.outputDir}`);
100
+ this.log(`Plugin storage path: ${result.storagePluginsPath}`);
101
+ if (runtime && shouldResolveTargetEnv) {
102
+ this.log(`Restart the app before enabling or using the plugin: \`nb app restart --env ${runtime.envName}\`.`);
103
+ }
104
+ else {
105
+ this.log('Restart the app that uses this plugin storage path before enabling or using the plugin.');
106
+ }
107
+ }
108
+ }