@feflow/cli
Version:
A front-end flow tool.
724 lines (676 loc) • 24 kB
text/typescript
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import Feflow from '../';
import { getTag, checkoutVersion, getCurrentTag } from '../universal-pkg/repository/git';
import { Plugin } from '../universal-pkg/schema/plugin';
import Linker from '../universal-pkg/linker';
import { UniversalPkg } from '../universal-pkg/dep/pkg';
import versionImpl from '../universal-pkg/dep/version';
import applyPlugins, { resolvePlugin } from '../plugin/apply-plugins';
import { CacheController } from '../command-picker';
import packageJson from '../../shared/package-json';
import { download } from '../../shared/git';
import {
HOOK_TYPE_ON_COMMAND_REGISTERED,
LATEST_VERSION,
FEFLOW_BIN,
FEFLOW_LIB,
NPM_PLUGIN_INFO_JSON,
INVALID_VERSION,
FEFLOW_PLUGIN_GIT_PREFIX,
FEFLOW_PLUGIN_PREFIX,
FEFLOW_PLUGIN_LOCAL_PREFIX,
SILENT_ARG,
DISABLE_ARG,
UNIVERSAL_PLUGIN_CONFIG,
} from '../../shared/constant';
import { getRegistryUrl, install } from '../../shared/npm';
import { getURL } from '../../shared/url';
import { copyDir } from '../../shared/fs';
import { Logger } from '../logger';
export default (ctx: Feflow) => {
ctx.commander.register('install', 'Install a devkit or plugin', async () => {
const dependencies = ctx.args._;
const installPluginStr = dependencies[0];
if (!installPluginStr) {
ctx.logger.error('parameter error');
return;
}
try {
await installPlugin(ctx, installPluginStr, true);
} catch (e) {
ctx.logger.error('install error: ', e);
process.exit(2);
}
});
ctx.commander.register('uninstall', 'Uninstall a devkit or plugin', async () => {
const dependencies = ctx.args._;
ctx.logger.info('Uninstalling packages. This might take a couple of minutes.');
const serverUrl = ctx.config?.serverUrl;
if (!serverUrl) {
return uninstallNpmPlugin(ctx, dependencies);
}
const { universalPkg } = ctx;
const installPluginStr = dependencies[0];
const pkgInfo = await getPkgInfo(ctx, installPluginStr);
if (pkgInfo && universalPkg.isInstalled(pkgInfo.repoName)) {
return uninstallUniversalPlugin(ctx, pkgInfo.repoName);
}
await uninstallNpmPlugin(ctx, dependencies);
const pickerConfig = new CacheController(ctx);
pickerConfig.removeCache(dependencies[0]);
});
};
interface PkgJson {
dependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
}
export async function getRepoInfo(ctx: Feflow, packageName: string) {
const serverUrl = ctx.config?.serverUrl;
if (!serverUrl) {
return Promise.reject('cannot find \'serverUrl\' from config file');
}
const url = getURL(serverUrl, `apply/getlist?name=${packageName}`);
if (!url) {
return Promise.reject(`the serverUrl is invalid: ${serverUrl}`);
}
return axios
.get(url, {
proxy: false,
})
.then((res) => {
const data = res.data || {};
return (data.data?.length > 0 && data.data[0]) || [];
})
.catch((e: unknown) => {
ctx.logger.debug('Get repo info error', e);
});
}
// git@github.com:tencent/feflow.git
// or http[s]://github.com/tencent/feflow.git or http[s]://user:pwd@github.com/tencent/feflow.git
// to
// encodeURIComponent("github.com/tencent/feflow.git")
function getGitRepoName(repoUrl: string): string | undefined {
const ret = /^((http:\/\/|https:\/\/)(.*?@)?|git@)/.exec(repoUrl);
let repurl = '';
if (Array.isArray(ret) && ret.length > 0) {
repurl = repoUrl.substring(ret[0].length);
}
return encodeURIComponent(FEFLOW_PLUGIN_GIT_PREFIX + repurl.split(':').join('/'));
}
function getDirRepoName(dir: string): string {
return encodeURIComponent(FEFLOW_PLUGIN_LOCAL_PREFIX + dir.trim());
}
function deleteDir(dirPath: string) {
let files: string[] = [];
try {
const dirStats = fs.statSync(dirPath);
if (!dirStats.isDirectory()) {
return;
}
} catch (e) {
return;
}
files = fs.readdirSync(dirPath);
files.forEach((file: string) => {
const curPath = `${dirPath}/${file}`;
const stat = fs.statSync(curPath);
if (stat.isDirectory()) {
deleteDir(curPath);
} else {
fs.unlinkSync(curPath);
}
});
const stat = fs.lstatSync(dirPath);
if (stat.isDirectory()) {
fs.rmdirSync(dirPath);
} else {
fs.rmdirSync(fs.realpathSync(dirPath));
fs.unlinkSync(dirPath);
}
}
function isGitRepo(url: string): boolean {
return (
new RegExp('^git@[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)+.git(@v(0|[1-9]\\d*).(0|[1-9]\\d*).(0|[1-9]\\d*))?$').test(url)
|| new RegExp('^http(s)?://([a-zA-Z0-9._-]*?(:[a-zA-Z0-9._-]*)?@)?[a-zA-Z0-9._-]+'
+ '(/[a-zA-Z0-9._-]+)+.git(@v(0|[1-9]\\d*).(0|[1-9]\\d*).(0|[1-9]\\d*))?$').test(url)
);
}
async function installNpmPlugin(ctx: Feflow, ...dependencies: string[]) {
const packageManager = ctx.config?.packageManager;
if (!packageManager) {
ctx.logger.error('cannot find \'packageManager\' in config file.');
return;
}
const registryUrl = await getRegistryUrl(packageManager);
let versionList: string[];
let needInstall: string[] = [];
try {
versionList = await Promise.all(dependencies.map(async (dependency: string) => {
try {
return await packageJson(dependency, registryUrl);
} catch (e) {
ctx.logger.error(`${dependency} not found on ${packageManager}, please check if it exists`);
ctx.logger.debug(e);
process.exit(2);
}
}));
const getCurversion = () => {
let json: PkgJson = {};
const installedPlugin = {};
try {
const data = fs.readFileSync(ctx.rootPkg, 'utf-8');
json = JSON.parse(data);
} catch (e) {
ctx.logger.error('getCurversion error: ', e);
}
if (!json.dependencies) {
return {};
}
const deps = json.dependencies || json.devDependencies || {};
Object.keys(deps).forEach((name) => {
if (!/^feflow-plugin-|^@[^/]+\/feflow-plugin-|^generator-|^@[^/]+\/generator-/.test(name)) {
return false;
}
installedPlugin[name] = deps[name];
});
return installedPlugin;
};
const hasInstallDep = getCurversion();
needInstall = dependencies.filter((dep, idx) => {
const depList = (dep || '').split('@');
const depName = !depList[0] ? `@${depList[1]}` : depList[0];
if (hasInstallDep[depName] !== versionList[idx]) {
return dep;
}
ctx.logger.info(`[${dep}] has installed the latest version: ${hasInstallDep[depName]}`);
return '';
});
} catch (err) {
ctx.logger.error('get pkg info error: ', err);
}
if (!needInstall.length) {
return Promise.resolve();
}
ctx.logger.info('Installing packages. This might take a couple of minutes.');
return install(packageManager, ctx.root, packageManager === 'yarn' ? 'add' : 'install', needInstall, false).then(() => {
ctx.logger.info('install success');
});
}
function updateNpmPluginInfo(ctx: Feflow, pluginName: string, options: any) {
const {
root,
}: {
root: string;
} = ctx;
const configPath = path.join(root, NPM_PLUGIN_INFO_JSON);
// eslint-disable-next-line @typescript-eslint/no-require-imports
const npmPluginInfoJson = fs.existsSync(configPath) ? require(configPath) : {};
if (options === false) {
delete npmPluginInfoJson[pluginName];
} else {
if (options.globalCmd) {
const pluginInfo = npmPluginInfoJson[pluginName] || {};
const globalCmd = pluginInfo.globalCmd || [];
pluginInfo.globalCmd = globalCmd
? Array.from(new Set<string>([...globalCmd, ...options.globalCmd]))
: options.globalCmd || [];
npmPluginInfoJson[pluginName] = pluginInfo;
// eslint-disable-next-line no-param-reassign
delete options.globalCmd;
}
npmPluginInfoJson[pluginName] = Object.assign({}, npmPluginInfoJson[pluginName] || {}, options || {});
}
fs.writeFileSync(configPath, JSON.stringify(npmPluginInfoJson, null, 4));
}
async function installJsPlugin(ctx: Feflow, installPlugin: string) {
const {
bin,
lib,
logger,
}: {
bin: string;
lib: string;
logger: any;
} = ctx;
const isGlobal = ctx?.args.g;
// install js npm plugin
await installNpmPlugin(ctx, installPlugin);
// if install with option -g, register as global command
if (isGlobal && /^feflow-plugin-|^@[^/]+\/feflow-plugin-/.test(installPlugin)) {
ctx.hook.on(HOOK_TYPE_ON_COMMAND_REGISTERED, (cmdName: string) => {
if (cmdName) {
logger.debug(`linking cmd [${cmdName}] registered by plugin ${installPlugin} to global`);
// create symbol link to plugin, support global plugin cmd
const linker = new Linker();
linker.register(bin, lib, cmdName);
updateNpmPluginInfo(ctx, installPlugin, { globalCmd: [cmdName] });
logger.info(`can just type > "${cmdName} options" in terminal, equal to "fef ${cmdName} options"`);
}
});
return applyPlugins([installPlugin])(ctx);
}
}
async function startInstall(ctx: Feflow, pkgInfo: PkgInfo, repoPath: string, updateFlag: boolean, isGlobal: boolean) {
const {
logger,
universalPkg,
universalModules,
bin,
lib,
}: {
logger: any;
universalPkg: UniversalPkg;
universalModules: string;
bin: string;
lib: string;
} = ctx;
// start install
logger.debug('install version:', pkgInfo.checkoutTag);
if (pkgInfo.fromType !== PkgInfo.dir) {
if (!fs.existsSync(repoPath)) {
logger.info(`start download from ${pkgInfo.repoFrom}`);
try {
await download(pkgInfo.repoFrom, pkgInfo.checkoutTag, repoPath);
} catch (e) {
logger.warn(`download warn with code ${e}`);
}
}
} else {
deleteDir(repoPath);
logger.info(`start copy from ${pkgInfo.repoFrom}`);
await copyDir(pkgInfo.repoFrom, repoPath);
}
let lastRepoName = '';
const lastVersion = universalPkg.getInstalled().get(pkgInfo.repoName);
if (lastVersion) {
const oldRepoPath = getRepoPath(universalModules, pkgInfo.repoName, lastVersion);
lastRepoName = toSimpleCommand(pkgInfo.repoName);
const oldPlugin = resolvePlugin(ctx, oldRepoPath);
if (oldPlugin.name) {
lastRepoName = oldPlugin.name;
}
}
if (pkgInfo.fromType !== PkgInfo.dir) {
logger.info(`switch to version: ${pkgInfo.checkoutTag}`);
await checkoutVersion(repoPath, pkgInfo.checkoutTag, pkgInfo.lastCheckoutTag);
}
// deal dependencies
const linker = new Linker();
const oldVersion = universalPkg.getInstalled().get(pkgInfo.repoName);
let oldDependencies;
if (isGlobal && oldVersion) {
oldDependencies = universalPkg.getDependencies(pkgInfo.repoName, oldVersion);
if (oldDependencies) {
oldDependencies = new Map(oldDependencies);
}
}
const plugin = resolvePlugin(ctx, repoPath);
// check the validity of the plugin before installing it
await plugin.check();
logger.debug('check plugin success');
const pluginBin = path.join(repoPath, `.${FEFLOW_BIN}`);
const pluginLib = path.join(repoPath, `.${FEFLOW_LIB}`);
for (const depPlugin of plugin.dep.plugin) {
try {
const curPkgInfo = await getPkgInfo(ctx, depPlugin);
if (!curPkgInfo) {
throw new Error(`the dependent plugin ${depPlugin} does not belong to the universal packge`);
}
await installPlugin(ctx, depPlugin, false);
const commandName = toSimpleCommand(curPkgInfo.repoName);
if (oldDependencies?.get(curPkgInfo.repoName) === curPkgInfo.installVersion) {
oldDependencies.delete(curPkgInfo.repoName);
}
universalPkg.depend(pkgInfo.repoName, pkgInfo.installVersion, curPkgInfo.repoName, curPkgInfo.installVersion);
const pluginPath = path.join(universalModules, `${curPkgInfo.repoName}@${curPkgInfo.installVersion}`);
const curPlugin = resolvePlugin(ctx, pluginPath);
let useCommandName = commandName;
// custom command name
if (curPlugin.name) {
useCommandName = curPlugin.name;
}
if (curPlugin.langRuntime) {
const commands = curPlugin.command.getCommands();
linker.registerCustom(pluginBin, pluginLib, commands, useCommandName);
} else {
// call {pkg}@{version} and disable-check
linker.register(
pluginBin,
pluginLib,
`${commandName}@${curPkgInfo.installVersion} ${DISABLE_ARG} ${SILENT_ARG}`,
useCommandName,
);
}
logger.info(`install [${curPkgInfo.showName()}] ` + `was successful and it is called using [${useCommandName}]`);
} catch (e) {
logger.error(`failed to install plugin dependency ${depPlugin}`);
throw e;
}
}
if (oldVersion && oldDependencies) {
for (const [oldPkg, oldPkgVersion] of oldDependencies) {
universalPkg.removeDepended(oldPkg, oldPkgVersion, pkgInfo.repoName, oldVersion);
}
}
plugin.preInstall.run();
const cmdName = toSimpleCommand(pkgInfo.repoName);
let useCommandName = cmdName;
// custom command name
if (plugin.name) {
useCommandName = plugin.name;
}
if (plugin.langRuntime) {
const commands = plugin.command.getCommands();
linker.registerCustom(bin, lib, commands, useCommandName);
} else {
linker.register(bin, lib, cmdName, useCommandName);
}
if (lastRepoName && lastRepoName !== useCommandName) {
linker.remove(bin, lib, lastRepoName);
}
// install when global or not exists
if (isGlobal || !universalPkg.isInstalled(pkgInfo.repoName)) {
universalPkg.install(pkgInfo.repoName, pkgInfo.installVersion);
}
// the package management information is retained only when the installation is fully successful
if (isGlobal) {
removeInvalidPkg(ctx);
}
universalPkg.saveChange();
plugin.test.runLess();
if (updateFlag) {
plugin.postUpgrade.runLess();
logger.info(`update [${pkgInfo.showName()}] ` + `was successful and it is called using [${useCommandName}]`);
} else {
plugin.postInstall.runLess();
logger.info(`install [${pkgInfo.showName()}] ` + `was successful and it is called using [${useCommandName}]`);
}
}
function getRepoPath(universalModules: string, repoName: string, installVersion: string): string {
return path.join(universalModules, `${repoName}@${installVersion}`);
}
export async function installPlugin(ctx: Feflow, installPluginStr: string, isGlobal: boolean) {
const {
logger,
universalPkg,
universalModules,
}: {
logger: Logger;
universalPkg: UniversalPkg;
universalModules: string;
} = ctx;
const serverUrl = ctx.config?.serverUrl;
const finalInstallPluginStr = installPluginStr.trim();
if (!serverUrl) {
logger.warn('please configure the serverUrl');
return installJsPlugin(ctx, finalInstallPluginStr);
}
const pkgInfo = await getPkgInfo(ctx, finalInstallPluginStr);
if (!pkgInfo) {
return installJsPlugin(ctx, finalInstallPluginStr);
}
if (!pkgInfo.repoName) {
throw new Error(`plugin [${pkgInfo.repoFrom}] does not exist`);
}
// if the specified version is already installed, skip it
if (universalPkg.isInstalled(pkgInfo.repoName, pkgInfo.checkoutTag, !isGlobal)) {
global && logger.info('the current version is installed');
return;
}
let updateFlag = false;
const repoPath = getRepoPath(universalModules, pkgInfo.repoName, pkgInfo.installVersion);
if (pkgInfo.installVersion === LATEST_VERSION) {
// TODO(blurooochen): 重构已安装插件记录表
const pluginFile = path.join(repoPath, UNIVERSAL_PLUGIN_CONFIG);
if (universalPkg.isInstalled(pkgInfo.repoName, LATEST_VERSION) && fs.existsSync(pluginFile)) {
const currentVersion = await getCurrentTag(repoPath);
if (currentVersion && pkgInfo.checkoutTag === currentVersion) {
if (global) {
logger.info(`[${pkgInfo.repoName}] the plugin version currently installed is the latest version: ${currentVersion}`);
}
return;
}
updateFlag = true;
if (currentVersion) {
pkgInfo.lastCheckoutTag = currentVersion;
}
}
}
if (updateFlag) {
logger.info(`[${pkgInfo.showName()}] update the plugin to version ${pkgInfo.checkoutTag}`);
resolvePlugin(ctx, repoPath).preUpgrade.runLess();
} else {
logger.info(`[${pkgInfo.showName()}] installing plugin`);
}
await startInstall(ctx, pkgInfo, repoPath, updateFlag, isGlobal);
}
function toSimpleCommand(command: string): string {
return command.replace(FEFLOW_PLUGIN_PREFIX, '');
}
function isDir(installPluginDir: string): boolean {
try {
return fs.statSync(installPluginDir).isDirectory();
} catch (e) {
return false;
}
}
// when you install a universal package, return PkgInfo, otherwise return undefined
export async function getPkgInfo(ctx: Feflow, installPlugin: string): Promise<PkgInfo | undefined> {
let installVersion;
let checkoutTag;
let repoFrom;
let repoName;
let fromType: number;
// install from git repo
if (isGitRepo(installPlugin)) {
fromType = PkgInfo.git;
if (installPlugin.indexOf('.git@') !== -1) {
const splits = installPlugin.split('@');
const ver = splits.pop();
repoFrom = splits.join('@');
installVersion = ver || LATEST_VERSION;
} else {
repoFrom = installPlugin;
installVersion = LATEST_VERSION;
}
const confirmTag = installVersion === LATEST_VERSION ? undefined : installVersion;
checkoutTag = await getTag(repoFrom, confirmTag);
repoName = getGitRepoName(repoFrom);
} else if (isDir(installPlugin)) {
fromType = PkgInfo.dir;
const plugin = resolvePlugin(ctx, installPlugin);
if (!plugin.name) {
throw new Error('the [name] field must be specified in plugin.yml');
}
installVersion = LATEST_VERSION;
checkoutTag = INVALID_VERSION;
repoFrom = installPlugin;
repoName = getDirRepoName(installPlugin);
} else {
fromType = PkgInfo.appStore;
let pluginName: string;
let pluginVersion: string;
if (installPlugin.startsWith('@')) {
[, pluginName, pluginVersion] = installPlugin.split('@');
pluginName = `@${pluginName}`;
} else {
[pluginName, pluginVersion] = installPlugin.split('@');
}
const repoInfo = await getRepoInfo(ctx, pluginName);
if (!repoInfo) {
ctx.logger.warn(`cant found message about ${pluginName} from Feflow Application market, please check if it exists`);
return;
}
repoFrom = repoInfo.repo;
repoName = repoInfo.name;
if (isGitRepo(repoFrom) && !repoInfo.tnpm) {
if (pluginVersion) {
pluginVersion = versionImpl.toFull(pluginVersion);
if (!versionImpl.check(pluginVersion)) {
throw new Error(`invalid version: ${pluginVersion} for ${pluginName}`);
}
}
installVersion = pluginVersion || LATEST_VERSION;
checkoutTag = await getTag(repoFrom, installVersion === LATEST_VERSION ? undefined : installVersion);
} else {
return;
}
}
if (!checkoutTag) {
throw new Error(`the version [${installVersion}] was not found`);
}
return new PkgInfo(repoName, repoFrom, installVersion, checkoutTag, fromType);
}
export class PkgInfo {
public static git = 1;
public static appStore = 2;
public static dir = 3;
repoName: string;
repoFrom: string;
installVersion: string;
checkoutTag: string;
lastCheckoutTag = '';
fromType: number;
constructor(repoName: string, repoUrl: string, installVersion: string, checkoutTag: string, fromType: number) {
this.repoName = repoName;
this.repoFrom = repoUrl;
this.installVersion = installVersion;
this.checkoutTag = checkoutTag;
this.fromType = fromType;
}
public showName(): string {
if (this.fromType !== PkgInfo.appStore) {
return this.repoFrom;
}
return this.repoName;
}
}
async function uninstallUniversalPlugin(ctx: Feflow, pluginName: string) {
const {
logger,
universalPkg,
}: {
logger: any;
universalPkg: UniversalPkg;
bin: string;
lib: string;
} = ctx;
const version = universalPkg.getInstalled().get(pluginName);
if (!version) {
logger.error('this plugin is not currently installed');
return;
}
let plugin: Plugin | undefined;
try {
const repoPath = path.join(ctx.universalModules, `${pluginName}@${version}`);
try {
plugin = resolvePlugin(ctx, repoPath);
} catch (e) {
logger.debug(`resolve plugin failure, ${e}`);
}
plugin?.preUninstall?.run();
universalPkg.uninstall(pluginName, version);
} catch (e) {
logger.error(`uninstall failure, ${e}`);
return;
}
try {
removeInvalidPkg(ctx);
plugin?.postUninstall?.runLess();
logger.info('uninstall success');
} catch (e) {
logger.info(`uninstall succeeded, but failed to clean the data, ${e}`);
}
}
async function uninstallNpmPlugin(ctx: Feflow, dependencies: string[]) {
const { logger, root, bin, lib } = ctx;
dependencies.forEach((pkg: string) => {
const npmPluginInfoPath = path.join(root, NPM_PLUGIN_INFO_JSON);
try {
if (fs.existsSync(npmPluginInfoPath)) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const npmPluginInfo = require(npmPluginInfoPath);
const pluginGlobalCmd = npmPluginInfo?.[pkg]?.globalCmd || [];
pluginGlobalCmd.forEach((cmd: string) => {
new Linker().remove(bin, lib, cmd);
});
updateNpmPluginInfo(ctx, pkg, false);
}
} catch (e) {
logger.debug(`remove plugin registered cmd link failure, ${e}`);
}
});
const { packageManager } = ctx.config || {};
if (!packageManager) {
logger.error('cannot find \'packageManager\' in config file');
return;
}
return install(
packageManager,
ctx.root,
packageManager === 'yarn' ? 'remove' : 'uninstall',
dependencies,
false,
).then(() => {
logger.info('uninstall success');
});
}
function removePkg(ctx: Feflow, pkg: string, version: string) {
const { bin, lib, universalPkg }: { universalPkg: UniversalPkg; bin: string; lib: string } = ctx;
const pluginPath = path.join(ctx.universalModules, `${pkg}@${version}`);
if (fs.existsSync(pluginPath)) {
let useName = toSimpleCommand(pkg);
const curPlugin = resolvePlugin(ctx, pluginPath);
if (curPlugin.name) {
useName = curPlugin.name;
}
deleteDir(pluginPath);
if (!universalPkg.isInstalled(pkg)) {
try {
new Linker().remove(bin, lib, useName);
} catch (e) {
ctx.logger.debug(`remove link failure, ${e}`);
}
}
}
}
function removeInvalidPkg(ctx: Feflow) {
const { universalPkg }: { universalPkg: UniversalPkg } = ctx;
const invalidDep = universalPkg.removeInvalidDependencies();
for (const [invalidPkg, invalidVersion] of invalidDep) {
removePkg(ctx, invalidPkg, invalidVersion);
}
}
// update only the plugins installed globally
export async function updateUniversalPlugin(ctx: Feflow, pkg: string, version: string, plugin: Plugin) {
const universalPkg = ctx.universalPkg as UniversalPkg;
const dependedOn = universalPkg.getDepended(pkg, version);
// update parent
if (dependedOn) {
for (const [dependedOnPkg, dependedOnVersion] of dependedOn) {
if (dependedOnVersion !== LATEST_VERSION) {
continue;
}
await updatePlugin(ctx, dependedOnPkg, dependedOnVersion);
}
}
const newVersion = universalPkg.getInstalled().get(pkg);
if (newVersion === version && version === LATEST_VERSION && plugin.autoUpdate) {
await updatePlugin(ctx, pkg, version);
}
}
async function updatePlugin(ctx: Feflow, pkg: string, version: string) {
const { universalPkg }: { universalPkg: UniversalPkg } = ctx;
const isGlobal = universalPkg.isInstalled(pkg, version);
try {
await installPlugin(ctx, `${pkg}@${version}`, isGlobal);
} catch (e) {
ctx.logger.error(`[${pkg}] update failure: `, e);
}
}