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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +2 -3
  4. package/dist/commands/app/upgrade.js +112 -128
  5. package/dist/commands/config/delete.js +30 -0
  6. package/dist/commands/config/get.js +29 -0
  7. package/dist/commands/config/index.js +20 -0
  8. package/dist/commands/config/list.js +29 -0
  9. package/dist/commands/config/set.js +35 -0
  10. package/dist/commands/db/check.js +230 -0
  11. package/dist/commands/db/shared.js +1 -1
  12. package/dist/commands/env/shared.js +1 -1
  13. package/dist/commands/init.js +0 -1
  14. package/dist/commands/install.js +87 -35
  15. package/dist/commands/license/activate.js +357 -0
  16. package/dist/commands/license/env.js +94 -0
  17. package/dist/commands/license/generate-id.js +107 -0
  18. package/dist/commands/license/id.js +52 -0
  19. package/dist/commands/license/index.js +20 -0
  20. package/dist/commands/license/plugins/clean.js +98 -0
  21. package/dist/commands/license/plugins/index.js +20 -0
  22. package/dist/commands/license/plugins/list.js +50 -0
  23. package/dist/commands/license/plugins/shared.js +325 -0
  24. package/dist/commands/license/plugins/sync.js +267 -0
  25. package/dist/commands/license/shared.js +411 -0
  26. package/dist/commands/license/status.js +50 -0
  27. package/dist/lib/api-client.js +74 -3
  28. package/dist/lib/app-runtime.js +26 -10
  29. package/dist/lib/auth-store.js +29 -66
  30. package/dist/lib/build-config.js +8 -0
  31. package/dist/lib/cli-config.js +176 -0
  32. package/dist/lib/cli-home.js +6 -21
  33. package/dist/lib/db-connection-check.js +178 -0
  34. package/dist/lib/generated-command.js +23 -3
  35. package/dist/lib/plugin-storage.js +127 -0
  36. package/dist/lib/prompt-validators.js +4 -4
  37. package/dist/lib/runtime-generator.js +89 -10
  38. package/dist/lib/self-manager.js +57 -2
  39. package/dist/lib/startup-update.js +85 -7
  40. package/dist/locale/en-US.json +16 -13
  41. package/dist/locale/zh-CN.json +16 -13
  42. package/nocobase-ctl.config.json +82 -0
  43. package/package.json +16 -4
