UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

318 lines 14.1 kB
// SPDX-License-Identifier: Apache-2.0 var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { inject, injectable } from 'tsyringe-neo'; import { SoloError } from './errors/solo-error.js'; import { MissingArgumentError } from './errors/missing-argument-error.js'; import { Flags, Flags as flags } from '../commands/flags.js'; import { patchInject } from './dependency-injection/container-helper.js'; import { StorageType } from './constants.js'; import { NamespaceName } from '../types/namespace/namespace-name.js'; import { InjectTokens } from './dependency-injection/inject-tokens.js'; import { PathEx } from '../business/utils/path-ex.js'; import { getSoloVersion } from '../../version.js'; import { isValidEnum } from './util/validation-helpers.js'; import { AsyncLocalStorage } from 'node:async_hooks'; /** * ConfigManager cache command flag values so that user doesn't need to enter the same values repeatedly. * * For example, 'namespace' is usually remains the same across commands once it is entered, and therefore user * doesn't need to enter it repeatedly. However, user should still be able to specify the flag explicitly for any command. */ let ConfigManager = class ConfigManager { logger; config; _configMaps = new Map(); // Parallel subcommands used to mutate `this.config` directly, which made // argv/flag resolution nondeterministic. Each command flow now runs against // its own scoped config snapshot to keep reads/writes isolated. configScope = new AsyncLocalStorage(); constructor(logger) { this.logger = logger; this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name); this.reset(); } /** Reset config */ reset() { this.config = { flags: {}, version: getSoloVersion(), updatedAt: new Date().toISOString(), }; } getActiveConfig() { // If no scope exists (normal single-command path), fallback to legacy // process-wide config behavior. return this.configScope.getStore() ?? this.config; } cloneActiveConfig() { // Snapshot current effective config so child command flows can mutate it // without affecting siblings. Use a shallow spread of the flags map rather // than structuredClone: structuredClone strips class prototypes (e.g. // NamespaceName becomes a plain {name} object that stringifies to // "[object Object]"). NamespaceName instances are immutable so sharing // them by reference across scopes is safe. const active = this.getActiveConfig(); return { ...active, flags: { ...active.flags }, }; } runWithScopedConfig(scopedConfig, callback) { // Bind the scoped config to the entire async call chain. return this.configScope.run(scopedConfig, callback); } /** * Apply the command flags precedence * * It uses the below precedence for command flag values: * 1. User input of the command flag * 2. Default value of the command flag if the command is not 'init'. */ applyPrecedence(argv, aliases) { const activeConfig = this.getActiveConfig(); for (const key of Object.keys(aliases)) { const flag = flags.allFlagsMap.get(key); if (flag) { if (argv[key] !== undefined) { // argv takes precedence, nothing to do } else if (this.hasFlag(flag)) { argv[key] = this.getFlag(flag); } else { argv[key] = flag.definition.defaultValue; } } } activeConfig.updatedAt = new Date().toISOString(); return argv; } /** Update the config using the argv */ update(argv) { const activeConfig = this.getActiveConfig(); if (!argv || Object.keys(argv).length === 0) { return; } for (const flag of flags.allFlags) { if (argv[flag.name] === undefined) { continue; } let value = argv[flag.name]; switch (flag.definition.type) { case 'string': { if (value && (flag.name === flags.chartDirectory.name || flag.name === flags.cacheDir.name)) { this.logger.debug(`Resolving directory path for '${flag.name}': ${value}, to: ${PathEx.resolve(value)}, note: ~/ is not supported`); value = PathEx.resolve(value); } // if it is a namespace flag then convert it to NamespaceName else if (value && (flag.name === flags.namespace.name || flag.name === flags.clusterSetupNamespace.name)) { activeConfig.flags[flag.name] = value instanceof NamespaceName ? value : NamespaceName.of(value); break; } activeConfig.flags[flag.name] = `${value}`; // force convert to string break; } case 'number': { try { activeConfig.flags[flag.name] = flags.integerFlags.has(flag.name) ? Number.parseInt(value) : Number.parseFloat(value); } catch (error) { throw new SoloError(`invalid number value '${value}': ${error.message}`, error); } break; } case 'boolean': { activeConfig.flags[flag.name] = value === true || value === 'true'; // use comparison to enforce boolean value break; } case 'StorageType': { // @ts-expect-error: TS2475: const enums can only be used in property or index access expressions if (isValidEnum(`${value}`, StorageType)) { activeConfig.flags[flag.name] = value; } else { throw new SoloError(`Invalid storage type value '${value}'`); } break; } default: { throw new SoloError(`Unsupported field type for flag '${flag.name}': ${flag.definition.type}`); } } } // store last command that was run if (argv._) { activeConfig.lastCommand = argv._; } activeConfig.updatedAt = new Date().toISOString(); const flagMessage = Object.entries(activeConfig.flags) .filter((entries) => entries[1] !== undefined && entries[1] !== null) .map(([key, value]) => { const flag = flags.allFlagsMap.get(key); const dataMask = flag.definition.dataMask; return `${key}=${dataMask || value}`; }) .join(', '); if (flagMessage) { this.logger.debug(`Updated config with flags: ${flagMessage}`); } } /** Check if a flag value is set */ hasFlag(flag) { return this.getActiveConfig().flags[flag.name] !== undefined; } /** * Return the value of the given flag * @returns value of the flag or undefined if flag value is not available */ getFlag(flag) { const activeConfig = this.getActiveConfig(); return activeConfig.flags[flag.name] === undefined ? undefined : activeConfig.flags[flag.name]; } /** Set value for the flag */ setFlag(flag, value) { const activeConfig = this.getActiveConfig(); if (!flag || !flag.name) { throw new MissingArgumentError('flag must have a name'); } // if it is a namespace then convert it to NamespaceName if (flag.name === flags.namespace.name || flag.name === flags.clusterSetupNamespace.name) { if (value instanceof NamespaceName) { activeConfig.flags[flag.name] = value; return; } activeConfig.flags[flag.name] = NamespaceName.of(value); return; } activeConfig.flags[flag.name] = value; } /** Get package version */ getVersion() { return this.getActiveConfig().version; } /** * Run prompts for the given set of flags * @param task task object from listr2 * @param flagList list of flag objects */ async executePrompt(task, flagList = []) { for (const flag of flagList) { if (flag.definition.disablePrompt || flag.prompt === undefined) { continue; } if (this.getFlag(Flags.quiet)) { return; } const input = await flag.prompt(task, this.getFlag(flag)); this.setFlag(flag, input); } } /** * Dynamically builds a class with properties from the provided list of flags * and extra properties, will keep track of which properties are used. Call * getUnusedConfigs() to get an array of unused properties. */ getConfig(configName, flags, extraProperties = []) { const getFlag = this.getFlag.bind(this); // build the dynamic class that will keep track of which properties are used class NewConfigClass { usedConfigs; constructor() { // the map to keep track of which properties are used this.usedConfigs = new Map(); // add the flags as properties to this class if (flags) { for (const flag of flags) { this[`_${flag.constName}`] = getFlag(flag); Object.defineProperty(this, flag.constName, { get() { this.usedConfigs.set(flag.constName, this.usedConfigs.get(flag.constName) + 1 || 1); return this[`_${flag.constName}`]; }, set(value) { this[`_${flag.constName}`] = value; }, }); } } // add the extra properties as properties to this class if (extraProperties) { for (const name of extraProperties) { this[`_${name}`] = ''; Object.defineProperty(this, name, { get() { this.usedConfigs.set(name, this.usedConfigs.get(name) + 1 || 1); return this[`_${name}`]; }, set(value) { this[`_${name}`] = value; }, }); } } } /** Get the list of unused configurations that were not accessed */ getUnusedConfigs() { const unusedConfigs = []; // add the flag constName to the unusedConfigs array if it was not accessed if (flags) { for (const flag of flags) { if (!this.usedConfigs.has(flag.constName)) { unusedConfigs.push(flag.constName); } } } // add the extra properties to the unusedConfigs array if it was not accessed if (extraProperties) { for (const item of extraProperties) { if (!this.usedConfigs.has(item)) { unusedConfigs.push(item); } } } return unusedConfigs; } } const newConfigInstance = new NewConfigClass(); // add the new instance to the configMaps so that it can be used to get the // unused configurations using the configName from the BaseCommand this._configMaps.set(configName, newConfigInstance); return newConfigInstance; } /** * Get the list of unused configurations that were not accessed * @returns an array of unused configurations */ getUnusedConfigs(configName) { const configMapEntry = this._configMaps.get(configName); return configMapEntry ? configMapEntry.getUnusedConfigs() : []; } getFlagFile(flag) { const value = this.getFlag(flag); if (value === flag.definition.defaultValue || !value) { const cacheDirectory = this.getFlag(flags.cacheDir) || flags.cacheDir.definition.defaultValue; return PathEx.join(cacheDirectory, flag.definition.defaultValue); } return this.getFlag(flag); } }; ConfigManager = __decorate([ injectable(), __param(0, inject(InjectTokens.SoloLogger)), __metadata("design:paramtypes", [Object]) ], ConfigManager); export { ConfigManager }; //# sourceMappingURL=config-manager.js.map