Skip to content

Commit 272d175

Browse files
author
Kent C. Dodds
committed
feat(autocomplete): add completion script to install autocomplete
**What**: This adds a command called `completion` which installs the autocomplete shell script. It also adds the logic to provide the autocompletion. **Why**: Because that's pretty swell! **How**: Using omelette which is pretty awesome.
1 parent f01ab0d commit 272d175

7 files changed

Lines changed: 162 additions & 8 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ npm install --global p-s
122122
```
123123

124124
From here you can use `p-s` on the command line via one of the installed aliases: `p-s`, `package-scripts`, or `nps`.
125-
In the future, this may support autocomplete capabilities. Check out #28 for updates on this.
125+
If you do this, you may also be interested in installing the shell autocompletion script. Do so by running:
126+
127+
```
128+
nps completion <optionally-your-bash-profile-file>
129+
```
130+
131+
The bash profile file defaults to `~/.bash_profile`. Special thanks to the [`omelette`][omelette] package for making
132+
this so easy.
126133

127134
## Getting started
128135

@@ -392,3 +399,4 @@ MIT
392399
[scripty]: https://npmjs.com/package/scripty
393400
[npm scripts]: https://docs.npmjs.com/misc/scripts
394401
[video]: http://kcd.im/p-s-video
402+
[omelette]: https://npmjs.com/package/omelette

package-scripts.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
/* eslint prefer-template:"off", no-var:"off", max-len:[2, 200] */ // this file runs in node 0.10.0
2+
var transpile = 'babel --copy-files --out-dir dist --ignore *.test.js,fixtures src'
3+
24
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1])
35
var validate = ['build', 'test']
46
if (nodeVersion >= 4) {
57
validate.push('lint') // we can't run linting on node versions < 4
68
}
9+
710
module.exports = {
811
scripts: {
912
commit: {
@@ -21,8 +24,13 @@ module.exports = {
2124
},
2225
},
2326
build: {
24-
description: 'deletes the `dist` directory and transpiles all relevant `src` to the `dist`',
25-
script: 'rimraf dist && babel --copy-files --out-dir dist --ignore *.test.js,fixtures src',
27+
default: {
28+
description: 'deletes the `dist` directory and transpiles all relevant `src` to the `dist`',
29+
script: 'rimraf dist && ' + transpile,
30+
},
31+
watch: {
32+
script: 'rimraf dist && ' + transpile + ' --watch',
33+
},
2634
},
2735
lint: {
2836
description: 'lint the code with eslint',

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"find-up": "1.1.2",
2828
"lodash": "4.14.0",
2929
"manage-path": "2.0.0",
30+
"omelette": "0.3.1",
3031
"prefix-matches": "0.0.9",
3132
"shell-escape": "0.2.0",
3233
"spawn-command": "0.0.2"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint no-invalid-this:"off" */
2+
import omelette from 'omelette'
3+
import {includes, isPlainObject, kebabCase} from 'lodash'
4+
5+
const complete = omelette('nps <script>')
6+
7+
export {autocomplete as default, install}
8+
9+
function autocomplete(config = {}) {
10+
complete.on('script', onScript)
11+
complete.init()
12+
13+
function onScript() {
14+
this.reply(getScripts(config.scripts))
15+
}
16+
}
17+
18+
function install(destination = '~/.bash_profile') {
19+
complete.setupShellInitFile(destination)
20+
return destination
21+
}
22+
23+
function getScripts(scriptsObject, prefix = '', allScripts = []) {
24+
const excludedKeys = ['default', 'script', 'description']
25+
return Object.keys(scriptsObject).reduce((acc, key) => {
26+
if (includes(excludedKeys, key)) {
27+
return acc
28+
}
29+
const value = scriptsObject[key]
30+
const kebabKey = kebabCase(key)
31+
const deepKey = prefix ? `${prefix}.${kebabKey}` : kebabKey
32+
acc.push(deepKey)
33+
if (isPlainObject(value)) {
34+
getScripts(value, deepKey, acc)
35+
}
36+
return acc
37+
}, allScripts)
38+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import test from 'ava'
2+
import {spy} from 'sinon'
3+
import proxyquire from 'proxyquire'
4+
5+
test('inits omelette', t => {
6+
const {autocomplete, initSpy} = getAutocomplete()
7+
autocomplete()
8+
t.true(initSpy.calledOnce)
9+
})
10+
11+
test('calls this.reply with the available scripts', t => {
12+
const stubConfig = {
13+
scripts: {
14+
build: {default: {script: 'build'}, watch: 'build.watch', main: {umd: 'build.main.umd', default: 'build.main'}},
15+
lint: {default: {script: 'lint', description: 'lint things'}, watch: 'lint.watch'},
16+
test: 'test',
17+
camelCase: 'camelCase',
18+
cover: {description: 'this is a description', script: 'this is the script'},
19+
},
20+
}
21+
const expectedReplyArgs = [
22+
'build', 'build.watch', 'build.main', 'build.main.umd',
23+
'lint', 'lint.watch',
24+
'test', 'camel-case', 'cover',
25+
]
26+
const {autocomplete, getReplyArgs} = getAutocomplete()
27+
autocomplete(stubConfig)
28+
const actualReplyArgs = getReplyArgs()
29+
t.deepEqual(actualReplyArgs, expectedReplyArgs)
30+
})
31+
32+
test('install calls setupShellInitFile with the given destination', t => {
33+
const {install, setupShellInitFileSpy} = getInstall()
34+
const destination = '~/.my_bash_profile'
35+
install(destination)
36+
const [actualDestination] = setupShellInitFileSpy.firstCall.args
37+
t.true(setupShellInitFileSpy.calledOnce)
38+
t.is(actualDestination, destination)
39+
})
40+
41+
test('install defaults to ~/.bash_profile', t => {
42+
const {install, setupShellInitFileSpy} = getInstall()
43+
const expectedDestination = '~/.bash_profile'
44+
install()
45+
const [actualDestination] = setupShellInitFileSpy.firstCall.args
46+
t.true(setupShellInitFileSpy.calledOnce)
47+
t.is(actualDestination, expectedDestination)
48+
})
49+
50+
function getAutocomplete() {
51+
const onSpy = spy()
52+
const initSpy = spy()
53+
const omelette = spy(() => {
54+
return {on: onSpy, init: initSpy}
55+
})
56+
const autocomplete = proxyquire('./index', {omelette}).default
57+
return {autocomplete, getReplyArgs, initSpy}
58+
59+
function getReplyArgs() {
60+
const [, replier] = onSpy.firstCall.args
61+
const reply = spy()
62+
const context = {reply}
63+
replier.call(context)
64+
return reply.firstCall.args[0]
65+
}
66+
}
67+
68+
function getInstall() {
69+
const setupShellInitFileSpy = spy()
70+
const omelette = spy(() => ({setupShellInitFile: setupShellInitFileSpy}))
71+
const {install} = proxyquire('./index', {omelette})
72+
return {install, setupShellInitFileSpy}
73+
}

src/bin-utils/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import colors from 'colors/safe'
66
import getLogger from '../get-logger'
77
import {resolveScriptObjectToScript} from '../resolve-script-object-to-string'
88
import initialize from './initialize'
9+
import {default as autocomplete, install as installAutocomplete} from './autocomplete'
910

1011
const log = getLogger()
1112

@@ -36,7 +37,7 @@ const loadConfig = getAttemptModuleRequireFn(function onFail(configPath, require
3637
})
3738

3839
export {
39-
getScriptsAndArgs, initialize, help,
40+
getScriptsAndArgs, initialize, help, autocomplete, installAutocomplete,
4041
getModuleRequirePath, preloadModule, loadConfig,
4142
}
4243

src/bin/p-s.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
#!/usr/bin/env node
22
/* eslint no-process-exit: "off" */
33
import findUp from 'find-up'
4-
import {merge} from 'lodash'
4+
import {merge, includes} from 'lodash'
55
import program from 'commander'
66
import colors from 'colors/safe'
77
import runPackageScript from '../index'
8-
import {getScriptsAndArgs, initialize, help, preloadModule, loadConfig} from '../bin-utils'
8+
import {
9+
getScriptsAndArgs, initialize, autocomplete, installAutocomplete,
10+
help, preloadModule, loadConfig,
11+
} from '../bin-utils'
912
import getLogger from '../get-logger'
1013

14+
const {version} = require('../../package.json')
15+
1116
const log = getLogger()
1217
const FAIL_CODE = 1
1318
let shouldRun = true
19+
const shouldAutocomplete = includes(process.argv, '--compbash')
1420

1521
program
16-
.version(require('../../package.json').version)
22+
.version(version)
1723
.allowUnknownOption()
1824
.option('-s, --silent', 'Silent p-s output')
1925
.option('-p, --parallel <script-name1,script-name2>', 'Scripts to run in parallel (comma seprated)')
2026
.option('-c, --config <filepath>', 'Config file to use (defaults to nearest package-scripts.js)')
2127
.option('-l, --log-level <level>', 'The log level to use (error, warn, info [default])')
2228
.option('-r, --require <module>', 'Module to preload')
2329
.on('init', onInit)
30+
.on('completion', onRequestToInstallCompletion)
2431
.on('--help', onHelp)
2532
.parse(process.argv)
2633

27-
if (shouldRun) {
34+
35+
if (shouldAutocomplete) {
36+
autocomplete(getPSConfig())
37+
} else if (shouldRun) {
2838
const psConfig = getPSConfig()
2939
const hasDefaultScript = !!psConfig.scripts.default
3040
const scriptsAndArgs = getScriptsAndArgs(program)
@@ -73,9 +83,24 @@ function onInit() {
7383
'Check out your scripts in there. Go ahead and update them and add descriptions to the ones that need it'
7484
))
7585
log.info(colors.gray('Your package.json scripts have also been updated. Run `npm start -- --help` for help'))
86+
log.info(colors.gray(
87+
'You may also want to install the package globally and installing autocomplete script. You can do so by running\n' +
88+
' npm install --global p-s\n' +
89+
' nps completion <optionally-your-bash-profile-file>\n' +
90+
'The bash profile file defaults to ~/.bash_profile'
91+
))
7692
}
7793

7894
function onHelp() {
7995
shouldRun = false
8096
log.info(help(getPSConfig()))
8197
}
98+
99+
function onRequestToInstallCompletion() {
100+
shouldRun = false
101+
const [, bin,, destination] = process.argv
102+
const finalDestination = installAutocomplete(destination)
103+
log.info(
104+
`Autocompletion has been set up and installed into ${colors.bold.green(finalDestination)} for ${colors.bold(bin)}`,
105+
)
106+
}

0 commit comments

Comments
 (0)