@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
package/dist/commands/install.js
CHANGED
|
@@ -17,11 +17,14 @@ import { exit } from 'node:process';
|
|
|
17
17
|
import { runPromptCatalog, } from "../lib/prompt-catalog.js";
|
|
18
18
|
import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
|
|
19
19
|
import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
|
|
20
|
+
import { defaultDockerContainerPrefix, defaultDockerNetworkName, } from '../lib/app-runtime.js';
|
|
21
|
+
import { resolveDockerContainerPrefix, resolveDockerNetworkName, } from '../lib/cli-config.js';
|
|
20
22
|
import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
|
|
23
|
+
import { validateExternalDbConfig } from "../lib/db-connection-check.js";
|
|
21
24
|
import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
|
|
22
25
|
import { run, runNocoBaseCommand } from '../lib/run-npm.js';
|
|
23
26
|
import { startTask, stopTask, updateTask } from '../lib/ui.js';
|
|
24
|
-
import {
|
|
27
|
+
import { getEnv, upsertEnv } from '../lib/auth-store.js';
|
|
25
28
|
import { buildStoredEnvConfig } from '../lib/env-config.js';
|
|
26
29
|
import Download, { defaultDockerRegistryForLang, } from './download.js';
|
|
27
30
|
import EnvAdd from "./env/add.js";
|
|
@@ -180,6 +183,16 @@ function validateBuiltinDbEnabled(value, values) {
|
|
|
180
183
|
}
|
|
181
184
|
return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
|
|
182
185
|
}
|
|
186
|
+
async function validateExternalDbPromptField(value, values) {
|
|
187
|
+
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
188
|
+
if (builtinDb) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return await validateExternalDbConfig(values);
|
|
195
|
+
}
|
|
183
196
|
function defaultInstallAppRootPath(envName) {
|
|
184
197
|
const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
|
|
185
198
|
return `./${name}/source/`;
|
|
@@ -447,6 +460,7 @@ export default class Install extends Command {
|
|
|
447
460
|
initialValue: (values) => defaultDbHostForBuiltinDb(values),
|
|
448
461
|
yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
|
|
449
462
|
required: true,
|
|
463
|
+
validate: validateExternalDbPromptField,
|
|
450
464
|
hidden: (values) => Boolean(values.builtinDb),
|
|
451
465
|
},
|
|
452
466
|
dbPort: {
|
|
@@ -464,6 +478,7 @@ export default class Install extends Command {
|
|
|
464
478
|
message: installText('prompts.dbDatabase.message'),
|
|
465
479
|
initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
|
|
466
480
|
required: true,
|
|
481
|
+
validate: validateExternalDbPromptField,
|
|
467
482
|
},
|
|
468
483
|
dbUser: {
|
|
469
484
|
type: 'text',
|
|
@@ -471,6 +486,7 @@ export default class Install extends Command {
|
|
|
471
486
|
initialValue: DEFAULT_INSTALL_DB_USER,
|
|
472
487
|
yesInitialValue: DEFAULT_INSTALL_DB_USER,
|
|
473
488
|
required: true,
|
|
489
|
+
validate: validateExternalDbPromptField,
|
|
474
490
|
},
|
|
475
491
|
dbPassword: {
|
|
476
492
|
type: 'password',
|
|
@@ -478,6 +494,7 @@ export default class Install extends Command {
|
|
|
478
494
|
initialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
479
495
|
yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
|
|
480
496
|
required: true,
|
|
497
|
+
validate: validateExternalDbPromptField,
|
|
481
498
|
},
|
|
482
499
|
};
|
|
483
500
|
static rootUserPrompts = {
|
|
@@ -741,6 +758,9 @@ export default class Install extends Command {
|
|
|
741
758
|
const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
|
|
742
759
|
const source = String(values.source ?? '').trim();
|
|
743
760
|
if (!builtinDb || source === 'docker') {
|
|
761
|
+
if (!builtinDb) {
|
|
762
|
+
return await validateExternalDbConfig({ ...values, dbPort: value });
|
|
763
|
+
}
|
|
744
764
|
return undefined;
|
|
745
765
|
}
|
|
746
766
|
return await Install.validateResumeAwareTcpPort(value, values, 'db');
|
|
@@ -765,6 +785,23 @@ export default class Install extends Command {
|
|
|
765
785
|
});
|
|
766
786
|
return reusesManagedPort ? undefined : portError;
|
|
767
787
|
}
|
|
788
|
+
static async ensureExternalDbReadyForInstall(dbResults) {
|
|
789
|
+
const builtinDb = dbResults.builtinDb === undefined ? true : Boolean(dbResults.builtinDb);
|
|
790
|
+
if (builtinDb) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const dialect = String(dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
794
|
+
const host = String(dbResults.dbHost ?? '').trim();
|
|
795
|
+
const port = String(dbResults.dbPort ?? '').trim();
|
|
796
|
+
const database = String(dbResults.dbDatabase ?? '').trim();
|
|
797
|
+
const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
|
|
798
|
+
const target = database ? `${address}/${database}` : address;
|
|
799
|
+
p.log.step(`Checking external ${dialect} database: ${target}`);
|
|
800
|
+
const validationError = await validateExternalDbConfig(dbResults);
|
|
801
|
+
if (validationError) {
|
|
802
|
+
throw new Error(validationError);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
768
805
|
static async readResumePortValidationContext(values) {
|
|
769
806
|
if (!Boolean(values.resume)) {
|
|
770
807
|
return undefined;
|
|
@@ -777,24 +814,23 @@ export default class Install extends Command {
|
|
|
777
814
|
const builtinDb = values.builtinDb === undefined ? undefined : Boolean(values.builtinDb);
|
|
778
815
|
const dbDialect = Install.toOptionalPromptString(values.dbDialect);
|
|
779
816
|
const appRootPath = Install.toOptionalPromptString(values.appRootPath);
|
|
780
|
-
const
|
|
781
|
-
|
|
817
|
+
const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
|
|
818
|
+
const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
|
|
782
819
|
return {
|
|
783
820
|
envName,
|
|
784
|
-
...(
|
|
821
|
+
...(dockerNetworkName ? { dockerNetworkName } : {}),
|
|
822
|
+
...(dockerContainerPrefix ? { dockerContainerPrefix } : {}),
|
|
785
823
|
...(source ? { source } : {}),
|
|
786
824
|
...(builtinDb !== undefined ? { builtinDb } : {}),
|
|
787
825
|
...(dbDialect ? { dbDialect } : {}),
|
|
788
826
|
...(appRootPath ? { appRootPath } : {}),
|
|
789
827
|
};
|
|
790
828
|
}
|
|
791
|
-
static async
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const stored = String(config.name ?? '').trim();
|
|
797
|
-
return stored || Install.defaultWorkspaceName();
|
|
829
|
+
static async resolveResumeDockerNetworkName() {
|
|
830
|
+
return await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() });
|
|
831
|
+
}
|
|
832
|
+
static async resolveResumeDockerContainerPrefix() {
|
|
833
|
+
return await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() });
|
|
798
834
|
}
|
|
799
835
|
static async isResumeManagedPortReuse(params) {
|
|
800
836
|
if (params.target === 'app') {
|
|
@@ -802,13 +838,13 @@ export default class Install extends Command {
|
|
|
802
838
|
&& params.context.appRootPath) {
|
|
803
839
|
return await Install.isLocalPm2ProcessUsingPort(params.context.appRootPath, params.port);
|
|
804
840
|
}
|
|
805
|
-
const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.
|
|
841
|
+
const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.dockerContainerPrefix);
|
|
806
842
|
return await Install.isDockerContainerPublishingPort(containerName, params.port);
|
|
807
843
|
}
|
|
808
844
|
if (!params.context.builtinDb || params.context.source === 'docker') {
|
|
809
845
|
return false;
|
|
810
846
|
}
|
|
811
|
-
const containerName = Install.buildBuiltinDbContainerName(params.context.envName, params.context.dbDialect ?? 'postgres', params.context.
|
|
847
|
+
const containerName = Install.buildBuiltinDbContainerName(params.context.envName, params.context.dbDialect ?? 'postgres', params.context.dockerContainerPrefix);
|
|
812
848
|
return await Install.isDockerContainerPublishingPort(containerName, params.port);
|
|
813
849
|
}
|
|
814
850
|
static async isDockerContainerPublishingPort(containerName, port) {
|
|
@@ -1148,27 +1184,33 @@ export default class Install extends Command {
|
|
|
1148
1184
|
.replace(/^-+|-+$/g, '');
|
|
1149
1185
|
return normalized || 'nocobase';
|
|
1150
1186
|
}
|
|
1151
|
-
static
|
|
1152
|
-
return Install.sanitizeDockerResourceName(
|
|
1187
|
+
static defaultDockerNetworkName() {
|
|
1188
|
+
return Install.sanitizeDockerResourceName(defaultDockerNetworkName());
|
|
1153
1189
|
}
|
|
1154
|
-
static
|
|
1155
|
-
|
|
1156
|
-
|
|
1190
|
+
static defaultDockerContainerPrefix() {
|
|
1191
|
+
return Install.sanitizeDockerResourceName(defaultDockerContainerPrefix(resolveEnvRoot(resolveDefaultConfigScope())));
|
|
1192
|
+
}
|
|
1193
|
+
static buildBuiltinDbContainerPrefix(containerPrefix) {
|
|
1194
|
+
const storedName = String(containerPrefix ?? '').trim();
|
|
1157
1195
|
return storedName
|
|
1158
1196
|
? Install.sanitizeDockerResourceName(storedName)
|
|
1159
|
-
: Install.
|
|
1197
|
+
: Install.defaultDockerContainerPrefix();
|
|
1160
1198
|
}
|
|
1161
|
-
static
|
|
1162
|
-
|
|
1199
|
+
static buildManagedDockerNetworkName(networkName) {
|
|
1200
|
+
const storedName = String(networkName ?? '').trim();
|
|
1201
|
+
return storedName
|
|
1202
|
+
? Install.sanitizeDockerResourceName(storedName)
|
|
1203
|
+
: Install.defaultDockerNetworkName();
|
|
1163
1204
|
}
|
|
1164
|
-
static buildBuiltinDbNetworkName(envName,
|
|
1165
|
-
|
|
1205
|
+
static buildBuiltinDbNetworkName(envName, networkName) {
|
|
1206
|
+
void envName;
|
|
1207
|
+
return Install.buildManagedDockerNetworkName(networkName);
|
|
1166
1208
|
}
|
|
1167
|
-
static buildBuiltinDbContainerName(envName, dbDialect,
|
|
1168
|
-
return Install.sanitizeDockerResourceName(`${Install.
|
|
1209
|
+
static buildBuiltinDbContainerName(envName, dbDialect, containerPrefix) {
|
|
1210
|
+
return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-${dbDialect}`);
|
|
1169
1211
|
}
|
|
1170
|
-
static buildDockerAppContainerName(envName,
|
|
1171
|
-
return Install.sanitizeDockerResourceName(`${Install.
|
|
1212
|
+
static buildDockerAppContainerName(envName, containerPrefix) {
|
|
1213
|
+
return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
|
|
1172
1214
|
}
|
|
1173
1215
|
static buildInitAppEnvVars(params) {
|
|
1174
1216
|
const out = {};
|
|
@@ -1194,8 +1236,8 @@ export default class Install extends Command {
|
|
|
1194
1236
|
const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
|
|
1195
1237
|
|| defaultDbPortForDialect(dbDialect);
|
|
1196
1238
|
const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
|
|
1197
|
-
const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
|
|
1198
|
-
const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.workspaceName);
|
|
1239
|
+
const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
|
|
1240
|
+
const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.dockerContainerPrefix ?? params.workspaceName);
|
|
1199
1241
|
const dbHostInput = String(params.dbHost ?? '').trim();
|
|
1200
1242
|
const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
|
|
1201
1243
|
? (dbHostInput
|
|
@@ -1485,6 +1527,8 @@ export default class Install extends Command {
|
|
|
1485
1527
|
const plan = Install.buildBuiltinDbPlan({
|
|
1486
1528
|
envName: params.envName,
|
|
1487
1529
|
workspaceName: params.workspaceName,
|
|
1530
|
+
dockerNetworkName: params.dockerNetworkName,
|
|
1531
|
+
dockerContainerPrefix: params.dockerContainerPrefix,
|
|
1488
1532
|
storagePath,
|
|
1489
1533
|
source: params.downloadResults.source,
|
|
1490
1534
|
dbDialect: params.dbResults.dbDialect,
|
|
@@ -1532,7 +1576,7 @@ export default class Install extends Command {
|
|
|
1532
1576
|
const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
|
|
1533
1577
|
const appKey = crypto.randomBytes(32).toString('hex');
|
|
1534
1578
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1535
|
-
const containerName = Install.buildDockerAppContainerName(params.envName, params.workspaceName);
|
|
1579
|
+
const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
|
|
1536
1580
|
const initEnvVars = Install.buildInitAppEnvVars({
|
|
1537
1581
|
appResults: params.appResults,
|
|
1538
1582
|
rootResults: params.rootResults,
|
|
@@ -1580,11 +1624,12 @@ export default class Install extends Command {
|
|
|
1580
1624
|
}
|
|
1581
1625
|
async installDockerApp(params) {
|
|
1582
1626
|
const networkName = params.builtinDbPlan?.networkName
|
|
1583
|
-
?? Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
|
|
1627
|
+
?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
|
|
1584
1628
|
await this.ensureDockerNetwork(networkName);
|
|
1585
1629
|
const plan = Install.buildDockerAppPlan({
|
|
1586
1630
|
envName: params.envName,
|
|
1587
1631
|
workspaceName: params.workspaceName,
|
|
1632
|
+
dockerContainerPrefix: params.dockerContainerPrefix,
|
|
1588
1633
|
appResults: params.appResults,
|
|
1589
1634
|
downloadResults: params.downloadResults,
|
|
1590
1635
|
dbResults: params.dbResults,
|
|
@@ -2041,9 +2086,13 @@ export default class Install extends Command {
|
|
|
2041
2086
|
const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
|
|
2042
2087
|
const usesDockerResources = Boolean(dbResults.builtinDb)
|
|
2043
2088
|
|| (Boolean(appResults.fetchSource) && source === 'docker');
|
|
2044
|
-
const
|
|
2045
|
-
? await
|
|
2089
|
+
const dockerNetworkName = usesDockerResources
|
|
2090
|
+
? await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() })
|
|
2091
|
+
: undefined;
|
|
2092
|
+
const dockerContainerPrefix = usesDockerResources
|
|
2093
|
+
? await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() })
|
|
2046
2094
|
: undefined;
|
|
2095
|
+
await Install.ensureExternalDbReadyForInstall(dbResults);
|
|
2047
2096
|
if (!parsed.resume) {
|
|
2048
2097
|
await this.saveInstalledEnv({
|
|
2049
2098
|
envName,
|
|
@@ -2053,12 +2102,14 @@ export default class Install extends Command {
|
|
|
2053
2102
|
rootResults,
|
|
2054
2103
|
envAddResults,
|
|
2055
2104
|
});
|
|
2105
|
+
p.log.info(`Saved install config for env "${envName}"`);
|
|
2056
2106
|
}
|
|
2057
2107
|
let builtinDbPlan;
|
|
2058
2108
|
if (Boolean(dbResults.builtinDb)) {
|
|
2059
2109
|
builtinDbPlan = await this.startBuiltinDb({
|
|
2060
2110
|
envName,
|
|
2061
|
-
|
|
2111
|
+
dockerNetworkName,
|
|
2112
|
+
dockerContainerPrefix,
|
|
2062
2113
|
appResults,
|
|
2063
2114
|
downloadResults,
|
|
2064
2115
|
dbResults,
|
|
@@ -2082,7 +2133,8 @@ export default class Install extends Command {
|
|
|
2082
2133
|
});
|
|
2083
2134
|
dockerAppPlan = await this.installDockerApp({
|
|
2084
2135
|
envName,
|
|
2085
|
-
|
|
2136
|
+
dockerNetworkName,
|
|
2137
|
+
dockerContainerPrefix,
|
|
2086
2138
|
appResults,
|
|
2087
2139
|
downloadResults,
|
|
2088
2140
|
dbResults,
|
|
@@ -0,0 +1,360 @@
|
|
|
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 * as p from '@clack/prompts';
|
|
10
|
+
import { Command, Flags } from '@oclif/core';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { ensureInstanceId, licenseEnvFlag, licenseJsonFlag, licensePkgUrlFlag, redactLicenseKey, requireLicenseRuntime, resolveLicenseKeyFile, resolveLicenseServiceUrl, saveLicenseKey, sanitizeLicenseOutput, validateLicenseKey, } from './shared.js';
|
|
13
|
+
import { announceTargetEnv, isInteractiveTerminal } from '../../lib/ui.js';
|
|
14
|
+
import { appUrl } from '../env/shared.js';
|
|
15
|
+
function resolveOnlineInputValue(value) {
|
|
16
|
+
return String(value ?? '').trim();
|
|
17
|
+
}
|
|
18
|
+
async function promptActivationMode() {
|
|
19
|
+
const answer = await p.select({
|
|
20
|
+
message: 'How do you want to activate the license?',
|
|
21
|
+
options: [
|
|
22
|
+
{ value: 'key', label: 'Use an existing license key' },
|
|
23
|
+
{ value: 'online', label: 'Request and activate a license online' },
|
|
24
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
25
|
+
],
|
|
26
|
+
initialValue: 'key',
|
|
27
|
+
});
|
|
28
|
+
if (p.isCancel(answer)) {
|
|
29
|
+
p.cancel('License activation cancelled.');
|
|
30
|
+
return 'cancel';
|
|
31
|
+
}
|
|
32
|
+
return answer;
|
|
33
|
+
}
|
|
34
|
+
async function promptLicenseKeyInput() {
|
|
35
|
+
const answer = await p.select({
|
|
36
|
+
message: 'How do you want to provide the license key?',
|
|
37
|
+
options: [
|
|
38
|
+
{ value: 'key', label: 'Paste the license key' },
|
|
39
|
+
{ value: 'file', label: 'Read the key from a file' },
|
|
40
|
+
],
|
|
41
|
+
initialValue: 'key',
|
|
42
|
+
});
|
|
43
|
+
if (p.isCancel(answer)) {
|
|
44
|
+
p.cancel('License activation cancelled.');
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
if (answer === 'key') {
|
|
48
|
+
const key = await p.text({
|
|
49
|
+
message: 'License key',
|
|
50
|
+
validate: (value) => String(value ?? '').trim() ? undefined : 'License key is required.',
|
|
51
|
+
});
|
|
52
|
+
if (p.isCancel(key)) {
|
|
53
|
+
p.cancel('License activation cancelled.');
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
return { key: String(key ?? '').trim() || undefined };
|
|
57
|
+
}
|
|
58
|
+
const keyFile = await p.text({
|
|
59
|
+
message: 'Path to the license key file',
|
|
60
|
+
validate: (value) => String(value ?? '').trim() ? undefined : 'License key file path is required.',
|
|
61
|
+
});
|
|
62
|
+
if (p.isCancel(keyFile)) {
|
|
63
|
+
p.cancel('License activation cancelled.');
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
return { keyFile: String(keyFile ?? '').trim() || undefined };
|
|
67
|
+
}
|
|
68
|
+
async function promptOnlineActivationInput(initial) {
|
|
69
|
+
let account = String(initial.account ?? '').trim();
|
|
70
|
+
if (!account) {
|
|
71
|
+
const answer = await p.text({
|
|
72
|
+
message: 'Service account',
|
|
73
|
+
validate: (value) => String(value ?? '').trim() ? undefined : 'Service account is required.',
|
|
74
|
+
});
|
|
75
|
+
if (p.isCancel(answer)) {
|
|
76
|
+
p.cancel('License activation cancelled.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
account = String(answer ?? '').trim();
|
|
80
|
+
}
|
|
81
|
+
if (!account) {
|
|
82
|
+
p.cancel('License activation cancelled.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let password = String(initial.password ?? '').trim();
|
|
86
|
+
if (!password) {
|
|
87
|
+
const answer = await p.password({
|
|
88
|
+
message: 'Service password',
|
|
89
|
+
validate: (value) => String(value ?? '').trim() ? undefined : 'Service password is required.',
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(answer)) {
|
|
92
|
+
p.cancel('License activation cancelled.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
password = String(answer ?? '').trim();
|
|
96
|
+
}
|
|
97
|
+
if (!password) {
|
|
98
|
+
p.cancel('License activation cancelled.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let appName = String(initial.appName ?? '').trim();
|
|
102
|
+
if (!appName) {
|
|
103
|
+
const answer = await p.text({
|
|
104
|
+
message: 'Application name',
|
|
105
|
+
validate: (value) => String(value ?? '').trim() ? undefined : 'Application name is required.',
|
|
106
|
+
});
|
|
107
|
+
if (p.isCancel(answer)) {
|
|
108
|
+
p.cancel('License activation cancelled.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
appName = String(answer ?? '').trim();
|
|
112
|
+
}
|
|
113
|
+
if (!appName) {
|
|
114
|
+
p.cancel('License activation cancelled.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const confirmedAnswer = typeof initial.confirmed === 'boolean'
|
|
118
|
+
? initial.confirmed
|
|
119
|
+
: await p.confirm({
|
|
120
|
+
message: 'Confirm that the submitted license information is true and accurate?',
|
|
121
|
+
initialValue: false,
|
|
122
|
+
});
|
|
123
|
+
if (p.isCancel(confirmedAnswer)) {
|
|
124
|
+
p.cancel('License activation cancelled.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
account,
|
|
129
|
+
password,
|
|
130
|
+
appName,
|
|
131
|
+
confirmed: Boolean(confirmedAnswer),
|
|
132
|
+
serviceUrl: await resolveLicenseServiceUrl(initial.serviceUrl),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function resolveAppUrlOrThrow(runtime) {
|
|
136
|
+
const currentAppUrl = appUrl(runtime);
|
|
137
|
+
if (!currentAppUrl) {
|
|
138
|
+
throw new Error(`Env "${runtime.envName}" does not have an app URL or app port configured.`);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return new URL(currentAppUrl).toString();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error(`Env "${runtime.envName}" has an invalid app URL: ${currentAppUrl}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function requestOnlineLicenseKey(serviceUrl, account, password, payload) {
|
|
148
|
+
const response = await fetch(`${serviceUrl}/license-key`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'content-type': 'application/json',
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
account,
|
|
155
|
+
password,
|
|
156
|
+
appUrl: payload.appUrl,
|
|
157
|
+
appName: payload.appName,
|
|
158
|
+
instanceId: payload.instanceId,
|
|
159
|
+
type: payload.type,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`License service request failed with status ${response.status}.`);
|
|
164
|
+
}
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
const key = String(data?.data?.key ?? '').trim();
|
|
167
|
+
if (!key) {
|
|
168
|
+
throw new Error('License service did not return a license key.');
|
|
169
|
+
}
|
|
170
|
+
return key;
|
|
171
|
+
}
|
|
172
|
+
export default class LicenseActivate extends Command {
|
|
173
|
+
static summary = 'Activate commercial licensing for the selected env';
|
|
174
|
+
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.';
|
|
175
|
+
static examples = [
|
|
176
|
+
'<%= config.bin %> <%= command.id %> --env app1 --key <licenseKey>',
|
|
177
|
+
'<%= config.bin %> <%= command.id %> --env app1 --key-file ./license.txt',
|
|
178
|
+
'<%= config.bin %> <%= command.id %> --env app1 --online',
|
|
179
|
+
'<%= config.bin %> <%= command.id %> --env app1 --online --account aa --password bb --desc test24 --yes',
|
|
180
|
+
'<%= config.bin %> <%= command.id %> --env app1 --json --key-file ./license.txt',
|
|
181
|
+
];
|
|
182
|
+
static flags = {
|
|
183
|
+
env: licenseEnvFlag,
|
|
184
|
+
json: licenseJsonFlag,
|
|
185
|
+
key: Flags.string({
|
|
186
|
+
description: 'Existing license key to activate',
|
|
187
|
+
}),
|
|
188
|
+
'key-file': Flags.string({
|
|
189
|
+
description: 'Path to a file containing the license key to activate',
|
|
190
|
+
}),
|
|
191
|
+
online: Flags.boolean({
|
|
192
|
+
description: 'Request a license online and activate it',
|
|
193
|
+
default: false,
|
|
194
|
+
}),
|
|
195
|
+
account: Flags.string({
|
|
196
|
+
description: 'License service account for online activation',
|
|
197
|
+
}),
|
|
198
|
+
password: Flags.string({
|
|
199
|
+
description: 'License service password for online activation',
|
|
200
|
+
}),
|
|
201
|
+
desc: Flags.string({
|
|
202
|
+
description: 'Application name for online activation',
|
|
203
|
+
}),
|
|
204
|
+
'pkg-url': licensePkgUrlFlag,
|
|
205
|
+
yes: Flags.boolean({
|
|
206
|
+
description: 'Confirm that the submitted application information is true and accurate',
|
|
207
|
+
default: false,
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
async run() {
|
|
211
|
+
const { flags } = await this.parse(LicenseActivate);
|
|
212
|
+
const runtime = await requireLicenseRuntime(flags.env);
|
|
213
|
+
if (!flags.json) {
|
|
214
|
+
announceTargetEnv(runtime.envName);
|
|
215
|
+
}
|
|
216
|
+
let key = String(flags.key ?? '').trim();
|
|
217
|
+
let keyFile = String(flags['key-file'] ?? '').trim();
|
|
218
|
+
let online = Boolean(flags.online);
|
|
219
|
+
if (!key && !keyFile && !online) {
|
|
220
|
+
if (!isInteractiveTerminal()) {
|
|
221
|
+
this.error('Provide --key, --key-file, or --online to continue.');
|
|
222
|
+
}
|
|
223
|
+
const mode = await promptActivationMode();
|
|
224
|
+
if (mode === 'cancel') {
|
|
225
|
+
this.log('Cancelled license activation.');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (mode === 'online') {
|
|
229
|
+
online = true;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const prompted = await promptLicenseKeyInput();
|
|
233
|
+
key = String(prompted.key ?? '').trim();
|
|
234
|
+
keyFile = String(prompted.keyFile ?? '').trim();
|
|
235
|
+
if (!key && !keyFile) {
|
|
236
|
+
this.error('License key input was empty.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if ((key || keyFile) && online) {
|
|
241
|
+
this.error('Use either an existing key (--key / --key-file) or --online, not both.');
|
|
242
|
+
}
|
|
243
|
+
if (online) {
|
|
244
|
+
const resolvedServiceUrl = await resolveLicenseServiceUrl(flags['pkg-url']);
|
|
245
|
+
const initialOnline = {
|
|
246
|
+
account: resolveOnlineInputValue(flags.account),
|
|
247
|
+
password: resolveOnlineInputValue(flags.password),
|
|
248
|
+
appName: resolveOnlineInputValue(flags.desc),
|
|
249
|
+
confirmed: flags.yes ? true : undefined,
|
|
250
|
+
serviceUrl: resolvedServiceUrl,
|
|
251
|
+
};
|
|
252
|
+
let onlineInput = initialOnline;
|
|
253
|
+
if (!onlineInput.account
|
|
254
|
+
|| !onlineInput.password
|
|
255
|
+
|| !onlineInput.appName
|
|
256
|
+
|| !onlineInput.confirmed) {
|
|
257
|
+
if (!isInteractiveTerminal()) {
|
|
258
|
+
this.error('Online activation requires --account, --password, --desc, and --yes when not using a TTY.');
|
|
259
|
+
}
|
|
260
|
+
const prompted = await promptOnlineActivationInput(initialOnline);
|
|
261
|
+
if (!prompted) {
|
|
262
|
+
this.log('Cancelled license activation.');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
onlineInput = prompted;
|
|
266
|
+
}
|
|
267
|
+
if (!onlineInput.confirmed) {
|
|
268
|
+
this.error('Online activation requires confirmation that the submitted application information is true and accurate.');
|
|
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
|
+
const resolvedKey = key || String(await readFile(keyFile, 'utf8')).trim();
|
|
321
|
+
const validation = await validateLicenseKey(runtime, resolvedKey);
|
|
322
|
+
const ok = !validation.keyStatus
|
|
323
|
+
&& validation.envMatch
|
|
324
|
+
&& validation.domainMatch
|
|
325
|
+
&& validation.licenseStatus === 'active';
|
|
326
|
+
const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
|
|
327
|
+
const payload = {
|
|
328
|
+
ok,
|
|
329
|
+
env: runtime.envName,
|
|
330
|
+
kind: runtime.kind,
|
|
331
|
+
instanceId: await ensureInstanceId(runtime),
|
|
332
|
+
mode: 'key',
|
|
333
|
+
key: redactLicenseKey(resolvedKey),
|
|
334
|
+
keyFile: keyFile || undefined,
|
|
335
|
+
licenseKeyPath,
|
|
336
|
+
validation: sanitizeLicenseOutput(validation),
|
|
337
|
+
};
|
|
338
|
+
if (flags.json) {
|
|
339
|
+
this.log(JSON.stringify(payload, null, 2));
|
|
340
|
+
if (!ok) {
|
|
341
|
+
this.exit(1);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!ok) {
|
|
346
|
+
const reason = validation.keyStatus
|
|
347
|
+
? `license key is ${validation.keyStatus}`
|
|
348
|
+
: !validation.envMatch
|
|
349
|
+
? 'license key does not match the current instance environment'
|
|
350
|
+
: !validation.domainMatch
|
|
351
|
+
? 'license key does not match the current app domain'
|
|
352
|
+
: 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}.`);
|
|
356
|
+
}
|
|
357
|
+
this.log(`Activated the license for env "${runtime.envName}".`);
|
|
358
|
+
this.log(`Saved license key at ${licenseKeyPath}`);
|
|
359
|
+
}
|
|
360
|
+
}
|