Skip to content

Commit

Permalink
feat: add necessary completion lines in shell config
Browse files Browse the repository at this point in the history
  • Loading branch information
mklabs committed Jul 21, 2018
1 parent 5def497 commit 2030676
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 28 deletions.
9 changes: 9 additions & 0 deletions lib/constants.js
@@ -0,0 +1,9 @@
const BASH_LOCATION = '~/.bashrc';
const FISH_LOCATION = '~/.config/fish/config.fish';
const ZSH_LOCATION = '~/.zshrc';

module.exports = {
BASH_LOCATION,
ZSH_LOCATION,
FISH_LOCATION
};
14 changes: 11 additions & 3 deletions lib/index.js
@@ -1,6 +1,7 @@
const debug = require('debug')('tabtab');
const inquirer = require('inquirer');
const prompt = require('./prompt');
const installer = require('./installer');

const tabtab = () => 'tabtab';

Expand All @@ -14,10 +15,17 @@ const tabtab = () => 'tabtab';
*
*/
tabtab.install = (options = { name: '', completer: '' }) => {
if (!options.name) throw new TypeError('options.name is required');
if (!options.completer) throw new TypeError('options.completer is required');
const { name, completer } = options;
if (!name) throw new TypeError('options.name is required');
if (!completer) throw new TypeError('options.completer is required');

return prompt();
return prompt().then(({ location, shell }) =>
installer.install({
name,
completer,
location
})
);
};

module.exports = tabtab;
112 changes: 112 additions & 0 deletions lib/installer.js
@@ -0,0 +1,112 @@
const debug = require('debug')('tabtab:installer');
const fs = require('fs');
const path = require('path');
const untildify = require('untildify');
const { promisify } = require('es6-promisify');
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const { BASH_LOCATION, FISH_LOCATION, ZSH_LOCATION } = require('./constants');

const shellExtension = location => {
if (location === BASH_LOCATION) return 'bash';
if (location === FISH_LOCATION) return 'fish';
if (location === ZSH_LOCATION) return 'zsh';
};

const scriptFromLocation = location => {
if (location === BASH_LOCATION) {
return path.join(__dirname, '../scripts/bash.sh');
}

if (location === FISH_LOCATION) {
return path.join(__dirname, '../scripts/fish.sh');
}

if (location === ZSH_LOCATION) {
return path.join(__dirname, '../scripts/zsh.sh');
}
};

const writeToShellConfig = ({ name, completer, location }) => {
debug(`Adding tabtab script to ${location}`);
const filename = path.join(
__dirname,
'../.completions',
`${name}.${shellExtension(location)}`
);
debug('Which filename', filename);

return new Promise((resolve, reject) => {
const stream = fs.createWriteStream(untildify(location), { flags: 'a' });
stream.on('error', reject);
stream.on('finish', () => resolve());

debug('Writing to shell configuration file (%s)', location);
stream.write(`\n# tabtab source for ${name} package`);
stream.write('\n# uninstall by removing these lines');

if (location === BASH_LOCATION) {
stream.write(`\n[ -f ${filename} ] && . ${filename} || true`);
} else if (location === FISH_LOCATION) {
debug('Addding fish line');
stream.write(`\n[ -f ${filename} ]; and . ${filename}; or true`);
} else if (location === ZSH_LOCATION) {
debug('Addding zsh line');
stream.write(`\n[[ -f ${filename} ]] && . ${filename} || true`);
}

stream.end('\n');
});
};

const writeToCompletionScript = ({ name, completer, location }) => {
const filename = path.join(
__dirname,
'../.completions',
`${name}.${shellExtension(location)}`
);

const script = scriptFromLocation(location);
debug('Writing completion script to', filename);
debug('with', script);

return readFile(script, 'utf8')
.then(filecontent => {
return filecontent
.replace(/\{pkgname\}/g, name)
.replace(/{completer}/g, completer)
// on Bash on windows, we need to make sure to remove any \r
.replace(/\r?\n/g, '\n');
})
.then(filecontent => writeFile(filename, filecontent));
};

const installer = {
install(options = { name: '', completer: '', location: '' }) {
if (!options.name) {
throw new TypeError('options.name is required');
}

if (!options.completer) {
throw new TypeError('options.completer is required');
}

if (!options.location) {
throw new TypeError('options.location is required');
}

return Promise.all([
writeToShellConfig(options),
writeToCompletionScript(options)
]);
},

uninstall(options = { name: '' }) {
throw new Error('Not yet implemented');
},

writeToShellConfig,
writeToCompletionScript
};

