diff --git a/src/commands/plugins/install.ts b/src/commands/plugins/install.ts index f8735339..da0d7202 100644 --- a/src/commands/plugins/install.ts +++ b/src/commands/plugins/install.ts @@ -1,4 +1,4 @@ -import {Command} from '@oclif/command' +import {Command, flags} from '@oclif/command' import chalk from 'chalk' import cli from 'cli-ux' @@ -10,12 +10,17 @@ export default class PluginsInstall extends Command { static examples = ['$ <%= config.bin %> plugins:install <%- config.pjson.oclif.examplePlugin || "myplugin" %> '] static strict = false static args = [{name: 'plugin', description: 'plugin to install', required: true}] + static flags = { + help: flags.help({char: 'h'}), + verbose: flags.boolean({char: 'v'}), + } static aliases = ['plugins:add'] plugins = new Plugins(this.config) async run() { - const {argv} = this.parse(PluginsInstall) + const {flags, argv} = this.parse(PluginsInstall) + if (flags.verbose) this.plugins.verbose = true for (let plugin of argv) { let {name, tag} = parsePlugin(plugin) cli.action.start(`Installing plugin ${chalk.cyan(this.plugins.friendlyName(name))}`) diff --git a/src/commands/plugins/link.ts b/src/commands/plugins/link.ts index f5573d5b..079f89dd 100644 --- a/src/commands/plugins/link.ts +++ b/src/commands/plugins/link.ts @@ -1,4 +1,4 @@ -import {Command} from '@oclif/command' +import {Command, flags} from '@oclif/command' import chalk from 'chalk' import cli from 'cli-ux' @@ -9,11 +9,16 @@ export default class PluginsLink extends Command { static usage = 'plugins:link PLUGIN' static examples = ['$ <%= config.bin %> plugins:link <%- config.pjson.oclif.examplePlugin || "myplugin" %> '] static args = [{name: 'path', description: 'path to plugin', required: true, default: '.'}] + static flags = { + help: flags.help({char: 'h'}), + verbose: flags.boolean({char: 'v'}), + } plugins = new Plugins(this.config) async run() { - const {args} = this.parse(PluginsLink) + const {flags, args} = this.parse(PluginsLink) + this.plugins.verbose = flags.verbose cli.action.start(`Linking plugin ${chalk.cyan(args.path)}`) await this.plugins.link(args.path) cli.action.stop() diff --git a/src/commands/plugins/uninstall.ts b/src/commands/plugins/uninstall.ts index f2ccdc0b..4b3205c5 100644 --- a/src/commands/plugins/uninstall.ts +++ b/src/commands/plugins/uninstall.ts @@ -1,4 +1,4 @@ -import {Command} from '@oclif/command' +import {Command, flags} from '@oclif/command' import cli from 'cli-ux' import Plugins from '../../plugins' @@ -12,13 +12,18 @@ export default class PluginsUninstall extends Command { ` static variableArgs = true static args = [{name: 'plugin', description: 'plugin to uninstall'}] + static flags = { + help: flags.help({char: 'h'}), + verbose: flags.boolean({char: 'v'}), + } static aliases = ['plugins:unlink', 'plugins:remove'] plugins = new Plugins(this.config) async run() { - const {argv} = this.parse(PluginsUninstall) + const {flags, argv} = this.parse(PluginsUninstall) this.plugins = new Plugins(this.config) + if (flags.verbose) this.plugins.verbose = true if (!argv.length) argv.push('.') for (let plugin of argv) { const friendly = this.plugins.friendlyName(plugin) diff --git a/src/commands/plugins/update.ts b/src/commands/plugins/update.ts index 7fce849d..46dda1f2 100644 --- a/src/commands/plugins/update.ts +++ b/src/commands/plugins/update.ts @@ -1,4 +1,4 @@ -import {Command} from '@oclif/command' +import {Command, flags} from '@oclif/command' import Plugins from '../../plugins' @@ -6,11 +6,16 @@ export default class PluginsUpdate extends Command { static topic = 'plugins' static command = 'update' static description = 'update installed plugins' + static flags = { + help: flags.help({char: 'h'}), + verbose: flags.boolean({char: 'v'}), + } plugins = new Plugins(this.config) async run() { - this.parse(PluginsUpdate) + const {flags} = this.parse(PluginsUpdate) + this.plugins.verbose = flags.verbose await this.plugins.update() } } diff --git a/src/plugins.ts b/src/plugins.ts index 36cf562e..a6705927 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -14,11 +14,12 @@ import Yarn from './yarn' const initPJSON: Config.PJSON.User = {private: true, oclif: {schema: 1, plugins: []}, dependencies: {}} export default class Plugins { + verbose = false readonly yarn: Yarn private readonly debug: any constructor(public config: Config.IConfig) { - this.yarn = new Yarn({config, cwd: this.config.dataDir}) + this.yarn = new Yarn({config}) this.debug = require('debug')('@oclif/plugins') } @@ -54,11 +55,12 @@ export default class Plugins { name = unfriendly } await this.createPJSON() - await this.yarn.exec(['add', `${name}@${tag}`]) + await this.yarn.exec(['add', `${name}@${tag}`], {cwd: this.config.dataDir, verbose: this.verbose}) const plugin = await Config.load({devPlugins: false, userPlugins: false, root: path.join(this.config.dataDir, 'node_modules', name), name}) if (!plugin.valid && !this.config.plugins.find(p => p.name === '@oclif/plugin-legacy')) { throw new Error('plugin is invalid') } + await this.refresh(plugin.root) await this.add({name, tag: range || tag, type: 'user'}) } catch (err) { await this.uninstall(name).catch(err => this.debug(err)) @@ -66,12 +68,21 @@ export default class Plugins { } } + // if yarn.lock exists, fetch locked dependencies + async refresh(root: string, {prod = true}: {prod?: boolean} = {}) { + if (fs.existsSync(path.join(root, 'yarn.lock'))) { + // use yarn.lock to fetch dependencies + await this.yarn.exec(prod ? ['--prod'] : [], {cwd: root, verbose: this.verbose}) + } + } + async link(p: string) { const c = await Config.load(path.resolve(p)) cli.action.start(`${this.config.name}: linking plugin ${c.name}`) if (!c.valid && !this.config.plugins.find(p => p.name === '@oclif/plugin-legacy')) { throw new CLIError('plugin is not a valid oclif plugin') } + await this.refresh(c.root, {prod: false}) await this.add({type: 'link', name: c.name, root: c.root}) } @@ -93,7 +104,7 @@ export default class Plugins { try { const pjson = await this.pjson() if ((pjson.oclif.plugins || []).find(p => typeof p === 'object' && p.type === 'user' && p.name === name)) { - await this.yarn.exec(['remove', name]) + await this.yarn.exec(['remove', name], {cwd: this.config.dataDir, verbose: this.verbose}) } } finally { await this.remove(name) @@ -104,7 +115,10 @@ export default class Plugins { const plugins = (await this.list()).filter((p): p is Config.PJSON.PluginTypes.User => p.type === 'user') if (plugins.length === 0) return cli.action.start(`${this.config.name}: Updating plugins`) - await this.yarn.exec(['add', ...plugins.map(p => `${p.name}@${p.tag}`)]) + await this.yarn.exec(['add', ...plugins.map(p => `${p.name}@${p.tag}`)], {cwd: this.config.dataDir, verbose: this.verbose}) + for (let p of plugins) { + await this.refresh(path.join(this.config.dataDir, 'node_modules', p.name)) + } cli.action.stop() } diff --git a/src/yarn.ts b/src/yarn.ts index c90f0b47..9256ec5f 100644 --- a/src/yarn.ts +++ b/src/yarn.ts @@ -1,15 +1,14 @@ import {IConfig} from '@oclif/config' +import ux from 'cli-ux' import * as path from 'path' const debug = require('debug')('cli:yarn') export default class Yarn { config: IConfig - cwd: string - constructor({config, cwd}: { config: IConfig; cwd: string }) { + constructor({config}: { config: IConfig }) { this.config = config - this.cwd = cwd } get bin(): string { @@ -20,8 +19,12 @@ export default class Yarn { return new Promise((resolve, reject) => { const {fork} = require('child_process') let forked = fork(modulePath, args, options) - forked.stdout.on('data', (d: any) => process.stdout.write(d)) forked.stderr.on('data', (d: any) => process.stderr.write(d)) + forked.stdout.setEncoding('utf8') + forked.stdout.on('data', (d: any) => { + if (options.verbose) process.stdout.write(d) + else ux.action.status = d.replace(/\n$/, '').split('\n').pop() + }) forked.on('error', reject) forked.on('exit', (code: number) => { @@ -39,13 +42,14 @@ export default class Yarn { }) } - async exec(args: string[] = []): Promise { + async exec(args: string[] = [], opts: {cwd: string, verbose: boolean}): Promise { + const cwd = opts.cwd if (args[0] !== 'run') { const cacheDir = path.join(this.config.cacheDir, 'yarn') args = [ ...args, '--non-interactive', - `--mutex=file:${path.join(this.cwd, 'yarn.lock')}`, + `--mutex=file:${path.join(cwd, 'yarn.lock')}`, `--preferred-cache-folder=${cacheDir}`, '--check-files', ] @@ -56,12 +60,16 @@ export default class Yarn { const npmRunPath = require('npm-run-path') let options = { - cwd: this.cwd, + ...opts, + cwd, stdio: [0, null, null, 'ipc'], - env: npmRunPath.env({cwd: this.cwd, env: process.env}), + env: npmRunPath.env({cwd, env: process.env}), } - debug(`${this.cwd}: ${this.bin} ${args.join(' ')}`) + if (opts.verbose) { + process.stderr.write(`${cwd}: ${this.bin} ${args.join(' ')}`) + } + debug(`${cwd}: ${this.bin} ${args.join(' ')}`) try { await this.fork(this.bin, args, options) debug('done') @@ -70,7 +78,7 @@ export default class Yarn { let networkConcurrency = '--network-concurrency=1' if (err.message.includes('EAI_AGAIN') && !args.includes(networkConcurrency)) { debug('EAI_AGAIN') - return this.exec([...args, networkConcurrency]) + return this.exec([...args, networkConcurrency], opts) } throw err }