1<p align="center">
2  <a href="http://liftoffjs.com">
3    <img height="100" width="297" src="https://cdn.rawgit.com/tkellen/js-liftoff/master/artwork/liftoff.svg"/>
4  </a>
5</p>
6
7# liftoff [![Build Status](http://img.shields.io/travis/js-cli/js-liftoff.svg?label=travis-ci)](http://travis-ci.org/js-cli/js-liftoff) [![Build status](https://img.shields.io/appveyor/ci/phated/js-liftoff.svg?label=appveyor)](https://ci.appveyor.com/project/phated/js-liftoff)
8
9
10> Launch your command line tool with ease.
11
12[![NPM](https://nodei.co/npm/liftoff.png)](https://nodei.co/npm/liftoff/)
13
14## What is it?
15[See this blog post](http://weblog.bocoup.com/building-command-line-tools-in-node-with-liftoff/), [check out this proof of concept](https://github.com/js-cli/js-hacker), or read on.
16
17Say you're writing a CLI tool.  Let's call it [hacker](https://github.com/js-cli/js-hacker).  You want to configure it using a `Hackerfile`.  This is node, so you install `hacker` locally for each project you use it in.  But, in order to get the `hacker` command in your PATH, you also install it globally.
18
19Now, when you run `hacker`, you want to configure what it does using the `Hackerfile` in your current directory, and you want it to execute using the local installation of your tool.  Also, it'd be nice if the `hacker` command was smart enough to traverse up your folders until it finds a `Hackerfile`&mdash;for those times when you're not in the root directory of your project.  Heck, you might even want to launch `hacker` from a folder outside of your project by manually specifying a working directory.  Liftoff manages this for you.
20
21So, everything is working great.  Now you can find your local `hacker` and `Hackerfile` with ease.  Unfortunately, it turns out you've authored your `Hackerfile` in coffee-script, or some other JS variant.  In order to support *that*, you have to load the compiler for it, and then register the extension for it with node.  Good news, Liftoff can do that, and a whole lot more, too.
22
23## API
24
25### constructor(opts)
26
27Create an instance of Liftoff to invoke your application.
28
29An example utilizing all options:
30```js
31const Hacker = new Liftoff({
32  name: 'hacker',
33  processTitle: 'hacker',
34  moduleName: 'hacker',
35  configName: 'hackerfile',
36  extensions: {
37    '.js': null,
38    '.json': null,
39    '.coffee': 'coffee-script/register'
40  },
41  v8flags: ['--harmony'] // or v8flags: require('v8flags')
42});
43```
44
45#### opts.name
46
47Sugar for setting `processTitle`, `moduleName`, `configName` automatically.
48
49Type: `String`
50Default: `null`
51
52These are equivalent:
53```js
54const Hacker = Liftoff({
55  processTitle: 'hacker',
56  moduleName: 'hacker',
57  configName: 'hackerfile'
58});
59```
60```js
61const Hacker = Liftoff({name:'hacker'});
62```
63
64#### opts.moduleName
65
66Sets which module your application expects to find locally when being run.
67
68Type: `String`
69Default: `null`
70
71#### opts.configName
72
73Sets the name of the configuration file Liftoff will attempt to find.  Case-insensitive.
74
75Type: `String`
76Default: `null`
77
78#### opts.extensions
79
80Set extensions to include when searching for a configuration file.  If an external module is needed to load a given extension (e.g. `.coffee`), the module name should be specified as the value for the key.
81
82Type: `Object`
83Default: `{".js":null,".json":null}`
84
85**Examples:**
86
87In this example Liftoff will look for `myappfile{.js,.json,.coffee}`.  If a config with the extension `.coffee` is found, Liftoff will try to require `coffee-script/require` from the current working directory.
88```js
89const MyApp = new Liftoff({
90  name: 'myapp',
91  extensions: {
92    '.js': null,
93    '.json': null,
94    '.coffee': 'coffee-script/register'
95  }
96});
97```
98
99In this example, Liftoff will look for `.myapp{rc}`.
100```js
101const MyApp = new Liftoff({
102  name: 'myapp',
103  configName: '.myapp',
104  extensions: {
105    'rc': null
106  }
107});
108```
109
110In this example, Liftoff will automatically attempt to load the correct module for any javascript variant supported by [interpret](https://github.com/js-cli/js-interpret) (as long as it does not require a register method).
111
112```js
113const MyApp = new Liftoff({
114  name: 'myapp',
115  extensions: require('interpret').jsVariants
116});
117```
118#### opts.v8flags
119
120Any flag specified here will be applied to node, not your program.  Useful for supporting invocations like `myapp --harmony command`, where `--harmony` should be passed to node, not your program. This functionality is implemented using [flagged-respawn](http://github.com/js-cli/js-flagged-respawn). To support all v8flags, see [v8flags](https://github.com/js-cli/js-v8flags).
121
122Type: `Array|Function`
123Default: `null`
124
125If this method is a function, it should take a node-style callback that yields an array of flags.
126
127#### opts.processTitle
128
129Sets what the [process title](http://nodejs.org/api/process.html#process_process_title) will be.
130
131Type: `String`
132Default: `null`
133
134#### opts.completions(type)
135
136A method to handle bash/zsh/whatever completions.
137
138Type: `Function`
139Default: `null`
140
141#### opts.configFiles
142
143An object of configuration files to find. Each property is keyed by the default basename of the file being found, and the value is an object of [path arguments](#path-arguments) keyed by unique names.
144
145__Note:__ This option is useful if, for example, you want to support an `.apprc` file in addition to an `appfile.js`. If you only need a single configuration file, you probably don't need this. In addition to letting you find multiple files, this option allows more fine-grained control over how configuration files are located.
146
147Type: `Object`
148Default: `null`
149
150#### Path arguments
151
152The [`fined`](https://github.com/js-cli/fined) module accepts a string representing the path to search or an object with the following keys:
153
154* `path` __(required)__
155
156  The path to search. Using only a string expands to this property.
157
158  Type: `String`
159  Default: `null`
160
161* `name`
162
163  The basename of the file to find. Extensions are appended during lookup.
164
165  Type: `String`
166  Default: Top-level key in `configFiles`
167
168* `extensions`
169
170  The extensions to append to `name` during lookup. See also: [`opts.extensions`](#optsextensions).
171
172  Type: `String|Array|Object`
173  Default: The value of [`opts.extensions`](#optsextensions)
174
175* `cwd`
176
177  The base directory of `path` (if relative).
178
179  Type: `String`
180  Default: The value of [`opts.cwd`](#optscwd)
181
182* `findUp`
183
184  Whether the `path` should be traversed up to find the file.
185
186  Type: `Boolean`
187  Default: `false`
188
189**Examples:**
190
191In this example Liftoff will look for the `.hacker.js` file relative to the `cwd` as declared in `configFiles`.
192```js
193const MyApp = new Liftoff({
194  name: 'hacker',
195  configFiles: {
196    '.hacker': {
197      cwd: '.'
198    }
199  }
200});
201```
202
203In this example, Liftoff will look for `.hackerrc` in the home directory.
204```js
205const MyApp = new Liftoff({
206  name: 'hacker',
207  configFiles: {
208    '.hacker': {
209      home: {
210        path: '~',
211        extensions: {
212          'rc': null
213        }
214      }
215    }
216  }
217});
218```
219
220In this example, Liftoff will look in the `cwd` and then lookup the tree for the `.hacker.js` file.
221```js
222const MyApp = new Liftoff({
223  name: 'hacker',
224  configFiles: {
225    '.hacker': {
226      up: {
227        path: '.',
228        findUp: true
229      }
230    }
231  }
232});
233```
234
235In this example, the `name` is overridden and the key is ignored so Liftoff looks for `.override.js`.
236```js
237const MyApp = new Liftoff({
238  name: 'hacker',
239  configFiles: {
240    hacker: {
241      override: {
242        path: '.',
243        name: '.override'
244      }
245    }
246  }
247});
248```
249
250In this example, Liftoff will use the home directory as the `cwd` and looks for `~/.hacker.js`.
251```js
252const MyApp = new Liftoff({
253  name: 'hacker',
254  configFiles: {
255    '.hacker': {
256      home: {
257        path: '.',
258        cwd: '~'
259      }
260    }
261  }
262});
263```
264
265## launch(opts, callback(env))
266Launches your application with provided options, builds an environment, and invokes your callback, passing the calculated environment as the first argument.
267
268##### Example Configuration w/ Options Parsing:
269```js
270const Liftoff = require('liftoff');
271const MyApp = new Liftoff({name:'myapp'});
272const argv = require('minimist')(process.argv.slice(2));
273const invoke = function (env) {
274  console.log('my environment is:', env);
275  console.log('my cli options are:', argv);
276  console.log('my liftoff config is:', this);
277};
278MyApp.launch({
279  cwd: argv.cwd,
280  configPath: argv.myappfile,
281  require: argv.require,
282  completion: argv.completion
283}, invoke);
284```
285
286#### opts.cwd
287
288Change the current working directory for this launch. Relative paths are calculated against `process.cwd()`.
289
290Type: `String`
291Default: `process.cwd()`
292
293**Example Configuration:**
294```js
295const argv = require('minimist')(process.argv.slice(2));
296MyApp.launch({
297  cwd: argv.cwd
298}, invoke);
299```
300
301**Matching CLI Invocation:**
302```
303myapp --cwd ../
304```
305
306#### opts.configPath
307
308Don't search for a config, use the one provided. **Note:** Liftoff will assume the current working directory is the directory containing the config file unless an alternate location is explicitly specified using `cwd`.
309
310Type: `String`
311Default: `null`
312
313**Example Configuration:**
314```js
315var argv = require('minimist')(process.argv.slice(2));
316MyApp.launch({
317  configPath: argv.myappfile
318}, invoke);
319```
320
321**Matching CLI Invocation:**
322```
323myapp --myappfile /var/www/project/Myappfile.js
324```
325
326**Examples using `cwd` and `configPath` together:**
327
328These are functionally identical:
329```
330myapp --myappfile /var/www/project/Myappfile.js
331myapp --cwd /var/www/project
332```
333
334These can run myapp from a shared directory as though it were located in another project:
335```
336myapp --myappfile /Users/name/Myappfile.js --cwd /var/www/project1
337myapp --myappfile /Users/name/Myappfile.js --cwd /var/www/project2
338```
339
340#### opts.require
341
342A string or array of modules to attempt requiring from the local working directory before invoking the launch callback.
343
344Type: `String|Array`
345Default: `null`
346
347**Example Configuration:**
348```js
349var argv = require('minimist')(process.argv.slice(2));
350MyApp.launch({
351  require: argv.require
352}, invoke);
353```
354
355**Matching CLI Invocation:**
356```js
357myapp --require coffee-script/register
358```
359
360#### opts.forcedFlags
361
362Allows you to force node or V8 flags during the launch. This is useful if you need to make sure certain flags will always be enabled or if you need to enable flags that don't show up in `opts.v8flags` (as these flags aren't validated against `opts.v8flags`).
363
364If this is specified as a function, it will receive the built `env` as its only argument and must return a string or array of flags to force.
365
366Type: `String|Array|Function`
367Default: `null`
368
369**Example Configuration:**
370```js
371MyApp.launch({
372  forcedFlags: ['--trace-deprecation']
373}, invoke);
374```
375
376**Matching CLI Invocation:**
377```js
378myapp --trace-deprecation
379```
380
381#### callback(env)
382
383A function to start your application.  When invoked, `this` will be your instance of Liftoff. The `env` param will contain the following keys:
384
385- `cwd`: the current working directory
386- `require`: an array of modules that liftoff tried to pre-load
387- `configNameSearch`: the config files searched for
388- `configPath`: the full path to your configuration file (if found)
389- `configBase`: the base directory of your configuration file (if found)
390- `modulePath`: the full path to the local module your project relies on (if found)
391- `modulePackage`: the contents of the local module's package.json (if found)
392- `configFiles`: an object of filepaths for each found config file (filepath values will be null if not found)
393
394### events
395
396#### require(name, module)
397
398Emitted when a module is pre-loaded.
399
400```js
401var Hacker = new Liftoff({name:'hacker'});
402Hacker.on('require', function (name, module) {
403  console.log('Requiring external module: '+name+'...');
404  // automatically register coffee-script extensions
405  if (name === 'coffee-script') {
406    module.register();
407  }
408});
409```
410
411#### requireFail(name, err)
412
413Emitted when a requested module cannot be preloaded.
414
415```js
416var Hacker = new Liftoff({name:'hacker'});
417Hacker.on('requireFail', function (name, err) {
418  console.log('Unable to load:', name, err);
419});
420```
421
422#### respawn(flags, child)
423
424Emitted when Liftoff re-spawns your process (when a [`v8flags`](#optsv8flags) is detected).
425
426```js
427var Hacker = new Liftoff({
428  name: 'hacker',
429  v8flags: ['--harmony']
430});
431Hacker.on('respawn', function (flags, child) {
432  console.log('Detected node flags:', flags);
433  console.log('Respawned to PID:', child.pid);
434});
435```
436
437Event will be triggered for this command:
438`hacker --harmony commmand`
439
440## Examples
441
442Check out how [gulp](https://github.com/gulpjs/gulp-cli/blob/master/index.js) uses Liftoff.
443
444For a bare-bones example, try [the hacker project](https://github.com/js-cli/js-hacker/blob/master/bin/hacker.js).
445
446To try the example, do the following:
447
4481. Install the sample project `hacker` with `npm install -g hacker`.
4492. Make a `Hackerfile.js` with some arbitrary javascript it.
4503. Install hacker next to it with `npm install hacker`.
4513. Run `hacker` while in the same parent folder.
452