-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
395 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import Command, {flags} from '@dxcli/command' | ||
import color from '@heroku-cli/color' | ||
import cli from 'cli-ux' | ||
import * as _ from 'lodash' | ||
|
||
let examplePlugins = { | ||
'heroku-ci': {version: '1.8.0'}, | ||
'heroku-cli-status': {version: '3.0.10', type: 'link'}, | ||
'heroku-fork': {version: '4.1.22'}, | ||
} | ||
let bin = 'heroku' | ||
const g = global as any | ||
if (g.config) { | ||
bin = g.config.bin | ||
let pjson = g.config.pjson['cli-engine'] | ||
if (pjson.help && pjson.help.plugins) { | ||
examplePlugins = pjson.help.plugins | ||
} | ||
} | ||
const examplePluginsHelp = Object.entries(examplePlugins).map(([name, p]: [string, any]) => ` ${name} ${p.version}`) | ||
|
||
export default class Plugins extends Command { | ||
static flags: flags.Input = { | ||
core: flags.boolean({description: 'show core plugins'}) | ||
} | ||
static description = 'list installed plugins' | ||
static help = `Example: | ||
$ ${bin} plugins | ||
${examplePluginsHelp.join('\n')} | ||
` | ||
|
||
async run() { | ||
let plugins = this.config.engine!.plugins | ||
plugins = plugins.filter(p => p.type !== 'builtin' && p.type !== 'main') | ||
_.sortBy(plugins, 'name') | ||
if (!this.flags.core) plugins = plugins.filter(p => p.type !== 'core') | ||
if (!plugins.length) cli.warn('no plugins installed') | ||
for (let plugin of plugins) { | ||
let output = `${plugin.name} ${color.dim(plugin.version)}` | ||
if (plugin.type !== 'user') output += color.dim(` (${plugin.type})`) | ||
if (plugin.type === 'link') output += ` ${plugin.root}` | ||
else if (plugin.tag !== 'latest') output += color.dim(` (${String(plugin.tag)})`) | ||
cli.log(output) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {Command} from '@dxcli/command' | ||
|
||
import Plugins from '../../plugins' | ||
|
||
let examplePlugin = 'heroku-production-status' | ||
let bin = 'heroku' | ||
const g = global as any | ||
if (g.config) { | ||
bin = g.config.bin | ||
let pjson = g.config.pjson.dxcli | ||
if (pjson.help && pjson.help.plugins) { | ||
examplePlugin = Object.keys(pjson.help.plugins)[0] | ||
} | ||
} | ||
|
||
export default class PluginsInstall extends Command { | ||
static description = 'installs a plugin into the CLI' | ||
static usage = 'plugins:install PLUGIN...' | ||
static help = ` | ||
Example: | ||
$ ${bin} plugins:install ${examplePlugin} | ||
` | ||
static variableArgs = true | ||
static args = [{name: 'plugin', description: 'plugin to install', required: true}] | ||
|
||
plugins: Plugins | ||
|
||
async run() { | ||
this.plugins = new Plugins(this.config) | ||
for (let plugin of this.argv) { | ||
let scoped = plugin[0] === '@' | ||
if (scoped) plugin = plugin.slice(1) | ||
let [name, tag = 'latest'] = plugin.split('@') | ||
if (scoped) name = `@${name}` | ||
await this.plugins.install(name, tag) | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import {IConfig, IPlugin} from '@dxcli/config' | ||
|
||
import Plugins from './plugins' | ||
|
||
export default async function (config: IConfig) { | ||
try { | ||
const plugins = new Plugins(config) | ||
return await plugins.load() | ||
} catch (err) { | ||
const cli = require('cli-ux').scope('loading plugins') | ||
cli.warn(err) | ||
} | ||
} | ||
|
||
export {IPlugin} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import ManifestFile from '@dxcli/manifest-file' | ||
|
||
export interface File { | ||
manifest: { | ||
plugins: { | ||
[name: string]: { | ||
tag: string | ||
} | ||
} | ||
} | ||
} | ||
|
||
export default class Manifest extends ManifestFile { | ||
constructor(file: string) { | ||
super(['@dxcli/plugins', file].join(':'), file) | ||
} | ||
|
||
async list(): Promise<File['manifest']['plugins']> { | ||
return (await this.get('plugins')) || {} as any | ||
} | ||
|
||
async add(name: string, tag: string) { | ||
this.debug(`adding ${name}@${tag}`) | ||
const plugins = await this.list() | ||
plugins[name] = {tag} | ||
await this.set(['plugins', plugins]) | ||
} | ||
|
||
async remove(name: string) { | ||
this.debug(`removing ${name}`) | ||
const plugins = await this.list() | ||
if (!plugins[name]) return this.debug('not found in manifest') | ||
delete plugins[name] | ||
await this.set(['plugins', plugins]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import {IConfig, IPlugin} from '@dxcli/config' | ||
import {load} from '@dxcli/loader' | ||
import {cli} from 'cli-ux' | ||
import * as fs from 'fs-extra' | ||
import * as _ from 'lodash' | ||
import * as path from 'path' | ||
|
||
import Manifest from './manifest' | ||
import Yarn from './yarn' | ||
|
||
export default class Plugins { | ||
private manifest: Manifest | ||
private yarn: Yarn | ||
private debug: any | ||
|
||
constructor(public config: IConfig) { | ||
this.manifest = new Manifest(path.join(this.config.dataDir, 'plugins', 'user.json')) | ||
this.yarn = new Yarn({config, cwd: this.userPluginsDir}) | ||
this.debug = require('debug')('@dxcli/plugins') | ||
} | ||
|
||
async list() { | ||
const plugins = await this.manifest.list() | ||
return Object.entries(plugins) | ||
} | ||
|
||
async install(name: string, tag = 'latest') { | ||
try { | ||
cli.info(`Installing plugin ${name}${tag === 'latest' ? '' : '@' + tag}`) | ||
await this.createPJSON() | ||
await this.yarn.exec(['add', `${name}@${tag}`]) | ||
let plugin = await this.loadPlugin(name) | ||
if (!plugin.commands.length) throw new Error('no commands found in plugin') | ||
await this.manifest.add(name, tag) | ||
} catch (err) { | ||
await this.uninstall(name).catch(err => this.debug(err)) | ||
throw err | ||
} | ||
} | ||
|
||
async load(): Promise<IPlugin[]> { | ||
const plugins = await this.list() | ||
return _.compact(await Promise.all(plugins.map(async ([p]) => { | ||
try { | ||
return await this.loadPlugin(p) | ||
} catch (err) { | ||
cli.warn(err) | ||
} | ||
}))) | ||
} | ||
|
||
public async uninstall(name: string) { | ||
const plugins = await this.manifest.list() | ||
if (!plugins[name]) return | ||
await this.manifest.remove(name) | ||
await this.yarn.exec(['remove', name]) | ||
} | ||
|
||
private async loadPlugin(name: string) { | ||
return load({root: this.userPluginPath(name), type: 'user', resetCache: true}) | ||
} | ||
|
||
private async createPJSON() { | ||
if (!await fs.pathExists(this.pjsonPath)) { | ||
await fs.outputJSON(this.pjsonPath, {private: true, 'cli-engine': {schema: 1}}, {spaces: 2}) | ||
} | ||
} | ||
|
||
private userPluginPath(name: string): string { | ||
return path.join(this.userPluginsDir, 'node_modules', name) | ||
} | ||
private get userPluginsDir() { | ||
return path.join(this.config.dataDir, 'plugins') | ||
} | ||
private get pjsonPath() { | ||
return path.join(this.userPluginsDir, 'package.json') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import {IConfig} from '@dxcli/config' | ||
import * as fs from 'fs-extra' | ||
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 }) { | ||
this.config = config | ||
this.cwd = cwd | ||
} | ||
|
||
get bin(): string { | ||
return require.resolve('yarn/bin/yarn.js') | ||
} | ||
|
||
fork(modulePath: string, args: string[] = [], options: any = {}): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
const {fork} = require('child_process') | ||
let forked = fork(modulePath, args, options) | ||
|
||
forked.on('error', reject) | ||
forked.on('exit', (code: number) => { | ||
if (code === 0) { | ||
resolve() | ||
} else { | ||
reject(new Error(`yarn ${args.join(' ')} exited with code ${code}`)) | ||
} | ||
}) | ||
|
||
// Fix windows bug with node-gyp hanging for input forever | ||
if (this.config.windows) { | ||
forked.stdin.write('\n') | ||
} | ||
}) | ||
} | ||
|
||
async exec(args: string[] = []): Promise<void> { | ||
if (args.length !== 0) await this.checkForYarnLock() | ||
if (args[0] !== 'run') { | ||
const cacheDir = path.join(this.config.cacheDir, 'yarn') | ||
args = [ | ||
...args, | ||
'--non-interactive', | ||
`--mutex=file:${path.join(this.cwd, 'yarn.lock')}`, | ||
`--preferred-cache-folder=${cacheDir}`, | ||
...this.proxyArgs(), | ||
] | ||
if (this.config.npmRegistry) { | ||
args.push(`--registry=${this.config.npmRegistry}`) | ||
} | ||
} | ||
|
||
const npmRunPath = require('npm-run-path') | ||
let options = { | ||
cwd: this.cwd, | ||
stdio: [0, 1, 2, 'ipc'], | ||
env: npmRunPath.env({cwd: this.cwd, env: process.env}), | ||
} | ||
|
||
debug(`${this.cwd}: ${this.bin} ${args.join(' ')}`) | ||
try { | ||
await this.fork(this.bin, args, options) | ||
debug('done') | ||
} catch (err) { | ||
// TODO: https://github.com/yarnpkg/yarn/issues/2191 | ||
let networkConcurrency = '--network-concurrency=1' | ||
if (err.message.includes('EAI_AGAIN') && !args.includes(networkConcurrency)) { | ||
debug('EAI_AGAIN') | ||
return this.exec([...args, networkConcurrency]) | ||
} | ||
throw err | ||
} | ||
} | ||
|
||
async checkForYarnLock() { | ||
// add yarn lockfile if it does not exist | ||
if (this.cwd && !await fs.pathExists(path.join(this.cwd, 'yarn.lock'))) { | ||
await this.exec() | ||
} | ||
} | ||
|
||
proxyArgs(): string[] { | ||
let args = [] | ||
let http = process.env.http_proxy || process.env.HTTP_PROXY | ||
let https = process.env.https_proxy || process.env.HTTPS_PROXY | ||
if (http) args.push(`--proxy=${http}`) | ||
if (https) args.push(`--https-proxy=${https}`) | ||
return args | ||
} | ||
} |
Oops, something went wrong.