@@ -0,0 +1,98 @@
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 { licenseEnvFlag, licenseJsonFlag, licensePkgUrlFlag, requireLicenseRuntime } from '../shared.js';
12
+ import { cleanLicensedPlugins } from './shared.js';
13
+ import { resolvePluginStoragePath } from '../../../lib/plugin-storage.js';
14
+ function formatActionLabel(action) {
15
+ switch (action) {
16
+ case 'removed':
17
+ return pc.yellow('removed');
18
+ case 'skipped':
19
+ return pc.dim('skipped');
20
+ }
21
+ }
22
+ export default class LicensePluginsClean extends Command {
23
+ static summary = 'Remove downloaded commercial plugins for the selected env';
24
+ static description = 'Remove local commercial plugin downloads for the selected env without changing license activation.';
25
+ static examples = [
26
+ '<%= config.bin %> <%= command.id %>',
27
+ '<%= config.bin %> <%= command.id %> --env app1',
28
+ '<%= config.bin %> <%= command.id %> --env app1 --dry-run',
29
+ '<%= config.bin %> <%= command.id %> --env app1 --verbose',
30
+ '<%= config.bin %> <%= command.id %> --env app1 --json',
31
+ ];
32
+ static flags = {
33
+ env: licenseEnvFlag,
34
+ json: licenseJsonFlag,
35
+ 'pkg-url': licensePkgUrlFlag,
36
+ 'dry-run': Flags.boolean({
37
+ description: 'Preview which commercial plugins would be removed without deleting anything',
38
+ default: false,
39
+ }),
40
+ verbose: Flags.boolean({
41
+ char: 'V',
42
+ description: 'Show detailed per-plugin clean logs',
43
+ default: false,
44
+ }),
45
+ };
46
+ async run() {
47
+ const { flags } = await this.parse(LicensePluginsClean);
48
+ const runtime = await requireLicenseRuntime(flags.env);
49
+ const shouldStreamLogs = !flags.json && Boolean(flags.verbose);
50
+ const pluginStoragePath = resolvePluginStoragePath(runtime.env.storagePath);
51
+ if (!flags.json) {
52
+ this.log(pc.bold(flags['dry-run']
53
+ ? `Commercial plugin clean preview for env "${runtime.envName}"`
54
+ : `Commercial plugin clean for env "${runtime.envName}"`));
55
+ this.log(pc.dim(`Plugin storage path: ${pluginStoragePath}`));
56
+ }
57
+ const result = await cleanLicensedPlugins(runtime, {
58
+ pkgUrl: flags['pkg-url'],
59
+ dryRun: Boolean(flags['dry-run']),
60
+ onProgress: shouldStreamLogs
61
+ ? async (detail) => {
62
+ this.log(`${formatActionLabel(detail.action)} ${pc.bold(detail.packageName)}`);
63
+ this.log(pc.dim(` output: ${detail.outputDir}`));
64
+ if (detail.action === 'removed') {
65
+ this.log(pc.dim(` symlink: ${detail.removedSymlink ? 'removed' : 'not found'}`));
66
+ }
67
+ }
68
+ : undefined,
69
+ });
70
+ const payload = {
71
+ ok: true,
72
+ env: runtime.envName,
73
+ kind: runtime.kind,
74
+ dryRun: Boolean(flags['dry-run']),
75
+ ...result,
76
+ };
77
+ if (flags.json) {
78
+ this.log(JSON.stringify(payload, null, 2));
79
+ return;
80
+ }
81
+ if (!flags.verbose) {
82
+ const changes = [];
83
+ if (result.removed.length > 0) {
84
+ changes.push(pc.yellow(`${result.removed.length} removed`));
85
+ }
86
+ if (result.skipped.length > 0) {
87
+ changes.push(pc.dim(`${result.skipped.length} skipped`));
88
+ }
89
+ if (changes.length === 0) {
90
+ changes.push(pc.dim('no plugin changes'));
91
+ }
92
+ this.log(`Result: ${changes.join(', ')}`);
93
+ }
94
+ else {
95
+ this.log(`Summary: ${result.removed.length} removed, ${result.skipped.length} skipped`);
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,20 @@
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, loadHelpClass } from '@oclif/core';
10
+ export default class LicensePlugins extends Command {
11
+ static summary = 'List or synchronize commercial plugins allowed by the current license';
12
+ async run() {
13
+ await this.parse(LicensePlugins);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'license plugins',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command } from '@oclif/core';
10
+ import { licenseEnvFlag, licenseJsonFlag, licensePkgUrlFlag, requireLicenseRuntime } from '../shared.js';
11
+ import { fetchLicensedPluginPackages } from './shared.js';
12
+ import { renderTable } from '../../../lib/ui.js';
13
+ export default class LicensePluginsList extends Command {
14
+ static summary = 'List commercial plugins available to the selected env';
15
+ static description = 'Show the commercial plugins associated with the current saved license key for the selected env.';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %>',
18
+ '<%= config.bin %> <%= command.id %> --env app1',
19
+ '<%= config.bin %> <%= command.id %> --env app1 --json',
20
+ ];
21
+ static flags = {
22
+ env: licenseEnvFlag,
23
+ json: licenseJsonFlag,
24
+ 'pkg-url': licensePkgUrlFlag,
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(LicensePluginsList);
28
+ const runtime = await requireLicenseRuntime(flags.env);
29
+ const { commercialPlugins, licensedPlugins } = await fetchLicensedPluginPackages(runtime, {
30
+ pkgUrl: flags['pkg-url'],
31
+ });
32
+ const payload = {
33
+ ok: true,
34
+ env: runtime.envName,
35
+ kind: runtime.kind,
36
+ commercialPlugins,
37
+ licensedPlugins,
38
+ };
39
+ if (flags.json) {
40
+ this.log(JSON.stringify(payload, null, 2));
41
+ return;
42
+ }
43
+ const rows = commercialPlugins.map((pluginName) => [
44
+ pluginName,
45
+ licensedPlugins.includes(pluginName) ? 'licensed' : 'unlicensed',
46
+ ]);
47
+ this.log(`Commercial plugins for env "${runtime.envName}"`);
48
+ this.log(renderTable(['Package', 'Status'], rows));
49
+ }
50
+ }
@@ -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
+ }