[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.
Say 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.
Now, 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`—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.
So, 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.
## API
### constructor(opts)
Create an instance of Liftoff to invoke your application.
An example utilizing all options:
```js
const Hacker = new Liftoff({
name: 'hacker',
processTitle: 'hacker',
moduleName: 'hacker',
configName: 'hackerfile',
extensions: {
'.js': null,
'.json': null,
'.coffee': 'coffee-script/register'
},
v8flags: ['--harmony'] // or v8flags: require('v8flags')
});
```
#### opts.name
Sugar for setting `processTitle`, `moduleName`, `configName` automatically.
Type: `String`
Default: `null`
These are equivalent:
```js
const Hacker = Liftoff({
processTitle: 'hacker',
moduleName: 'hacker',
configName: 'hackerfile'
});
```
```js
const Hacker = Liftoff({name:'hacker'});
```
#### opts.moduleName
Sets which module your application expects to find locally when being run.
Type: `String`
Default: `null`
#### opts.configName
Sets the name of the configuration file Liftoff will attempt to find. Case-insensitive.
Type: `String`
Default: `null`
#### opts.extensions
Set 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.
Type: `Object`
Default: `{".js":null,".json":null}`
**Examples:**
In 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.
```js
const MyApp = new Liftoff({
name: 'myapp',
extensions: {
'.js': null,
'.json': null,
'.coffee': 'coffee-script/register'
}
});
```
In this example, Liftoff will look for `.myapp{rc}`.
```js
const MyApp = new Liftoff({
name: 'myapp',
configName: '.myapp',
extensions: {
'rc': null
}
});
```
In this example, Liftoff will automatically attempt to load the correct module for any javascript variant supported by [node-interpret](https://github.com/tkellen/node-interpret) (as long as it does not require a register method).
```js
const MyApp = new Liftoff({
name: 'myapp',
extensions: require('interpret').jsVariants
});
```
#### opts.v8flags
Any 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/tkellen/node-flagged-respawn). To support all v8flags, see [node-v8flags](https://github.com/tkellen/node-v8flags).
Type: `Array|Function`
Default: `null`
If this method is a function, it should take a node-style callback that yields an array of flags.
#### opts.processTitle
Sets what the [process title](http://nodejs.org/api/process.html#process_process_title) will be.
Type: `String`
Default: `null`
#### opts.completions(type)
A method to handle bash/zsh/whatever completions.
Type: `Function`
Default: `null`
#### opts.configFiles
An 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.
__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.
Type: `Object`
Default: `null`
#### Path arguments
The [`fined`](https://github.com/js-cli/fined) module accepts a string representing the path to search or an object with the following keys:
* `path` __(required)__
The path to search. Using only a string expands to this property.
Type: `String`
Default: `null`
* `name`
The basename of the file to find. Extensions are appended during lookup.
Type: `String`
Default: Top-level key in `configFiles`
* `extensions`
The extensions to append to `name` during lookup. See also: [`opts.extensions`](#optsextensions).
Type: `String|Array|Object`
Default: The value of [`opts.extensions`](#optsextensions)
* `cwd`
The base directory of `path` (if relative).
Type: `String`
Default: The value of [`opts.cwd`](#optscwd)
* `findUp`
Whether the `path` should be traversed up to find the file.
Type: `Boolean`
Default: `false`
**Examples:**
In this example Liftoff will look for the `.hacker.js` file relative to the `cwd` as declared in `configFiles`.
```js
const MyApp = new Liftoff({
name: 'hacker',
configFiles: {
'.hacker': {
cwd: '.'
}
}
});
```
In this example, Liftoff will look for `.hackerrc` in the home directory.
```js
const MyApp = new Liftoff({
name: 'hacker',
configFiles: {
'.hacker': {
home: {
path: '~',
extensions: {
'rc': null
}
}
}
}
});
```
In this example, Liftoff will look in the `cwd` and then lookup the tree for the `.hacker.js` file.
```js
const MyApp = new Liftoff({
name: 'hacker',
configFiles: {
'.hacker': {
up: {
path: '.',
findUp: true
}
}
}
});
```
In this example, the `name` is overridden and the key is ignored so Liftoff looks for `.override.js`.
```js
const MyApp = new Liftoff({
name: 'hacker',
configFiles: {
hacker: {
override: {
path: '.',
name: '.override'
}
}
}
});
```
In this example, Liftoff will use the home directory as the `cwd` and looks for `~/.hacker.js`.
```js
const MyApp = new Liftoff({
name: 'hacker',
configFiles: {
'.hacker': {
home: {
path: '.',
cwd: '~'
}
}
}
});
```
## launch(opts, callback(env))
Launches your application with provided options, builds an environment, and invokes your callback, passing the calculated environment as the first argument.
Don'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`.
Type: `String`
Default: `null`
**Example Configuration:**
```js
var argv = require('minimist')(process.argv.slice(2));
MyApp.launch({
configPath: argv.myappfile
}, invoke);
```
**Matching CLI Invocation:**
```
myapp --myappfile /var/www/project/Myappfile.js
```
**Examples using `cwd` and `configPath` together:**
These are functionally identical:
```
myapp --myappfile /var/www/project/Myappfile.js
myapp --cwd /var/www/project
```
These can run myapp from a shared directory as though it were located in another project: