serverless-spy
Version:
CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.
310 lines (264 loc) • 9.44 kB
JavaScript
const debug = require('./debug')('tabtab:installer')
import fs from 'fs';
import path from 'path';
import inquirer from 'inquirer';
import { spawn, exec } from 'child_process';
import mkdirp from 'mkdirp';
let errmsg = `
Error: You don't have permission to write to :destination.
Try running with sudo instead:
sudo ${process.argv.join(' ')}
`;
// Public: Manage installation / setup of completion scripts.
//
// pkg-config --variable=completionsdir bash-completion
// pkg-config --variable=compatdir bash-completion
export default class Installer {
get home() {
return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
}
constructor(options, complete) {
this.options = options || {};
this.complete = complete;
this._shell = (process.env.SHELL || '').split('/').slice(-1)[0];
this.dest = this._shell === 'zsh' ? 'zshrc' :
this._shell === 'bash' ? 'bashrc' :
'fish';
this.dest = this.dest.replace('~', process.env.HOME);
}
// Called on install command.
//
// Performs the installation process.
handle(name, options) {
this.options.name = name;
if (options.stdout) return new Promise((r, errback) => {
return this.writeTo({
destination: 'stdout'
});
});
if (options.auto) {
this.template = this._shell;
return this.writeTo({ destination: this.dest });
}
return this.prompt().then(this.writeTo.bind(this));
}
writeTo(data) {
var destination = data.destination;
debug('Installing completion script to %s directory', destination);
var script = this.complete.script(this.options.name, this.options.completer || this.options.name, this.template);
if (destination === 'stdout') return process.stdout.write('\n\n' + script + '\n');
if (destination === 'bashrc') destination = path.join(this.home, '.bashrc');
else if (destination === 'zshrc') destination = path.join(this.home, '.zshrc');
else if (destination === 'fish') destination = path.join(this.home, '.config/fish/config.fish');
else if (destination === 'fishdir') destination = path.join(this.home, '.config/fish/completions', this.options.name + '.fish');
else destination = path.join(destination, this.options.name);
return new Promise(this.createStream.bind(this, destination))
.then(this.installCompletion.bind(this, destination));
}
createStream(destination, r, errback) {
// debug('Check %s destination', destination);
var flags = 'a';
fs.stat(destination, (err, stat) => {
if (err && err.code === 'ENOENT') flags = 'w';
else if (err) return errback(err);
mkdirp(path.dirname(destination), (err) => {
if (err) return errback(err);
var out = fs.createWriteStream(destination, { flags });
out.on('error', (err) => {
if (err.code === 'EACCES') {
console.error(errmsg.replace(':destination', destination));
}
return errback(err);
});
out.on('open', () => {
debug('Installing completion script to %s directory', destination);
debug('Writing to %s file in %s mode', destination, flags === 'a' ? 'append' : 'write');
r(out);
});
});
});
}
installCompletion(destination, out) {
var name = this.options.name;
var script = this.complete.script(name, this.options.completer || name, this.template);
var filename = path.join(__dirname, '../.completions', name + '.' + this.template);
if(process.platform === 'win32') { filename = filename.replace(/\\/g, '/'); } // win32: replace backslashes with forward slashes
debug('Writing actual completion script to %s', filename);
// First write internal completion script in a local .comletions directory
// in this module. This gets sourced in user scripts after, to avoid
// cluttering bash/zshrc files with too much boilerplate.
return new Promise((r, errback) => {
fs.writeFile(filename, script, (err) => {
if (err) return errback(err);
var regex = new RegExp(`tabtab source for ${name}`);
fs.readFile(destination, 'utf8', (err, content) => {
if (err) return errback(err);
if (regex.test(content)) {
return debug('Already installed %s in %s', name, destination);
}
console.error('\n[tabtab] Adding source line to load %s\nin %s\n', filename, destination);
debug('. %s > %s', filename, destination);
out.write('\n# tabtab source for ' + name + ' package');
out.write('\n# uninstall by removing these lines or running ');
out.write('`tabtab uninstall ' + name + '`');
if (this.template === 'fish') {
out.write('\n[ -f ' + filename + ' ]; and . ' + filename);
} else if (this.template === 'zsh') {
out.write('\n[[ -f ' + filename + ' ]] && . ' + filename);
} else {
out.write('\n[ -f ' + filename + ' ] && . ' + filename);
}
});
});
});
}
uninstallCompletion(destination) {
return new Promise((r, errback) => {
fs.readFile(destination, 'utf8', (err, body) => {
if (err) return errback(err);
r(body);
});
})
.then((body) => {
var lines = body.split(/\r?\n/);
debug('Uninstall', this.options);
var name = this.options.name;
var reg = new RegExp('(tabtab source for ' + name + ' package|`tabtab uninstall ' + name + '`|tabtab/.completions/' + name + ')');
lines = lines.filter((line) => {
return !reg.test(line);
});
return lines.join('\n');
})
.then((content) => {
return new Promise((r, errback) => {
fs.writeFile(destination, content, (err) => {
if (err) return errback(err);
debug('%s sucesfully updated to remove tabtab', destination);
});
});
});
}
// Prompts user for installation location.
prompt() {
var choices = [{
name: 'Nowhere. Just output to STDOUT',
value: 'stdout',
short: 'stdout'
}];
var prompts = [{
message: 'Where do you want to setup the completion script',
name: 'destination',
type: 'list',
choices: choices
}];
return this.shell()
.then((entries) => {
prompts[0].choices = choices.concat(entries);
return inquirer.prompt(prompts);
});
}
// Shell adapters.
//
// Supported:
//
// - bash - Asks pkg-config for completion directories
// - zsh - Lookup $fpath environment variable
// - fish - Lookup for $fish_complete_path
shell() {
return new Promise((r, errback) => {
var shell = process.env.SHELL;
if (shell) shell = shell.split((process.platform !== 'win32') ? '/' : '\\').slice(-1)[0];
return this[shell]().then(r)
.catch(errback);
});
}
fish() {
debug('Fish shell detected');
this.template = 'fish';
return new Promise((r, errback) => {
var dir = path.join(this.home, '.config/fish/completions');
return r([{
name: 'Fish config file (~/.config/fish/config.fish)',
value: 'fish',
short: 'fish'
}, {
name: 'Fish completion directory (' + dir + ')',
value: 'fishdir',
short: 'fish'
}]);
});
}
bash() {
debug('Bash shell detected');
this.template = 'bash';
var entries = [{
name: 'Bash config file (~/.bashrc)',
value: 'bashrc',
short: 'bash'
}];
return this.completionsdir()
.then((dir) => {
debug(dir);
if (dir) {
entries.push({
name: 'Bash completionsdir ( ' + dir + ' )',
value: dir
});
}
return this.compatdir();
})
.then((dir) => {
if (dir) {
entries.push({
name: 'Bash compatdir ( ' + dir + ' )',
value: dir
});
}
return entries;
});
}
zsh() {
debug('Zsh shell detected');
this.template = 'zsh';
return new Promise((r, errback) => {
var dir = '/usr/local/share/zsh/site-functions';
return r([{
name: 'Zsh config file (~/.zshrc)',
value: 'zshrc',
short: 'zsh'
}, {
name: 'Zsh completion directory (' + dir + ')',
value: dir
}]);
});
}
// Bash
// Public: pkg-config wrapper
pkgconfig(variable) {
return new Promise((r, errback) => {
var cmd = `pkg-config --variable=${variable} bash-completion`;
debug('cmd', cmd);
exec(cmd, function(err, stdout, stderr) {
if (err) {
// silently fail if pkg-config bash-completion returns an error,
// meaning bash-completion is either not installed or misconfigured
// with PKG_CONFIG_PATH
return r();
}
stdout = stdout.trim();
debug('Got %s for %s', stdout, variable);
r(stdout);
});
});
}
// Returns the pkg-config variable for "completionsdir" and bash-completion
completionsdir() {
debug('Asking pkg-config for completionsdir');
return this.pkgconfig('completionsdir');
}
// Returns the pkg-config variable for "compatdir" and bash-completion
compatdir() {
debug('Asking pkg-config for compatdir');
return this.pkgconfig('compatdir');
}
}