@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.
- package/README.md +24 -0
- package/README.zh-CN.md +4 -0
- package/dist/commands/app/down.js +12 -6
- package/dist/commands/app/logs.js +2 -2
- package/dist/commands/app/start.js +2 -1
- package/dist/commands/app/stop.js +2 -1
- package/dist/commands/app/upgrade.js +116 -129
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +238 -0
- package/dist/commands/db/logs.js +2 -2
- package/dist/commands/db/shared.js +6 -5
- package/dist/commands/db/start.js +2 -1
- package/dist/commands/db/stop.js +2 -1
- package/dist/commands/env/info.js +6 -2
- package/dist/commands/env/shared.js +1 -1
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +87 -35
- package/dist/commands/license/activate.js +360 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +108 -0
- package/dist/commands/license/id.js +56 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +101 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +269 -0
- package/dist/commands/license/shared.js +414 -0
- package/dist/commands/license/status.js +50 -0
- package/dist/commands/plugin/disable.js +2 -0
- package/dist/commands/plugin/enable.js +2 -0
- package/dist/commands/source/dev.js +2 -1
- package/dist/lib/api-client.js +74 -3
- package/dist/lib/app-managed-resources.js +10 -6
- package/dist/lib/app-runtime.js +29 -11
- package/dist/lib/auth-store.js +36 -68
- package/dist/lib/bootstrap.js +0 -4
- package/dist/lib/build-config.js +8 -0
- package/dist/lib/builtin-db.js +86 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +6 -21
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-config.js +7 -0
- package/dist/lib/generated-command.js +24 -3
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/prompt-validators.js +4 -4
- package/dist/lib/run-npm.js +53 -0
- package/dist/lib/runtime-env-vars.js +32 -0
- package/dist/lib/runtime-generator.js +89 -10
- package/dist/lib/self-manager.js +57 -2
- package/dist/lib/skills-manager.js +2 -2
- package/dist/lib/startup-update.js +85 -7
- package/dist/lib/ui.js +3 -0
- package/dist/locale/en-US.json +16 -13
- package/dist/locale/zh-CN.json +16 -13
- package/nocobase-ctl.config.json +82 -0
- package/package.json +16 -4
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { access, mkdir, rm } from 'node:fs/promises';
|
|
11
|
+
import { Readable } from 'node:stream';
|
|
12
|
+
import { createGunzip } from 'node:zlib';
|
|
13
|
+
import * as tar from 'tar';
|
|
14
|
+
import { removeStoragePluginSymlink, resolvePluginStoragePath, } from '../../../lib/plugin-storage.js';
|
|
15
|
+
import { parseLicenseKey, readSavedLicenseKey, resolveLicensePkgUrl, } from '../shared.js';
|
|
16
|
+
async function resolvePkgBaseUrl(pkgUrl) {
|
|
17
|
+
return await resolveLicensePkgUrl(pkgUrl);
|
|
18
|
+
}
|
|
19
|
+
function responseBodyToNodeReadable(body) {
|
|
20
|
+
return Readable.fromWeb(body);
|
|
21
|
+
}
|
|
22
|
+
async function pathExists(target) {
|
|
23
|
+
try {
|
|
24
|
+
await access(target);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function loginPkg(baseURL, keyData) {
|
|
32
|
+
const username = String(keyData.accessKeyId ?? '').trim();
|
|
33
|
+
const password = String(keyData.accessKeySecret ?? '').trim();
|
|
34
|
+
if (!username || !password) {
|
|
35
|
+
throw new Error('The saved license key does not include package registry credentials.');
|
|
36
|
+
}
|
|
37
|
+
const response = await fetch(`${baseURL}-/verdaccio/sec/login`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ username, password }),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`Package registry login failed with status ${response.status}.`);
|
|
46
|
+
}
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
const token = String(data?.token ?? '').trim();
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw new Error('Package registry login did not return a token.');
|
|
51
|
+
}
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
54
|
+
export async function loadSavedLicenseKeyData(runtime) {
|
|
55
|
+
const licenseKey = await readSavedLicenseKey(runtime);
|
|
56
|
+
if (!licenseKey) {
|
|
57
|
+
throw new Error(`No saved license key was found for env "${runtime.envName}". Run \`nb license activate\` first.`);
|
|
58
|
+
}
|
|
59
|
+
return parseLicenseKey(licenseKey);
|
|
60
|
+
}
|
|
61
|
+
export async function fetchLicensedPluginPackages(runtime, options = {}) {
|
|
62
|
+
const keyData = await loadSavedLicenseKeyData(runtime);
|
|
63
|
+
const baseURL = await resolvePkgBaseUrl(options.pkgUrl);
|
|
64
|
+
const token = await loginPkg(baseURL, keyData);
|
|
65
|
+
const response = await fetch(`${baseURL}pro-packages`, {
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${token}`,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Failed to fetch commercial plugins with status ${response.status}.`);
|
|
72
|
+
}
|
|
73
|
+
const rawData = await response.json();
|
|
74
|
+
if (Array.isArray(rawData)) {
|
|
75
|
+
return {
|
|
76
|
+
commercialPlugins: rawData,
|
|
77
|
+
licensedPlugins: rawData,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
commercialPlugins: rawData?.meta?.commercial_plugins || [],
|
|
82
|
+
licensedPlugins: rawData?.data || [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function packageMetadata(baseURL, token, pluginName) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(`${baseURL}${pluginName}`, {
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${token}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return await response.json();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function resolveTarball(metadata, requestedVersion) {
|
|
102
|
+
if (metadata.versions?.[requestedVersion]) {
|
|
103
|
+
return [requestedVersion, metadata.versions[requestedVersion].dist.tarball];
|
|
104
|
+
}
|
|
105
|
+
let version = requestedVersion;
|
|
106
|
+
if (version.includes('rc')) {
|
|
107
|
+
version = version.split('-').shift() || version;
|
|
108
|
+
}
|
|
109
|
+
const keys = version.split('.');
|
|
110
|
+
if (keys.length === 5) {
|
|
111
|
+
keys.pop();
|
|
112
|
+
version = keys.join('.');
|
|
113
|
+
}
|
|
114
|
+
if (version === 'latest') {
|
|
115
|
+
version = metadata['dist-tags']?.latest;
|
|
116
|
+
}
|
|
117
|
+
else if (version === 'next') {
|
|
118
|
+
version = metadata['dist-tags']?.next;
|
|
119
|
+
}
|
|
120
|
+
else if (requestedVersion.includes('beta')) {
|
|
121
|
+
version = metadata['dist-tags']?.next;
|
|
122
|
+
}
|
|
123
|
+
else if (requestedVersion.includes('alpha')) {
|
|
124
|
+
version = metadata['dist-tags']?.alpha || metadata['dist-tags']?.next;
|
|
125
|
+
}
|
|
126
|
+
if (!metadata.versions?.[version]) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return [version, metadata.versions[version].dist.tarball];
|
|
130
|
+
}
|
|
131
|
+
async function downloadPlugin(baseURL, token, pluginName, requestedVersion, storagePath) {
|
|
132
|
+
const metadata = await packageMetadata(baseURL, token, pluginName);
|
|
133
|
+
if (!metadata) {
|
|
134
|
+
return {
|
|
135
|
+
action: 'skipped',
|
|
136
|
+
warning: `Commercial plugin package "${pluginName}" does not exist in the package registry.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const tarball = resolveTarball(metadata, requestedVersion);
|
|
140
|
+
if (!tarball) {
|
|
141
|
+
return {
|
|
142
|
+
action: 'skipped',
|
|
143
|
+
warning: `Package ${pluginName} does not have a downloadable version for "${requestedVersion}".`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const [resolvedVersion, tarballUrl] = tarball;
|
|
147
|
+
const outputDir = path.resolve(storagePath, pluginName);
|
|
148
|
+
const existedBefore = await pathExists(path.resolve(storagePath, pluginName, 'package.json'));
|
|
149
|
+
try {
|
|
150
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
151
|
+
await mkdir(outputDir, { recursive: true });
|
|
152
|
+
const response = await fetch(tarballUrl, {
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${token}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`download failed with status ${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
if (!response.body) {
|
|
161
|
+
throw new Error('download response body is empty');
|
|
162
|
+
}
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
responseBodyToNodeReadable(response.body)
|
|
165
|
+
.pipe(createGunzip())
|
|
166
|
+
.pipe(tar.extract({ cwd: outputDir, strip: 1 }))
|
|
167
|
+
.on('finish', () => resolve())
|
|
168
|
+
.on('error', reject);
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
action: existedBefore ? 'updated' : 'installed',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
return {
|
|
177
|
+
action: 'skipped',
|
|
178
|
+
warning: `Failed to download ${pluginName}@${resolvedVersion} from ${tarballUrl}: ${message}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function removeUnlicensedPlugin(pluginName, storagePath) {
|
|
183
|
+
const dir = path.resolve(storagePath, pluginName);
|
|
184
|
+
if (!(await pathExists(dir))) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
await rm(dir, { recursive: true, force: true });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
async function removeDownloadedPlugin(pluginName, storagePath) {
|
|
191
|
+
const dir = path.resolve(storagePath, pluginName);
|
|
192
|
+
if (!(await pathExists(dir))) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
await rm(dir, { recursive: true, force: true });
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
export async function syncLicensedPlugins(runtime, options) {
|
|
199
|
+
const keyData = await loadSavedLicenseKeyData(runtime);
|
|
200
|
+
const baseURL = await resolvePkgBaseUrl(options.pkgUrl);
|
|
201
|
+
const token = await loginPkg(baseURL, keyData);
|
|
202
|
+
const { commercialPlugins, licensedPlugins } = await fetchLicensedPluginPackages(runtime, { pkgUrl: options.pkgUrl });
|
|
203
|
+
const storagePath = resolvePluginStoragePath(runtime.env.storagePath);
|
|
204
|
+
const nodeModulesPath = String(runtime.env.envVars.NODE_MODULES_PATH ?? '').trim();
|
|
205
|
+
const result = {
|
|
206
|
+
commercialPlugins,
|
|
207
|
+
licensedPlugins,
|
|
208
|
+
installed: [],
|
|
209
|
+
updated: [],
|
|
210
|
+
removed: [],
|
|
211
|
+
skipped: [],
|
|
212
|
+
warnings: [],
|
|
213
|
+
storagePath,
|
|
214
|
+
details: [],
|
|
215
|
+
};
|
|
216
|
+
const emitDetail = async (detail) => {
|
|
217
|
+
result.details.push(detail);
|
|
218
|
+
await options.onProgress?.(detail);
|
|
219
|
+
};
|
|
220
|
+
for (const pluginName of commercialPlugins) {
|
|
221
|
+
if (!licensedPlugins.includes(pluginName)) {
|
|
222
|
+
if (options.dryRun) {
|
|
223
|
+
result.removed.push(pluginName);
|
|
224
|
+
await emitDetail({
|
|
225
|
+
packageName: pluginName,
|
|
226
|
+
action: 'removed',
|
|
227
|
+
outputDir: path.resolve(storagePath, pluginName),
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (await removeUnlicensedPlugin(pluginName, storagePath)) {
|
|
232
|
+
result.removed.push(pluginName);
|
|
233
|
+
await emitDetail({
|
|
234
|
+
packageName: pluginName,
|
|
235
|
+
action: 'removed',
|
|
236
|
+
outputDir: path.resolve(storagePath, pluginName),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const pluginName of licensedPlugins) {
|
|
242
|
+
if (options.dryRun) {
|
|
243
|
+
const outputDir = path.resolve(storagePath, pluginName);
|
|
244
|
+
const existedBefore = await pathExists(path.resolve(outputDir, 'package.json'));
|
|
245
|
+
const action = existedBefore ? 'updated' : 'installed';
|
|
246
|
+
result[action].push(pluginName);
|
|
247
|
+
await emitDetail({
|
|
248
|
+
packageName: pluginName,
|
|
249
|
+
action,
|
|
250
|
+
outputDir,
|
|
251
|
+
});
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const { action, warning } = await downloadPlugin(baseURL, token, pluginName, options.version, storagePath);
|
|
255
|
+
if (warning) {
|
|
256
|
+
result.warnings.push(warning);
|
|
257
|
+
}
|
|
258
|
+
if (action === 'installed') {
|
|
259
|
+
result.installed.push(pluginName);
|
|
260
|
+
}
|
|
261
|
+
else if (action === 'updated') {
|
|
262
|
+
result.updated.push(pluginName);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
result.skipped.push(pluginName);
|
|
266
|
+
}
|
|
267
|
+
await emitDetail({
|
|
268
|
+
packageName: pluginName,
|
|
269
|
+
action,
|
|
270
|
+
outputDir: path.resolve(storagePath, pluginName),
|
|
271
|
+
...(warning ? { warning } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
export async function cleanLicensedPlugins(runtime, options = {}) {
|
|
277
|
+
const { commercialPlugins } = await fetchLicensedPluginPackages(runtime, { pkgUrl: options.pkgUrl });
|
|
278
|
+
const storagePath = resolvePluginStoragePath(runtime.env.storagePath);
|
|
279
|
+
const nodeModulesPath = String(runtime.env.envVars.NODE_MODULES_PATH ?? '').trim();
|
|
280
|
+
const result = {
|
|
281
|
+
commercialPlugins,
|
|
282
|
+
removed: [],
|
|
283
|
+
skipped: [],
|
|
284
|
+
storagePath,
|
|
285
|
+
details: [],
|
|
286
|
+
};
|
|
287
|
+
const emitDetail = async (detail) => {
|
|
288
|
+
result.details.push(detail);
|
|
289
|
+
await options.onProgress?.(detail);
|
|
290
|
+
};
|
|
291
|
+
for (const pluginName of commercialPlugins) {
|
|
292
|
+
const outputDir = path.resolve(storagePath, pluginName);
|
|
293
|
+
const exists = await pathExists(outputDir);
|
|
294
|
+
if (!exists) {
|
|
295
|
+
result.skipped.push(pluginName);
|
|
296
|
+
await emitDetail({
|
|
297
|
+
packageName: pluginName,
|
|
298
|
+
action: 'skipped',
|
|
299
|
+
outputDir,
|
|
300
|
+
removedSymlink: false,
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (options.dryRun) {
|
|
305
|
+
result.removed.push(pluginName);
|
|
306
|
+
await emitDetail({
|
|
307
|
+
packageName: pluginName,
|
|
308
|
+
action: 'removed',
|
|
309
|
+
outputDir,
|
|
310
|
+
removedSymlink: Boolean(nodeModulesPath),
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
await removeDownloadedPlugin(pluginName, storagePath);
|
|
315
|
+
const removedSymlink = await removeStoragePluginSymlink(pluginName, runtime.env.storagePath, nodeModulesPath);
|
|
316
|
+
result.removed.push(pluginName);
|
|
317
|
+
await emitDetail({
|
|
318
|
+
packageName: pluginName,
|
|
319
|
+
action: 'removed',
|
|
320
|
+
outputDir,
|
|
321
|
+
removedSymlink,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
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 pc from 'picocolors';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { licenseEnvFlag, licenseJsonFlag, licensePkgUrlFlag, requireLicenseRuntime } from '../shared.js';
|
|
14
|
+
import { syncLicensedPlugins } from './shared.js';
|
|
15
|
+
import { resolvePluginStoragePath } from '../../../lib/plugin-storage.js';
|
|
16
|
+
import { commandOutput } from '../../../lib/run-npm.js';
|
|
17
|
+
import { announceTargetEnv, startTask, stopTask, succeedTask, updateTask } from '../../../lib/ui.js';
|
|
18
|
+
const SYNC_LOADING_DELAY_MS = 1200;
|
|
19
|
+
const SYNC_LOADING_UPDATE_MS = 5000;
|
|
20
|
+
const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
|
|
21
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
22
|
+
const DEFAULT_DOCKER_VERSION = 'alpha';
|
|
23
|
+
function formatActionLabel(action) {
|
|
24
|
+
switch (action) {
|
|
25
|
+
case 'installed':
|
|
26
|
+
return pc.green('installed');
|
|
27
|
+
case 'updated':
|
|
28
|
+
return pc.cyan('updated');
|
|
29
|
+
case 'removed':
|
|
30
|
+
return pc.yellow('removed');
|
|
31
|
+
case 'skipped':
|
|
32
|
+
return pc.dim('skipped');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function trimValue(value) {
|
|
36
|
+
const text = String(value ?? '').trim();
|
|
37
|
+
return text || undefined;
|
|
38
|
+
}
|
|
39
|
+
function normalizeDockerPlatform(value) {
|
|
40
|
+
const text = trimValue(value);
|
|
41
|
+
if (!text || text === 'auto') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function normalizePluginRegistryVersion(version) {
|
|
50
|
+
const normalized = version.trim();
|
|
51
|
+
const rcMatch = normalized.match(/^(.+)-rc\.\d{8,}$/);
|
|
52
|
+
if (rcMatch) {
|
|
53
|
+
return rcMatch[1];
|
|
54
|
+
}
|
|
55
|
+
const betaMatch = normalized.match(/^(.+-beta\.\d+)\.\d{8,}$/);
|
|
56
|
+
if (betaMatch) {
|
|
57
|
+
return betaMatch[1];
|
|
58
|
+
}
|
|
59
|
+
const alphaMatch = normalized.match(/^(.+-alpha\.\d+)\.\d{8,}$/);
|
|
60
|
+
if (alphaMatch) {
|
|
61
|
+
return alphaMatch[1];
|
|
62
|
+
}
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
async function parseVersionFromPackageJson(content, sourceLabel) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(content);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`Failed to parse ${sourceLabel}.`);
|
|
72
|
+
}
|
|
73
|
+
const version = trimValue(parsed.version);
|
|
74
|
+
if (!version) {
|
|
75
|
+
throw new Error(`Missing version in ${sourceLabel}.`);
|
|
76
|
+
}
|
|
77
|
+
return version;
|
|
78
|
+
}
|
|
79
|
+
async function resolveLocalAppVersion(runtime) {
|
|
80
|
+
const packageJsonPath = path.join(runtime.projectRoot, LOCAL_APP_PACKAGE_JSON_PATH);
|
|
81
|
+
let content;
|
|
82
|
+
try {
|
|
83
|
+
content = await readFile(packageJsonPath, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error(`Missing ${LOCAL_APP_PACKAGE_JSON_PATH} for env "${runtime.envName}" at ${packageJsonPath}.`);
|
|
87
|
+
}
|
|
88
|
+
return await parseVersionFromPackageJson(content, packageJsonPath);
|
|
89
|
+
}
|
|
90
|
+
async function resolveDockerAppVersion(runtime) {
|
|
91
|
+
const config = runtime.env.config ?? {};
|
|
92
|
+
const imageRef = `${trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY}:${trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION}`;
|
|
93
|
+
const args = [
|
|
94
|
+
'run',
|
|
95
|
+
'--rm',
|
|
96
|
+
'--network',
|
|
97
|
+
runtime.workspaceName,
|
|
98
|
+
];
|
|
99
|
+
const dockerPlatform = normalizeDockerPlatform(config.dockerPlatform);
|
|
100
|
+
if (dockerPlatform) {
|
|
101
|
+
args.push('--platform', dockerPlatform);
|
|
102
|
+
}
|
|
103
|
+
args.push('--entrypoint', 'nb', imageRef, '--version');
|
|
104
|
+
let output;
|
|
105
|
+
try {
|
|
106
|
+
output = await commandOutput('docker', args, {
|
|
107
|
+
errorName: 'docker run',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
throw new Error(`Failed to read app version for env "${runtime.envName}" from Docker image ${imageRef}. ${message}`);
|
|
113
|
+
}
|
|
114
|
+
const versionMatch = output.match(/@nocobase\/cli\/([^\s]+)/);
|
|
115
|
+
const version = trimValue(versionMatch?.[1] ?? output.replace(/^"+|"+$/g, ''));
|
|
116
|
+
if (!version) {
|
|
117
|
+
throw new Error(`Missing app version for env "${runtime.envName}" inside Docker image ${imageRef}.`);
|
|
118
|
+
}
|
|
119
|
+
return version;
|
|
120
|
+
}
|
|
121
|
+
async function resolveManagedAppVersion(runtime) {
|
|
122
|
+
if (runtime.kind === 'local') {
|
|
123
|
+
return await resolveLocalAppVersion(runtime);
|
|
124
|
+
}
|
|
125
|
+
if (runtime.kind === 'docker') {
|
|
126
|
+
return await resolveDockerAppVersion(runtime);
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Env "${runtime.envName}" does not support automatic app version detection.`);
|
|
129
|
+
}
|
|
130
|
+
export default class LicensePluginsSync extends Command {
|
|
131
|
+
static summary = 'Synchronize commercial plugins for the selected env';
|
|
132
|
+
static description = 'Synchronize the commercial plugins allowed by the current saved license key.';
|
|
133
|
+
static examples = [
|
|
134
|
+
'<%= config.bin %> <%= command.id %>',
|
|
135
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
136
|
+
'<%= config.bin %> <%= command.id %> --env app1 --dry-run',
|
|
137
|
+
'<%= config.bin %> <%= command.id %> --env app1 --json',
|
|
138
|
+
];
|
|
139
|
+
static flags = {
|
|
140
|
+
env: licenseEnvFlag,
|
|
141
|
+
json: licenseJsonFlag,
|
|
142
|
+
'pkg-url': licensePkgUrlFlag,
|
|
143
|
+
'dry-run': Flags.boolean({
|
|
144
|
+
description: 'Preview plugin changes without installing, upgrading, or removing anything',
|
|
145
|
+
default: false,
|
|
146
|
+
}),
|
|
147
|
+
version: Flags.string({
|
|
148
|
+
description: 'Registry version or dist-tag to synchronize. Defaults to the current workspace version.',
|
|
149
|
+
}),
|
|
150
|
+
verbose: Flags.boolean({
|
|
151
|
+
description: 'Show detailed per-plugin sync logs',
|
|
152
|
+
default: false,
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
async run() {
|
|
156
|
+
const { flags } = await this.parse(LicensePluginsSync);
|
|
157
|
+
const runtime = await requireLicenseRuntime(flags.env);
|
|
158
|
+
if (!flags.json) {
|
|
159
|
+
announceTargetEnv(runtime.envName);
|
|
160
|
+
}
|
|
161
|
+
const version = trimValue(flags.version) || await resolveManagedAppVersion(runtime);
|
|
162
|
+
const registryVersion = normalizePluginRegistryVersion(version);
|
|
163
|
+
const shouldStreamLogs = !flags.json && Boolean(flags.verbose);
|
|
164
|
+
const pluginStoragePath = resolvePluginStoragePath(runtime.env.storagePath);
|
|
165
|
+
const shouldShowLoading = !flags.json && !flags.verbose;
|
|
166
|
+
if (!flags.json) {
|
|
167
|
+
this.log(pc.bold(flags['dry-run']
|
|
168
|
+
? `Commercial plugin sync preview for env "${runtime.envName}"`
|
|
169
|
+
: `Commercial plugin sync for env "${runtime.envName}"`));
|
|
170
|
+
this.log(pc.dim(`App version: ${version}`));
|
|
171
|
+
if (registryVersion !== version) {
|
|
172
|
+
this.log(pc.dim(`Download version: ${registryVersion}`));
|
|
173
|
+
}
|
|
174
|
+
this.log(pc.dim(`Plugin storage path: ${pluginStoragePath}`));
|
|
175
|
+
}
|
|
176
|
+
let loadingStarted = false;
|
|
177
|
+
let loadingTimer;
|
|
178
|
+
let updateTimer;
|
|
179
|
+
let elapsedSeconds = 0;
|
|
180
|
+
if (shouldShowLoading) {
|
|
181
|
+
loadingTimer = setTimeout(() => {
|
|
182
|
+
loadingStarted = true;
|
|
183
|
+
elapsedSeconds = Math.floor(SYNC_LOADING_DELAY_MS / 1000);
|
|
184
|
+
startTask(flags['dry-run']
|
|
185
|
+
? 'Preparing commercial plugin sync preview. Please wait...'
|
|
186
|
+
: 'Synchronizing commercial plugins. Please wait...');
|
|
187
|
+
updateTimer = setInterval(() => {
|
|
188
|
+
elapsedSeconds += Math.floor(SYNC_LOADING_UPDATE_MS / 1000);
|
|
189
|
+
updateTask(flags['dry-run']
|
|
190
|
+
? `Preparing commercial plugin sync preview. Still working... (${elapsedSeconds}s elapsed)`
|
|
191
|
+
: `Synchronizing commercial plugins. Still working... (${elapsedSeconds}s elapsed)`);
|
|
192
|
+
}, SYNC_LOADING_UPDATE_MS);
|
|
193
|
+
}, SYNC_LOADING_DELAY_MS);
|
|
194
|
+
}
|
|
195
|
+
let result;
|
|
196
|
+
try {
|
|
197
|
+
result = await syncLicensedPlugins(runtime, {
|
|
198
|
+
pkgUrl: flags['pkg-url'],
|
|
199
|
+
version: registryVersion,
|
|
200
|
+
dryRun: Boolean(flags['dry-run']),
|
|
201
|
+
onProgress: shouldStreamLogs
|
|
202
|
+
? async (detail) => {
|
|
203
|
+
this.log(`${formatActionLabel(detail.action)} ${pc.bold(detail.packageName)}`);
|
|
204
|
+
this.log(pc.dim(` output: ${detail.outputDir}`));
|
|
205
|
+
if (detail.warning) {
|
|
206
|
+
this.log(pc.yellow(` warning: ${detail.warning}`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
: undefined,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
if (loadingTimer) {
|
|
214
|
+
clearTimeout(loadingTimer);
|
|
215
|
+
}
|
|
216
|
+
if (updateTimer) {
|
|
217
|
+
clearInterval(updateTimer);
|
|
218
|
+
}
|
|
219
|
+
if (loadingStarted) {
|
|
220
|
+
stopTask();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const payload = {
|
|
224
|
+
ok: true,
|
|
225
|
+
env: runtime.envName,
|
|
226
|
+
kind: runtime.kind,
|
|
227
|
+
dryRun: Boolean(flags['dry-run']),
|
|
228
|
+
version,
|
|
229
|
+
registryVersion,
|
|
230
|
+
...result,
|
|
231
|
+
};
|
|
232
|
+
if (flags.json) {
|
|
233
|
+
this.log(JSON.stringify(payload, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (loadingStarted) {
|
|
237
|
+
succeedTask(flags['dry-run']
|
|
238
|
+
? 'Commercial plugin sync preview is ready.'
|
|
239
|
+
: 'Commercial plugin sync completed.');
|
|
240
|
+
}
|
|
241
|
+
if (!flags.verbose) {
|
|
242
|
+
const changes = [];
|
|
243
|
+
if (result.installed.length > 0) {
|
|
244
|
+
changes.push(pc.green(`${result.installed.length} installed`));
|
|
245
|
+
}
|
|
246
|
+
if (result.updated.length > 0) {
|
|
247
|
+
changes.push(pc.cyan(`${result.updated.length} updated`));
|
|
248
|
+
}
|
|
249
|
+
if (result.removed.length > 0) {
|
|
250
|
+
changes.push(pc.yellow(`${result.removed.length} removed`));
|
|
251
|
+
}
|
|
252
|
+
if (result.skipped.length > 0) {
|
|
253
|
+
changes.push(pc.dim(`${result.skipped.length} skipped`));
|
|
254
|
+
}
|
|
255
|
+
if (changes.length === 0) {
|
|
256
|
+
changes.push(pc.dim('no plugin changes'));
|
|
257
|
+
}
|
|
258
|
+
this.log(`Result: ${changes.join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
this.log(`Summary: ${result.installed.length} installed, ${result.updated.length} updated, ${result.removed.length} removed, ${result.skipped.length} skipped, ${result.warnings.length} warnings`);
|
|
262
|
+
}
|
|
263
|
+
if (result.warnings.length > 0 && !flags.verbose) {
|
|
264
|
+
for (const warning of result.warnings) {
|
|
265
|
+
this.log(pc.yellow(`Warning: ${warning}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|