@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.
- package/LICENSE +201 -0
- package/README.md +63 -380
- package/assets/env-proxy/nginx/app.conf.tpl +23 -0
- package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
- package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
- package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
- package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
- package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
- package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
- package/dist/commands/app/autostart/disable.js +55 -0
- package/dist/commands/app/autostart/enable.js +55 -0
- package/dist/commands/app/autostart/list.js +37 -0
- package/dist/commands/app/autostart/run.js +84 -0
- package/dist/commands/app/autostart/shared.js +49 -0
- package/dist/commands/app/destroy.js +8 -6
- package/dist/commands/app/down.js +2 -2
- package/dist/commands/app/logs.js +2 -1
- package/dist/commands/app/restart.js +79 -23
- package/dist/commands/app/shared.js +1 -1
- package/dist/commands/app/start.js +134 -38
- package/dist/commands/app/stop.js +31 -2
- package/dist/commands/app/upgrade.js +3 -1
- package/dist/commands/config/delete.js +4 -1
- package/dist/commands/config/get.js +4 -1
- package/dist/commands/config/set.js +5 -2
- package/dist/commands/env/add.js +19 -39
- package/dist/commands/env/info.js +3 -2
- package/dist/commands/env/proxy/caddy.js +28 -0
- package/dist/commands/env/proxy/index.js +353 -0
- package/dist/commands/env/proxy/nginx.js +28 -0
- package/dist/commands/env/remove.js +112 -22
- package/dist/commands/env/shared.js +17 -9
- package/dist/commands/env/update.js +385 -21
- package/dist/commands/init.js +233 -91
- package/dist/commands/install.js +174 -68
- package/dist/commands/license/activate.js +63 -244
- package/dist/commands/license/plugins/shared.js +64 -13
- package/dist/commands/plugin/import.js +108 -0
- package/dist/commands/revision/create.js +89 -0
- package/dist/locale/en-US.json +105 -19
- package/dist/locale/zh-CN.json +102 -16
- package/package.json +5 -8
- package/scripts/build.mjs +34 -0
- package/scripts/clean.mjs +9 -0
- package/tsconfig.json +19 -0
- package/LICENSE.txt +0 -107
- package/README.zh-CN.md +0 -355
- package/dist/lib/api-client.js +0 -335
- package/dist/lib/api-command-compat.js +0 -641
- package/dist/lib/app-health.js +0 -139
- package/dist/lib/app-managed-resources.js +0 -316
- package/dist/lib/app-runtime.js +0 -180
- package/dist/lib/auth-store.js +0 -405
- package/dist/lib/backup.js +0 -171
- package/dist/lib/bootstrap.js +0 -409
- package/dist/lib/build-config.js +0 -18
- package/dist/lib/builtin-db.js +0 -86
- package/dist/lib/cli-config.js +0 -309
- package/dist/lib/cli-entry-error.js +0 -44
- package/dist/lib/cli-home.js +0 -47
- package/dist/lib/cli-locale.js +0 -141
- package/dist/lib/command-discovery.js +0 -39
- package/dist/lib/db-connection-check.js +0 -219
- package/dist/lib/docker-env-file.js +0 -52
- package/dist/lib/docker-image.js +0 -37
- package/dist/lib/docker-log-stream.js +0 -45
- package/dist/lib/env-auth.js +0 -960
- package/dist/lib/env-config.js +0 -95
- package/dist/lib/env-guard.js +0 -62
- package/dist/lib/generated-command.js +0 -203
- package/dist/lib/http-request.js +0 -49
- package/dist/lib/inquirer-theme.js +0 -17
- package/dist/lib/inquirer.js +0 -244
- package/dist/lib/naming.js +0 -70
- package/dist/lib/object-utils.js +0 -76
- package/dist/lib/openapi.js +0 -62
- package/dist/lib/plugin-storage.js +0 -64
- package/dist/lib/post-processors.js +0 -23
- package/dist/lib/prompt-catalog-core.js +0 -185
- package/dist/lib/prompt-catalog-terminal.js +0 -375
- package/dist/lib/prompt-catalog.js +0 -10
- package/dist/lib/prompt-validators.js +0 -258
- package/dist/lib/prompt-web-ui.js +0 -2227
- package/dist/lib/resource-command.js +0 -357
- package/dist/lib/resource-request.js +0 -104
- package/dist/lib/run-npm.js +0 -385
- package/dist/lib/runtime-env-vars.js +0 -32
- package/dist/lib/runtime-generator.js +0 -498
- package/dist/lib/runtime-store.js +0 -56
- package/dist/lib/self-manager.js +0 -301
- package/dist/lib/session-id.js +0 -17
- package/dist/lib/session-integration.js +0 -703
- package/dist/lib/session-store.js +0 -118
- package/dist/lib/skills-manager.js +0 -436
- package/dist/lib/source-publish.js +0 -309
- package/dist/lib/source-registry.js +0 -188
- package/dist/lib/startup-update.js +0 -309
- 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,
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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: '
|
|
48
|
+
message: licenseActivateText('interactive.prompts.provideMethod.message'),
|
|
40
49
|
choices: [
|
|
41
|
-
{ value: 'key', name: '
|
|
42
|
-
{ value: 'file', name: '
|
|
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
|
|
53
|
-
message: '
|
|
54
|
-
|
|
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: '
|
|
65
|
-
validate: (value) => String(value ?? '').trim() ? true : '
|
|
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
|
|
171
|
-
static description = 'Activate
|
|
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
|
|
226
|
-
if (!key && !keyFile
|
|
124
|
+
let interactiveKeyFlowInstanceId;
|
|
125
|
+
if (!key && !keyFile) {
|
|
227
126
|
if (!isInteractiveTerminal()) {
|
|
228
|
-
this.error('
|
|
127
|
+
this.error(licenseActivateText('errors.provideKeyOrKeyFile'));
|
|
229
128
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
?
|
|
166
|
+
? licenseActivateText('errors.reasons.keyStatus', { status: validation.keyStatus })
|
|
348
167
|
: !validation.envMatch
|
|
349
|
-
? '
|
|
168
|
+
? licenseActivateText('errors.reasons.envMismatch')
|
|
350
169
|
: !validation.domainMatch
|
|
351
|
-
? '
|
|
170
|
+
? licenseActivateText('errors.reasons.domainMismatch')
|
|
352
171
|
: validation.licenseStatus !== 'active'
|
|
353
|
-
?
|
|
354
|
-
: '
|
|
355
|
-
this.error(
|
|
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(
|
|
358
|
-
this.log(
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
137
|
+
const tarball = trimString(metadata.versions?.[version]?.dist?.tarball);
|
|
138
|
+
if (!tarball) {
|
|
133
139
|
return undefined;
|
|
134
140
|
}
|
|
135
|
-
return [version,
|
|
141
|
+
return [version, tarball];
|
|
136
142
|
}
|
|
137
|
-
async function
|
|
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
|
|
154
|
-
const
|
|
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
|
|
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 =
|
|
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
|
+
}
|