module.exports = installer;
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -8,7 +8,8 @@
"mocha": "DEBUG='tabtab*' mocha --timeout 5000",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"watch": "npm-watch",
"prettier": "prettier -l '{lib,test}/**/*.js'"
"prettier": "prettier -l '{lib,test}/**/*.js'",
"quick-test": "DEBUG='tabtab*' node test/fixtures/installer-install.js"
},
"watch": {
"test": "{lib,test}/**/*.js"
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/installer-install.js
@@ -0,0 +1,9 @@
const { install, uninstall } = require('../../lib/installer');

(async () => {
await install({
name: 'foo',
completer: 'foo-complete',
location: '~/.bashrc'
});
})();
63 changes: 63 additions & 0 deletions test/installer.js
@@ -0,0 +1,63 @@
const assert = require('assert');
const {
install,
uninstall,
writeToShellConfig,
writeToCompletionScript
} = require('../lib/installer');
const fs = require('fs');
const path = require('path');
const untildify = require('untildify');
const { promisify } = require('es6-promisify');
const readFile = promisify(fs.readFile);

describe('installer', () => {
it('has install / uninstall functions', () => {
assert.equal(typeof install, 'function');
assert.equal(typeof uninstall, 'function');
});

it('both throws on missing options', () => {
assert.throws(() => install(), /options.name is required/);
assert.throws(
() => install({ name: 'foo ' }),
/options.completer is required/
);

assert.throws(
() => install({ name: 'foo ', completer: 'foo-complete' }),
/options.location is required/
);
});

it('has writeToShellConfig / writeToCompletionScript functions', () => {
assert.equal(typeof writeToShellConfig, 'function');
assert.equal(typeof writeToCompletionScript, 'function');
});

describe('installer on ~/.bashrc', () => {
const bashrc = fs.readFileSync(untildify('~/.bashrc'));

afterEach(done => {
fs.writeFile(untildify('~/.bashrc'), bashrc, done);
});

it('installs the necessary line into ~/.bashrc', () => {
return install({
name: 'foo',
completer: 'foo-complete',
location: '~/.bashrc'
})
.then(() => readFile(untildify('~/.bashrc'), 'utf8'))
.then(filecontent => {
assert.ok(/tabtab source for foo/.test(filecontent));
assert.ok(/uninstall by removing these lines/.test(filecontent));
assert.ok(
/\[ -f .+foo.bash ] && \. .+tabtab\/.completions\/foo.bash || true/.test(
filecontent
)
);
});
});
});
});
68 changes: 44 additions & 24 deletions test/tabtab-install.js
@@ -1,8 +1,12 @@
const tabtab = require('..');
const assert = require('assert');
const run = require('inquirer-test');
const path = require('path');
const debug = require('debug')('tabtab:test:install');
const untildify = require('untildify');
const path = require('path');
const fs = require('fs');
const { promisify } = require('es6-promisify');
const readFile = promisify(fs.readFile);

// inquirer-test needs a little bit more time, or my setup
const TIMEOUT = 500;
Expand Down Expand Up @@ -33,11 +37,21 @@ describe('tabtab.install()', () => {
}, /options\.completer is required/);
});

it('asks about shell (bash)', () => {
const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js');
describe('tabtab.install() on ~/.bashrc', () => {
const bashrc = fs.readFileSync(untildify('~/.bashrc'));

afterEach(done => {
fs.writeFile(untildify('~/.bashrc'), bashrc, done);
});

return run([cliPath], [ENTER, 'n', ENTER, '/tmp/foo', ENTER], TIMEOUT).then(
result => {
it('asks about shell (bash)', () => {
const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js');

return run(
[cliPath],
[ENTER, 'n', ENTER, '/tmp/foo', ENTER],
TIMEOUT
).then(result => {
debug('Test result', result);

assert.ok(/Which Shell do you use \? bash/.test(result));
Expand All @@ -46,29 +60,35 @@ describe('tabtab.install()', () => {
);
assert.ok(/Which path then \? Must be absolute/.test(result));
assert.ok(/Very well, we will install using \/tmp\/foo/.test(result));
assert.ok(
/Result \{ location: '\/tmp\/foo', shell: 'bash' \}/.test(result)
);
}
);
});

return Promise.resolve();
});
return Promise.resolve();
});

it('asks about shell (bash) with default location', () => {
const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js');
it('asks about shell (bash) with default location', () => {
const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js');

return run([cliPath], [ENTER, ENTER], TIMEOUT).then(
result => {
debug('Test result', result);
return run([cliPath], [ENTER, ENTER], TIMEOUT)
.then(result => {
debug('Test result', result);

assert.ok(/Which Shell do you use \? bash/.test(result));
assert.ok(
/We will install completion to ~\/\.bashrc, is it ok \? Yes/.test(result)
);
}
);
assert.ok(/Which Shell do you use \? bash/.test(result));
assert.ok(
/install completion to ~\/\.bashrc, is it ok \? Yes/.test(result)
);
})
.then(() => readFile(untildify('~/.bashrc'), 'utf8'))
.then(filecontent => {
assert.ok(/tabtab source for foo/.test(filecontent));
assert.ok(/uninstall by removing these lines/.test(filecontent));
assert.ok(
/\[ -f .+foo.bash ] && \. .+tabtab\/.completions\/foo.bash || true/.test(
filecontent
)
);
});

return Promise.resolve();
return Promise.resolve();
});
});
});

0 comments on commit 2030676

Please sign in to comment.