@zowe/imperative
Version:
framework for building configurable CLIs
604 lines • 26.2 kB
JavaScript
"use strict";
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Config = void 0;
const path = require("path");
const os = require("os");
const fs = require("fs");
const deepmerge = require("deepmerge");
const findUp = require("find-up");
const JSONC = require("comment-json");
const lodash = require("lodash");
const url_1 = require("url");
const ConfigConstants_1 = require("./ConfigConstants");
const error_1 = require("../../error");
const api_1 = require("./api");
const ConfigUtils_1 = require("./ConfigUtils");
const JsUtils_1 = require("../../utilities/src/JsUtils");
/**
* Enum used by Config class to maintain order of config layers
*/
var Layers;
(function (Layers) {
Layers[Layers["ProjectUser"] = 0] = "ProjectUser";
Layers[Layers["ProjectConfig"] = 1] = "ProjectConfig";
Layers[Layers["GlobalUser"] = 2] = "GlobalUser";
Layers[Layers["GlobalConfig"] = 3] = "GlobalConfig";
})(Layers || (Layers = {}));
/**
* The Config class provides facilities for reading and writing team
* configuration files. It is used by Imperative to perform low-level
* operations on a team configuration. The intent is that consumer
* apps will not typically use the Config class, since end-users are
* expected to write team configuration files by directly editing them
* in an editor like VSCode.
*/
class Config {
// _______________________________________________________________________
/**
* Constructor for Config class. Don't use this directly. Await `Config.load` instead.
* @param opts Options to control how Config class behaves
* @private
*/
constructor(opts) {
this.opts = opts;
}
// _______________________________________________________________________
/**
* Return a Config interface with required fields initialized as empty.
*/
static empty() {
return {
profiles: {},
defaults: {}
};
}
// _______________________________________________________________________
/**
* Load config files from disk and secure properties from vault.
* @param app App name used in config filenames (e.g., *my_cli*.config.json)
* @param opts Options to control how Config class behaves
* @throws An ImperativeError if the configuration does not load successfully
*/
static load(app, opts) {
return __awaiter(this, void 0, void 0, function* () {
opts = opts || {};
// Create the basic empty configuration
const myNewConfig = new Config();
myNewConfig.mApp = app;
myNewConfig.mActive = { user: false, global: false };
myNewConfig.mVault = opts.vault;
myNewConfig.mSecure = {};
// Populate configuration file layers
yield myNewConfig.reload(opts);
return myNewConfig;
});
}
/**
* Reload config files from disk in the current project directory.
* @param opts Options to control how Config class behaves
* @throws An ImperativeError if the configuration does not load successfully
*/
reload(opts) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
this.mLayers = [];
this.mHomeDir = (_b = (_a = opts === null || opts === void 0 ? void 0 : opts.homeDir) !== null && _a !== void 0 ? _a : this.mHomeDir) !== null && _b !== void 0 ? _b : path.join(os.homedir(), `.${this.mApp}`);
this.mProjectDir = (_c = opts === null || opts === void 0 ? void 0 : opts.projectDir) !== null && _c !== void 0 ? _c : process.cwd();
// Populate configuration file layers
for (const layer of [
Layers.ProjectUser, Layers.ProjectConfig,
Layers.GlobalUser, Layers.GlobalConfig
]) {
this.mLayers.push({
path: this.layerPath(layer),
exists: false,
properties: Config.empty(),
global: layer === Layers.GlobalUser || layer === Layers.GlobalConfig,
user: layer === Layers.ProjectUser || layer === Layers.GlobalUser,
ignoreErrors: opts === null || opts === void 0 ? void 0 : opts.ignoreErrors
});
}
// Read and populate each configuration layer
try {
let setActive = true;
for (const currLayer of this.mLayers) {
if (!(opts === null || opts === void 0 ? void 0 : opts.noLoad)) {
this.api.layers.read(currLayer);
}
// Find the active layer
if (setActive && currLayer.exists) {
this.mActive.user = currLayer.user;
this.mActive.global = currLayer.global;
setActive = false;
}
// Populate any undefined defaults
currLayer.properties.defaults = currLayer.properties.defaults || {};
currLayer.properties.profiles = currLayer.properties.profiles || {};
}
}
catch (e) {
if (e instanceof error_1.ImperativeError) {
throw e;
}
else {
throw new error_1.ImperativeError({ msg: `An unexpected error occurred during config load: ${e.message}` });
}
}
// Load secure fields unless we have already failed to load them from the vault
if (!(opts === null || opts === void 0 ? void 0 : opts.noLoad) && ((opts === null || opts === void 0 ? void 0 : opts.vault) != null || !this.api.secure.loadFailed)) {
yield this.api.secure.load(opts === null || opts === void 0 ? void 0 : opts.vault);
}
});
}
// _______________________________________________________________________
/**
* Save config files to disk and store secure properties in vault.
* @param allLayers Specify true to save all config layers instead of only the active one
*/
save(allLayers) {
return __awaiter(this, void 0, void 0, function* () {
// Save secure fields
yield this.api.secure.save(allLayers);
try {
for (const currLayer of this.mLayers) {
if (allLayers || currLayer.user === this.mActive.user && currLayer.global === this.mActive.global) {
this.api.layers.write(currLayer);
}
}
}
catch (e) {
if (e instanceof error_1.ImperativeError) {
throw e;
}
else {
throw new error_1.ImperativeError({ msg: `An unexpected error occurred during config save: ${e.message}` });
}
}
});
}
// _______________________________________________________________________
/**
* Get absolute file path for a config layer.
* For project config files, We search up from our current directory and
* ignore the Zowe hone directory (in case our current directory is under
* Zowe home.). For global config files we only retrieve config files
* from the Zowe home directory.
*
* @internal
* @param layer Enum value for config layer
*/
layerPath(layer) {
switch (layer) {
case Layers.ProjectUser: {
if (this.mProjectDir === false)
return "";
const userConfigPath = Config.search(this.userConfigName, { ignoreDirs: [this.mHomeDir], startDir: this.mProjectDir });
return userConfigPath || path.join(this.mProjectDir, this.userConfigName);
}
case Layers.ProjectConfig: {
if (this.mProjectDir === false)
return "";
const configPath = Config.search(this.configName, { ignoreDirs: [this.mHomeDir], startDir: this.mProjectDir });
return configPath || path.join(this.mProjectDir, this.configName);
}
case Layers.GlobalUser:
return path.join(this.mHomeDir, this.userConfigName);
case Layers.GlobalConfig:
return path.join(this.mHomeDir, this.configName);
}
}
// _______________________________________________________________________
/**
* Access the config API for manipulating profiles, plugins, layers, and secure values.
*/
get api() {
if (this.mApi == null) {
this.mApi = {
profiles: new api_1.ConfigProfiles(this),
plugins: new api_1.ConfigPlugins(this),
layers: new api_1.ConfigLayers(this),
secure: new api_1.ConfigSecure(this)
};
}
return this.mApi;
}
// _______________________________________________________________________
/**
* True if any config layers exist on disk, otherwise false.
*/
get exists() {
for (const layer of this.mLayers)
if (layer.exists)
return true;
return false;
}
// _______________________________________________________________________
/**
* List of absolute file paths for all config layers.
*/
get paths() {
return this.mLayers.map((layer) => layer.path);
}
// _______________________________________________________________________
/**
* List of all config layers.
* Returns a clone to prevent accidental edits of the original object.
*/
get layers() {
// Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42
return JSONC.parse(JSONC.stringify(this.mLayers, null, ConfigConstants_1.ConfigConstants.INDENT));
}
// _______________________________________________________________________
/**
* List of properties across all config layers.
* Returns a clone to prevent accidental edits of the original object.
*/
get properties() {
return this.layerMerge();
}
/**
* List of properties across all config layers.
* Returns the original object, not a clone, so use with caution.
* This is used in internal code because cloning a JSONC object is slow.
* @internal
*/
get mProperties() {
return this.layerMerge({ cloneLayers: false });
}
// _______________________________________________________________________
/**
* App name used in config filenames (e.g., *my_cli*.config.json)
*/
get appName() {
return this.mApp;
}
// _______________________________________________________________________
/**
* Filename used for config JSONC files
*/
get configName() {
return `${this.appName}${Config.END_OF_TEAM_CONFIG}`;
}
// _______________________________________________________________________
/**
* Filename used for user config JSONC files
*/
get userConfigName() {
return `${this.appName}${Config.END_OF_USER_CONFIG}`;
}
// _______________________________________________________________________
/**
* Filename used for config schema JSON files
*/
get schemaName() {
return `${this.appName}.schema.json`;
}
// _______________________________________________________________________
/**
* Schema file path used by the active layer
*/
getSchemaInfo() {
const layer = this.layerActive();
const originalSchema = layer.properties.$schema;
if (originalSchema == null) {
return {
original: null,
resolved: null,
local: false,
};
}
const tempSchema = originalSchema.startsWith("file://") ? (0, url_1.fileURLToPath)(originalSchema) : originalSchema;
const schemaFilePath = path.resolve(tempSchema.startsWith("./") ? path.join(path.dirname(layer.path), tempSchema) : tempSchema);
return {
original: originalSchema,
resolved: !JsUtils_1.JsUtils.isUrl(tempSchema) ? schemaFilePath : originalSchema,
local: !JsUtils_1.JsUtils.isUrl(tempSchema),
};
}
// _______________________________________________________________________
/**
* Search up the directory tree for the directory containing the
* specified config file.
*
* @param file Contains the name of the desired config file
* @param opts.ignoreDirs Contains an array of directory names to be
* ignored (skipped) during the search.
* @param opts.startDir Contains the name of the directory where the
* search should be started. Defaults to working directory.
*
* @returns The full path name to config file or null if not found.
*/
static search(file, opts) {
opts = opts || {};
const p = findUp.sync((directory) => {
var _a;
if ((_a = opts.ignoreDirs) === null || _a === void 0 ? void 0 : _a.includes(directory))
return;
return fs.existsSync(path.join(directory, file)) && directory;
}, { cwd: opts.startDir, type: "directory" });
return p ? path.join(p, file) : null;
}
// _______________________________________________________________________
/**
* The properties object with secure values masked.
* @type {IConfig}
* @memberof Config
*/
get maskedProperties() {
return this.layerMerge({ maskSecure: true });
}
// _______________________________________________________________________
/**
* Set value of a property in the active config layer.
* TODO: more validation
*
* @param propertyPath Property path
* @param value Property value
* @param opts Optional parameters to change behavior
* * `parseString` - If true, strings will be converted to a more specific
* type like boolean or number when possible
* * `secure` - If true, the property will be stored securely.
* If false, the property will be stored in plain text.
*/
set(propertyPath, value, opts) {
opts = opts || {};
const layer = this.layerActive();
let obj = layer.properties;
const segments = propertyPath.split(".");
propertyPath.split(".").forEach((segment) => {
if (obj[segment] == null && segments.indexOf(segment) < segments.length - 1) {
obj[segment] = {};
obj = obj[segment];
}
else if (segments.indexOf(segment) === segments.length - 1) {
if (opts === null || opts === void 0 ? void 0 : opts.parseString) {
value = ConfigUtils_1.ConfigUtils.coercePropValue(value);
}
if ((opts === null || opts === void 0 ? void 0 : opts.parseString) && Array.isArray(obj[segment])) {
obj[segment].push(value);
}
else {
obj[segment] = value;
}
}
else {
obj = obj[segment];
}
});
if (opts.secure != null) {
const secureInfo = this.api.secure.secureInfoForProp(propertyPath);
if (secureInfo != null) {
const secureProps = lodash.get(layer.properties, secureInfo.path, []);
if (opts.secure && !secureProps.includes(secureInfo.prop)) {
lodash.set(layer.properties, secureInfo.path, [...secureProps, secureInfo.prop]);
}
else if (!opts.secure && secureProps.includes(secureInfo.prop)) {
lodash.set(layer.properties, secureInfo.path, secureProps.filter((p) => p !== secureInfo.prop));
}
}
else if (opts.secure === true) {
throw new error_1.ImperativeError({ msg: "The secure option is only valid when setting a single property" });
}
}
}
// _______________________________________________________________________
/**
* Unset value of a property in the active config layer.
* @param propertyPath Property path
* @param opts Include `secure: false` to preserve property in secure array
*/
delete(propertyPath, opts) {
opts = opts || {};
const layer = this.layerActive();
lodash.unset(layer.properties, propertyPath);
if (opts.secure !== false) {
const secureInfo = this.api.secure.secureInfoForProp(propertyPath);
if (secureInfo != null) {
const secureProps = lodash.get(layer.properties, secureInfo.path);
if (secureProps != null && secureProps.includes(secureInfo.prop)) {
lodash.set(layer.properties, secureInfo.path, secureProps.filter((p) => p !== secureInfo.prop));
}
}
}
}
// _______________________________________________________________________
/**
* Set the $schema value at the top of the config JSONC.
* Also save the schema to disk if an object is provided.
* @param schema The URI of JSON schema, or a schema object to use
*/
setSchema(schema) {
const layer = this.layerActive();
const schemaUri = typeof schema === "string" ? schema : `./${this.schemaName}`;
const schemaObj = typeof schema !== "string" ? schema : null;
if (layer.properties.$schema == null) {
// Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42
layer.properties = JSONC.parse(JSONC.stringify(Object.assign({ $schema: schemaUri }, layer.properties), null, ConfigConstants_1.ConfigConstants.INDENT));
}
const schemaInfo = this.getSchemaInfo();
if (schemaObj != null && (schemaInfo.local || schemaInfo.original.startsWith("./"))) {
fs.writeFileSync(schemaInfo.resolved, JSONC.stringify(schemaObj, null, ConfigConstants_1.ConfigConstants.INDENT));
}
}
// _______________________________________________________________________
/**
* Merge the properties from multiple layers into a single Config object.
*
* @internal
* @param opts Options to control how config layers are merged
*
* @returns The resulting Config object
*/
layerMerge(opts = {}) {
// config starting point
// NOTE: "properties" and "secure" only apply to the individual layers
// NOTE: they will be blank for the merged config
const c = Config.empty();
// merge each layer
this.mLayers.forEach((layer) => {
// Merge "plugins" - create a unique set from all entries
const allPlugins = Array.from(new Set((layer.properties.plugins || []).concat(c.plugins || [])));
if (allPlugins.length > 0) {
c.plugins = allPlugins;
}
// Merge "defaults" - only add new properties from this layer
for (const [name, value] of Object.entries(layer.properties.defaults)) {
c.defaults[name] = c.defaults[name] || value;
}
if (c.autoStore == null && layer.properties.autoStore != null) {
c.autoStore = layer.properties.autoStore;
}
});
// Merge the project layer profiles
const usrProject = this.layerProfiles(this.mLayers[Layers.ProjectUser], opts);
const project = this.layerProfiles(this.mLayers[Layers.ProjectConfig], opts);
const proj = deepmerge(project, usrProject);
// Merge the global layer profiles
const usrGlobal = this.layerProfiles(this.mLayers[Layers.GlobalUser], opts);
const global = this.layerProfiles(this.mLayers[Layers.GlobalConfig], opts);
const glbl = deepmerge(global, usrGlobal);
// Traverse all the global profiles merging any missing from project profiles
c.profiles = proj;
if (!opts.excludeGlobalLayer) {
for (const [n, p] of Object.entries(glbl)) {
if (c.profiles[n] == null)
c.profiles[n] = p;
}
}
return c;
}
// _______________________________________________________________________
/**
* Obtain the profiles object for a specified layer object.
*
* @internal
* @param opts Options to control how config layers are merged
*
* @returns The resulting profile object
*/
layerProfiles(layer, opts = {}) {
let properties = layer.properties;
if (opts.cloneLayers !== false || opts.maskSecure) {
// Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42
properties = JSONC.parse(JSONC.stringify(properties, null, ConfigConstants_1.ConfigConstants.INDENT));
}
if (opts.maskSecure) {
for (const secureProp of this.api.secure.secureFields(layer)) {
if (lodash.has(properties, secureProp)) {
lodash.set(properties, secureProp, ConfigConstants_1.ConfigConstants.SECURE_VALUE);
}
}
}
return properties.profiles;
}
// _______________________________________________________________________
/**
* Find the layer with the specified user and global properties.
*
* @internal
* @param user True specifies that you want the user layer.
* @param global True specifies that you want the layer at the global level.
*
* @returns The desired layer object. Null if no layer matches.
*/
findLayer(user, global) {
for (const layer of this.mLayers || []) {
if (layer.user === user && layer.global === global)
return layer;
}
}
// _______________________________________________________________________
/**
* Obtain the layer object that is currently active.
*
* @returns The active layer object
*/
layerActive() {
const layer = this.findLayer(this.mActive.user, this.mActive.global);
if (layer != null)
return layer;
throw new error_1.ImperativeError({ msg: `internal error: no active layer found` });
}
// _______________________________________________________________________
/**
* Check if a layer exists in the given path
*
* @param inDir The directory to which you want to look for the layer.
*/
layerExists(inDir, user) {
let found = false;
// Search in all layers
this.mLayers.forEach(layer => {
found = !found && layer.exists && (typeof user !== "undefined" ? layer.user === user : true) && path.dirname(layer.path) === inDir;
});
// Search for user and non-user config in the given directory
if (!found) {
if (typeof user === "undefined") {
found = fs.existsSync(path.join(inDir, this.configName)) || fs.existsSync(path.join(inDir, this.userConfigName));
}
else {
found = fs.existsSync(path.join(inDir, user ? this.userConfigName : this.configName));
}
}
return found;
}
// _______________________________________________________________________
/**
* Form the path name of the team config file to display in messages.
* Always return the team name (not the user name).
* If the a team configuration is active, return the full path to the
* config file.
*
* @param options - a map containing option properties. Currently, the only
* property supported is a boolean named addPath.
* {addPath: true | false}
*
* @returns The path (if requested) and file name of the team config file.
*/
formMainConfigPathNm(options) {
// if a team configuration is not active, just return the file name.
let configPathNm = this.appName + Config.END_OF_TEAM_CONFIG;
if (options.addPath === false) {
// if our caller does not want the path, just return the file name.
return configPathNm;
}
if (this.exists) {
// form the full path to the team config file
configPathNm = this.api.layers.get().path;
// this.api.layers.get() returns zowe.config.user.json
// when both shared and user config files exit.
// Ensure that we use zowe.config.json, not zowe.config.user.json.
configPathNm = configPathNm.replace(Config.END_OF_USER_CONFIG, Config.END_OF_TEAM_CONFIG);
}
return configPathNm;
}
}
exports.Config = Config;
/**
* The trailing portion of a shared config file name
*/
Config.END_OF_TEAM_CONFIG = ".config.json";
/**
* The trailing portion of a user-specific config file name
*/
Config.END_OF_USER_CONFIG = ".config.user.json";
//# sourceMappingURL=Config.js.map