@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.
Files changed (61) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +12 -6
  4. package/dist/commands/app/logs.js +2 -2
  5. package/dist/commands/app/start.js +2 -1
  6. package/dist/commands/app/stop.js +2 -1
  7. package/dist/commands/app/upgrade.js +116 -129
  8. package/dist/commands/config/delete.js +30 -0
  9. package/dist/commands/config/get.js +29 -0
  10. package/dist/commands/config/index.js +20 -0
  11. package/dist/commands/config/list.js +29 -0
  12. package/dist/commands/config/set.js +35 -0
  13. package/dist/commands/db/check.js +238 -0
  14. package/dist/commands/db/logs.js +2 -2
  15. package/dist/commands/db/shared.js +6 -5
  16. package/dist/commands/db/start.js +2 -1
  17. package/dist/commands/db/stop.js +2 -1
  18. package/dist/commands/env/info.js +6 -2
  19. package/dist/commands/env/shared.js +1 -1
  20. package/dist/commands/init.js +0 -1
  21. package/dist/commands/install.js +87 -35
  22. package/dist/commands/license/activate.js +360 -0
  23. package/dist/commands/license/env.js +94 -0
  24. package/dist/commands/license/generate-id.js +108 -0
  25. package/dist/commands/license/id.js +56 -0
  26. package/dist/commands/license/index.js +20 -0
  27. package/dist/commands/license/plugins/clean.js +101 -0
  28. package/dist/commands/license/plugins/index.js +20 -0
  29. package/dist/commands/license/plugins/list.js +50 -0
  30. package/dist/commands/license/plugins/shared.js +325 -0
  31. package/dist/commands/license/plugins/sync.js +269 -0
  32. package/dist/commands/license/shared.js +414 -0
  33. package/dist/commands/license/status.js +50 -0
  34. package/dist/commands/plugin/disable.js +2 -0
  35. package/dist/commands/plugin/enable.js +2 -0
  36. package/dist/commands/source/dev.js +2 -1
  37. package/dist/lib/api-client.js +74 -3
  38. package/dist/lib/app-managed-resources.js +10 -6
  39. package/dist/lib/app-runtime.js +29 -11
  40. package/dist/lib/auth-store.js +36 -68
  41. package/dist/lib/bootstrap.js +0 -4
  42. package/dist/lib/build-config.js +8 -0
  43. package/dist/lib/builtin-db.js +86 -0
  44. package/dist/lib/cli-config.js +176 -0
  45. package/dist/lib/cli-home.js +6 -21
  46. package/dist/lib/db-connection-check.js +178 -0
  47. package/dist/lib/env-config.js +7 -0
  48. package/dist/lib/generated-command.js +24 -3
  49. package/dist/lib/plugin-storage.js +127 -0
  50. package/dist/lib/prompt-validators.js +4 -4
  51. package/dist/lib/run-npm.js +53 -0
  52. package/dist/lib/runtime-env-vars.js +32 -0
  53. package/dist/lib/runtime-generator.js +89 -10
  54. package/dist/lib/self-manager.js +57 -2
  55. package/dist/lib/skills-manager.js +2 -2
  56. package/dist/lib/startup-update.js +85 -7
  57. package/dist/lib/ui.js +3 -0
  58. package/dist/locale/en-US.json +16 -13
  59. package/dist/locale/zh-CN.json +16 -13
  60. package/nocobase-ctl.config.json +82 -0
  61. package/package.json +16 -4
@@ -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 { ensureWorkspaceName, getEnv, loadAuthConfig, upsertEnv } from '../lib/auth-store.js';
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 workspaceName = Install.toOptionalPromptString(values.workspaceName)
781
- ?? await Install.resolveResumeWorkspaceName(envName);
817
+ const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
818
+ const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
782
819
  return {
783
820
  envName,
784
- ...(workspaceName ? { workspaceName } : {}),
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 resolveResumeWorkspaceName(envName) {
792
- if (!envName) {
793
- return undefined;
794
- }
795
- const config = await loadAuthConfig({ scope: resolveDefaultConfigScope() });
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.workspaceName);
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.workspaceName);
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 defaultWorkspaceName() {
1152
- return Install.sanitizeDockerResourceName(`nb-${path.basename(resolveEnvRoot(resolveDefaultConfigScope()))}`);
1187
+ static defaultDockerNetworkName() {
1188
+ return Install.sanitizeDockerResourceName(defaultDockerNetworkName());
1153
1189
  }
1154
- static buildBuiltinDbResourcePrefix(envName, workspaceName) {
1155
- void envName;
1156
- const storedName = String(workspaceName ?? '').trim();
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.defaultWorkspaceName();
1197
+ : Install.defaultDockerContainerPrefix();
1160
1198
  }
1161
- static async ensureWorkspaceName() {
1162
- return await ensureWorkspaceName(Install.defaultWorkspaceName(), { scope: resolveDefaultConfigScope() });
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, workspaceName) {
1165
- return Install.buildBuiltinDbResourcePrefix(envName, workspaceName);
1205
+ static buildBuiltinDbNetworkName(envName, networkName) {
1206
+ void envName;
1207
+ return Install.buildManagedDockerNetworkName(networkName);
1166
1208
  }
1167
- static buildBuiltinDbContainerName(envName, dbDialect, workspaceName) {
1168
- return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-${dbDialect}`);
1209
+ static buildBuiltinDbContainerName(envName, dbDialect, containerPrefix) {
1210
+ return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-${dbDialect}`);
1169
1211
  }
1170
- static buildDockerAppContainerName(envName, workspaceName) {
1171
- return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-app`);
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 workspaceName = usesDockerResources
2045
- ? await Install.ensureWorkspaceName()
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
- workspaceName,
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
- workspaceName,
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
+ }