Repository: bustlelabs/shep
Pull request #82 (2016-10-02 16:12:09)

Massive refactor
This one got a little intense but I think it sets us up better for some future improvements.

Highlights:
- `observatory` not really being maintained anymore so switching to `listr` for task reporting
- Isolate more logic in the `util` folder. Task setup/running now much simpler and easier to test
- Remove webpack and make it a dependency of the generated project
- The generated webpack config now can build all the functions instead of just one. a CLI flag can be used to filter what is built by webpack.
- Removes concurrency option. Might be nice to get this back at some point, but hard to do with how the new webpack config works.
- Remove dependency on the entire lodash package.

Everything works here except `run` which needs a few tweaks. Ideally I can keep it backwards compatible while still supporting the new AWS lambda proxy style integration.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 4177b70..7566b6b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,8 +2,8 @@ language: node_js
 node_js:
   - stable
 install:
-  - npm install -g ava
   - npm install
 
 script:
+  - npm run lint
   - npm run test
diff --git a/README.md b/README.md
index b260d7a..72d2c32 100644
--- a/README.md
+++ b/README.md
@@ -39,13 +39,13 @@ _Coming soon!_
 Usage: shep <command> [options]
 
 Commands:
-  build [env] [functions..]   Builds functions and writes them to disk
-  deploy [env] [functions..]  Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified
-  generate                    Run `shep generate --help` for additional information
-  new [path]                  Create a new shep project
-  pull                        Pulls a swagger JSON representation of an existing API and writes it to a local file
-  push                        Pushes the api.json swagger configuration to API Gateway. Does not deploy the API.
-  run [name]                  Run a function in your local environemnt
+  build [env] [functions]   Builds functions and writes them to disk
+  deploy [env] [functions]  Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified
+  generate                  Run `shep generate --help` for additional information
+  new [path]                Create a new shep project
+  pull                      Pulls a swagger JSON representation of an existing API and writes it to a local file
+  push                      Create a new shep project
+  run [name]                Run a function in your local environemnt
 
 Options:
   --version  Show version number                                                                               [boolean]
@@ -106,36 +106,39 @@ Options:
   --help         Show help                                                                                     [boolean]
   --environment  Environment variables to use                                                   [default: "development"]
   --event        Event to use
+  -v             Responses from functions aren't truncated
+  --build        Build functions before running. Use --no-build to skip this step                        [default: true]
 
 Examples:
   shep run                               Launch an interactive CLI
   shep run foo                           Runs the `foo` function for all events
+  shep run foo --no-build                Run the already built `foo` function in the dist folder
   shep run foo --event default           Runs the `foo` function for just the `default` event
   shep run foo --environment production  Runs the `foo` function with production environment
 ```
 #### `shep deploy`
 ```
-shep deploy [env] [functions..]
+shep deploy [env] [functions]
 
 Options:
-  --version          Show version number                                                                       [boolean]
-  --help             Show help                                                                                 [boolean]
-  --api-id           The API Gateway API id. Read from package.json if not provided.                          [required]
-  --concurrency, -c  Number of functions to build and upload at one time                             [default: Infinity]
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+  --build    Build functions before deployment. Use --no-build to skip this step                         [default: true]
 
 Examples:
+  shep deploy                         Launch an interactive CLI
   shep deploy production              Deploy all functions with production env variables
+  shep deploy beta --no-build         Deploy all functions as currently built in the dist folder
   shep deploy production create-user  Deploy only the create-user function
-  shep deploy production *-user       Deploy only functions matching the pattern *-user
+  shep deploy beta *-user             Deploy only functions matching the pattern *-user
 ```
 #### `shep build`
 ```
-shep build [env] [functions..]
+shep build [env] [functions]
 
 Options:
-  --version          Show version number                                                                       [boolean]
-  --help             Show help                                                                                 [boolean]
-  --concurrency, -c  The number of functions to build at one time                                    [default: Infinity]
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
 
 Examples:
   shep build                   Launch an interactive CLI
@@ -143,6 +146,44 @@ Examples:
   shep build beta create-user  Build only the create-user function
   shep build beta *-user       Build functions matching the pattern *-user
 ```
+#### `shep generate function`
+```
+shep generate function [name]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+  --name     Function name
+
+Examples:
+  shep generate function      Launch an interactive CLI
+  shep generate function foo  Genereate a new functon called "foo"
+
+```
+#### `shep generate endpoint`
+```
+shep generate endpoint [path]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+  --method   HTTP Method                                     [choices: "get", "post", "put", "delete", "options", "any"]
+
+```
+#### `shep generate webpack`
+```
+shep generate webpack
+
+Options:
+  --version     Show version number                                                                            [boolean]
+  --help        Show help                                                                                      [boolean]
+  --output, -o  Output file                                                               [default: "webpack.config.js"]
+
+```
+
+## Lambda Execution Role
+
+More details to come, but check out the AWS docs about setting up the proper IAM role for lambda: http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html http://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-create-iam-role.html
 
 ## Why the name 'shep'?
 
diff --git a/package.json b/package.json
index 54a5a52..70ba6b1 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,9 @@
   "description": "A framework for building JavaScript APIs with AWS API Gateway and Lambda",
   "main": "lib/index.js",
   "scripts": {
-    "compile": "./node_modules/.bin/babel -d lib/ src/",
+    "compile": "babel -d lib/ src/",
     "test": "ava",
-    "lint": "jshint --exclude ./node_modules .",
+    "lint": "standard",
     "prepublish": "npm run compile"
   },
   "keywords": [
@@ -37,24 +37,23 @@
     "bluebird": "^3.4.0",
     "chalk": "^1.1.3",
     "cliui": "^3.2.0",
-    "fs-extra-promise": "^0.4.0",
-    "glob": "^7.0.0",
-    "globby": "^5.0.0",
+    "execa": "^0.4.0",
+    "fs-extra-promise": "^0.4.1",
     "inquirer": "^1.0.3",
-    "lodash": "^4.7.0",
+    "listr": "^0.6.1",
+    "lodash.kebabcase": "^4.1.1",
+    "lodash.merge": "^4.6.0",
     "loud-rejection": "^1.6.0",
-    "observatory": "^1.0.0",
     "stream-buffers": "^3.0.0",
-    "webpack": "2.1.0-beta.22",
-    "yargs": "^4.8.1"
+    "yargs": "^6.0.0"
   },
   "devDependencies": {
     "ava": "^0.16.0",
     "babel-cli": "^6.5.1",
     "babel-plugin-add-module-exports": "^0.2.1",
-    "babel-plugin-transform-object-rest-spread": "^6.8.0",
     "babel-preset-es2015-node4": "^2.1.0",
     "babel-register": "^6.7.2",
+    "standard": "^8.3.0",
     "testdouble": "^1.6.1"
   },
   "babel": {
diff --git a/src/build/index.js b/src/build/index.js
index 540b749..4fe7378 100644
--- a/src/build/index.js
+++ b/src/build/index.js
@@ -1,15 +1,16 @@
-import loadFuncs from '../util/load-funcs'
-import build from '../util/build-function'
-import Promise from 'bluebird'
-import { queue, done } from '../util/tasks'
+import listr from '../util/modules/listr'
+import build from '../util/build-functions'
 
-export default function(opts) {
-
-  const functions = loadFuncs(opts.functions)
+export default function (opts) {
+  const functions = opts.functions || '*'
   const env = opts.env || 'development'
-  const concurrency = opts.concurrency || Infinity
 
-  return Promise.resolve(functions)
-  .tap((funcs) => funcs.map(queue))
-  .map((func) => build(func, env).tap(() => done(func)), { concurrency })
+  const tasks = listr([
+    {
+      title: `Build Functions`,
+      task: () => build(functions, env)
+    }
+  ], opts.quiet)
+
+  return tasks.run()
 }
diff --git a/src/commands/build.js b/src/commands/build.js
index aa7cf99..4062474 100644
--- a/src/commands/build.js
+++ b/src/commands/build.js
@@ -1,32 +1,29 @@
 import inquirer from 'inquirer'
 import build from '../build'
-import loadEnvs from '../util/load-envs'
-import { merge } from 'lodash'
+import * as load from '../util/load'
+import merge from 'lodash.merge'
 
-export const command = 'build [env] [functions..]'
+export const command = 'build [env] [functions]'
 export const desc = 'Builds functions and writes them to disk'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
-  .describe('concurrency', 'The number of functions to build at one time')
-  .alias('concurrency', 'c')
-  .default('concurrency', Infinity )
   .example('shep build', 'Launch an interactive CLI')
   .example('shep build beta', 'Build all functions with beta environment variables')
   .example('shep build beta create-user', 'Build only the create-user function')
   .example('shep build beta *-user', 'Build functions matching the pattern *-user')
 }
 
-export function handler(opts) {
+export function handler (opts) {
   const questions = [
     {
       name: 'env',
       message: 'Environment',
       type: 'list',
-      choices: () => loadEnvs()
+      choices: () => load.envs()
     }
   ]
 
-  inquirer.prompt(questions.filter((q)=> !opts[q.name] ))
-  .then((inputs) => merge({}, inputs, opts) )
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
   .then(build)
 }
diff --git a/src/commands/deploy.js b/src/commands/deploy.js
index eeefa5e..7693afa 100644
--- a/src/commands/deploy.js
+++ b/src/commands/deploy.js
@@ -1,36 +1,33 @@
 import inquirer from 'inquirer'
 import deploy from '../deploy'
-import loadEnvs from '../util/load-envs'
-import { merge } from 'lodash'
+import * as load from '../util/load'
+import merge from 'lodash.merge'
 
-export const command = 'deploy [env] [functions..]'
+export const command = 'deploy [env] [functions]'
 export const desc = 'Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
   .pkgConf('shep', process.cwd())
-  .describe('api-id', 'The API Gateway API id. Read from package.json if not provided.')
-  .describe('concurrency', 'Number of functions to build and upload at one time')
-  .default('concurrency', Infinity)
-  .alias('concurrency', 'c')
   .describe('build', 'Build functions before deployment. Use --no-build to skip this step')
   .default('build', true)
+  .example('shep deploy', 'Launch an interactive CLI')
   .example('shep deploy production', 'Deploy all functions with production env variables')
-  .example('shep deploy production --no-build', 'Deploy all functions as currently built in the dist folder')
+  .example('shep deploy beta --no-build', 'Deploy all functions as currently built in the dist folder')
   .example('shep deploy production create-user', 'Deploy only the create-user function')
-  .example('shep deploy production *-user', 'Deploy only functions matching the pattern *-user')
+  .example('shep deploy beta *-user', 'Deploy only functions matching the pattern *-user')
 }
 
-export function handler(opts) {
+export function handler (opts) {
   const questions = [
     {
       name: 'env',
       message: 'Environment',
       type: 'list',
-      choices: () => loadEnvs()
+      choices: () => load.envs()
     }
   ]
 
-  inquirer.prompt(questions.filter((q)=> !opts[q.name] ))
-  .then((inputs) => merge({}, inputs, opts) )
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
   .then(deploy)
 }
diff --git a/src/commands/generate.js b/src/commands/generate.js
index b79e210..e3e5534 100644
--- a/src/commands/generate.js
+++ b/src/commands/generate.js
@@ -1,8 +1,6 @@
-import inquirer from 'inquirer'
-
 export const command = 'generate'
 export const desc = 'Run `shep generate --help` for additional information'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
   .commandDir('./generate')
 }
diff --git a/src/commands/generate/endpoint.js b/src/commands/generate/endpoint.js
index b020a8f..340d055 100644
--- a/src/commands/generate/endpoint.js
+++ b/src/commands/generate/endpoint.js
@@ -1,31 +1,25 @@
 import inquirer from 'inquirer'
-import { kebabCase } from 'lodash'
-import loadFuncs from '../../util/load-funcs'
-import generateEndpoint from '../../generate/endpoint'
+import generateEndpoint from '../../generate-endpoint'
+import merge from 'lodash.merge'
 
-const httpMethods = ['get','post','put','delete','options']
+const httpMethods = ['get', 'post', 'put', 'delete', 'options', 'any']
 
 export const command = 'endpoint [path]'
 export const desc = 'Generate a new API endpoint'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
+  .pkgConf('shep', process.cwd())
   .describe('method', 'HTTP Method')
   .choices('method', httpMethods)
-  .describe('response-code', 'Default response code')
-  .default('response-code', 200)
-  .describe('content-type', 'Default response content type')
-  .default('content-type', 'application/json')
-  .describe('function-name', 'Lambda function name')
-  .default('function-name', null, 'kebabCase(apiName path method)')
 }
 
-export function handler () {
+export function handler (opts) {
   inquirer.prompt([
     {
       name: 'path',
       type: 'input',
       message: 'Endpoint path',
-      default: '/posts/{id}',
+      default: '/users/{id}',
       validate: (val) => val === '' ? 'Path cannot be blank' : true
     },
     {
@@ -33,42 +27,8 @@ export function handler () {
       type: 'list',
       message: 'HTTP method',
       choices: httpMethods
-    },
-    {
-      name: 'contentType',
-      message: 'Response content type',
-      default: 'application/json'
-    },
-    {
-      name: 'responseCode',
-      message: 'Success response code',
-      default: '200'
-    },
-    {
-      name: 'cors',
-      type: 'confirm',
-      message: 'Enable CORS?'
-    },
-    {
-      name: 'createFunction',
-      message: 'Do you want to generate a new function or use an existing one?',
-      type: 'list',
-      choices: [{ name: 'Generate New', value: true }, { name: 'Use Existing', value: false }]
-    },
-    {
-      name: 'functionName',
-      message: 'Function name',
-      type: 'input',
-      default: (answers) => kebabCase(`${answers.path} ${answers.method}`),
-      when: (answers) => answers.createFunction === true
-    },
-    {
-      name: 'functionName',
-      message: 'Function name',
-      type: 'list',
-      choices: () => loadFuncs(),
-      when: (answers) => answers.createFunction === false
     }
   ])
+  .then((inputs) => merge({}, inputs, opts))
   .then(generateEndpoint)
 }
diff --git a/src/commands/generate/function.js b/src/commands/generate/function.js
index 63284e6..9284426 100644
--- a/src/commands/generate/function.js
+++ b/src/commands/generate/function.js
@@ -1,26 +1,27 @@
 import inquirer from 'inquirer'
-import generateFunction from '../../generate/function'
-import { merge } from 'lodash'
+import generateFunction from '../../generate-function'
+import merge from 'lodash.merge'
 
 const questions = [
   {
     name: 'name',
     type: 'input',
-    message: 'Function name',
+    message: 'Function name'
   }
 ]
 
 export const command = 'function [name]'
 export const desc = 'Generate a new function'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
+  .pkgConf('shep', process.cwd())
   .describe('name', 'Function name')
   .example('shep generate function', 'Launch an interactive CLI')
   .example('shep generate function foo', 'Genereate a new functon called "foo"')
 }
 
-export function handler(opts) {
-  inquirer.prompt(questions.filter((q)=> !opts[q.name] ))
-  .then((inputs) => merge({}, inputs, opts) )
+export function handler (opts) {
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
   .then(generateFunction)
 }
diff --git a/src/commands/generate/webpack.js b/src/commands/generate/webpack.js
new file mode 100644
index 0000000..8407ac2
--- /dev/null
+++ b/src/commands/generate/webpack.js
@@ -0,0 +1,15 @@
+import generateWebpack from '../../generate-webpack'
+
+export const command = 'webpack'
+export const desc = 'Generates a webpack.config.js with default template'
+export function builder (yargs) {
+  return yargs
+  .pkgConf('shep', process.cwd())
+  .describe('output', 'Output file')
+  .alias('output', 'o')
+  .default('output', 'webpack.config.js')
+  .example('shep generate webpack -o foo.js', 'Writes default webpack configuration to foo.js')
+}
+
+export const handler = generateWebpack
+
diff --git a/src/commands/new.js b/src/commands/new.js
index abd5c09..d93c0fd 100644
--- a/src/commands/new.js
+++ b/src/commands/new.js
@@ -1,6 +1,6 @@
 import _new from '../new'
 import inquirer from 'inquirer'
-import { merge } from 'lodash'
+import merge from 'lodash.merge'
 
 const questions = [
   {
@@ -13,7 +13,7 @@ const questions = [
 
 export const command = 'new [path]'
 export const desc = 'Create a new shep project'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
   .describe('path', 'Location to create the new shep project')
   .example('shep new', 'Launch an interactive CLI')
@@ -21,7 +21,7 @@ export function builder (yargs){
 }
 
 export function handler (opts) {
-  inquirer.prompt(questions.filter((q)=> !opts[q.name] ))
-  .then((inputs) => merge({}, inputs, opts) )
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
   .then(_new)
 }
diff --git a/src/commands/pull.js b/src/commands/pull.js
index 82a7381..dac9110 100644
--- a/src/commands/pull.js
+++ b/src/commands/pull.js
@@ -1,6 +1,6 @@
 export const command = 'pull'
 export const desc = 'Pulls a swagger JSON representation of an existing API and writes it to a local file'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
   .pkgConf('shep', process.cwd())
   .describe('region', 'AWS region')
@@ -20,5 +20,4 @@ export function builder (yargs){
   .example('shep pull --output other-path.json', 'Writes the JSON swagger file to `other-path.json`')
 }
 
-
 export { default as handler } from '../pull'
diff --git a/src/commands/push.js b/src/commands/push.js
index cfbd205..c956d40 100644
--- a/src/commands/push.js
+++ b/src/commands/push.js
@@ -1,6 +1,6 @@
 export const command = 'push'
 export const desc = 'Create a new shep project'
-export function builder(yargs){
+export function builder (yargs) {
   return yargs
   .pkgConf('shep', process.cwd())
   .describe('api-id', 'API Gateway resource id. Read from package.json if not provided')
diff --git a/src/commands/run.js b/src/commands/run.js
index ba94f6a..df895b2 100644
--- a/src/commands/run.js
+++ b/src/commands/run.js
@@ -1,15 +1,16 @@
 import inquirer from 'inquirer'
 import run from '../run'
-import loadFuncs from '../util/load-funcs'
-import { merge } from 'lodash'
+import * as load from '../util/load'
+import merge from 'lodash.merge'
 
 export const command = 'run [name]'
 export const desc = 'Run a function in your local environemnt'
-export function builder (yargs){
+export function builder (yargs) {
   return yargs
   .pkgConf('shep', process.cwd())
   .describe('environment', 'Environment variables to use')
   .describe('event', 'Event to use')
+  .describe('v', 'Responses from functions aren\'t truncated')
   .default('environment', 'development')
   .describe('build', 'Build functions before running. Use --no-build to skip this step')
   .default('build', true)
@@ -20,17 +21,17 @@ export function builder (yargs){
   .example('shep run foo --environment production', 'Runs the `foo` function with production environment')
 }
 
-export function handler(opts) {
+export function handler (opts) {
   const questions = [
     {
       name: 'name',
       message: 'Function',
       type: 'list',
-      choices: () => loadFuncs()
+      choices: () => load.funcs()
     }
   ]
 
-  inquirer.prompt(questions.filter((q)=> !opts[q.name] ))
-  .then((inputs) => merge({}, inputs, opts) )
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
   .then(run)
 }
diff --git a/src/deploy/index.js b/src/deploy/index.js
index 7217954..fc80841 100644
--- a/src/deploy/index.js
+++ b/src/deploy/index.js
@@ -1,90 +1,70 @@
-import Promise from 'bluebird'
-import loadFuncs from '../util/load-funcs'
-import build from '../util/build-function'
-import upload from '../util/upload-function'
-import { deployApi } from '../util/api-gateway'
-import { setPermission, setAlias } from '../util/lambda'
-import pushApi from '../push'
+import build from '../util/build-functions'
+import upload from '../util/upload-functions'
+import { deploy } from '../util/aws/api-gateway'
+import promoteAliases from '../util/promote-aliases'
+import setPermissions from '../util/set-permissions'
+import * as load from '../util/load'
+import push from '../util/push-api'
 import AWS from 'aws-sdk'
-import { queue, done } from '../util/tasks'
-import fs from 'fs-extra-promise'
+import listr from '../util/modules/listr'
 
-const pushApiTask = 'Upload api.json'
-const aliasesTask = 'Promote function aliases'
-const permissionsTask = 'Setup API Gateway <-> Lambda Permissions'
-const deployTask = 'Deploy API'
-
-export default function(opts){
-  const functions = loadFuncs(opts.functions)
-  const concurrency = opts.concurrency || Infinity
-  const env = opts.env
+export default function (opts) {
+  const functions = opts.functions || '*'
+  const env = opts.env || 'development'
   const region = opts.region
   const performBuild = opts.build
+  const api = load.api()
 
-  let api
-  try {
-    api = fs.readJSONSync(`api.json`)
-  } catch(e) {
-    console.log('No api.json file found. Skipping API deployment steps')
-  }
+  let apiId, uploadedFuncs
+
+  if (opts.apiId) { apiId = opts.apiId }
 
-  AWS.config.update({region: region})
+  AWS.config.update({region})
 
-  return Promise.resolve(functions)
-  .each(queue)
-  .tap(() => {
-    if (api) {
-      queue(pushApiTask)
-      queue(aliasesTask)
-      queue(permissionsTask)
-      queue(deployTask)
-    } else {
-      queue(aliasesTask)
+  const tasks = listr([
+    {
+      title: `Build Functions`,
+      task: () => build(functions, env),
+      skip: () => {
+        if (performBuild === false) {
+          return 'Called with --no-build'
+        }
+      }
+    },
+    {
+      title: 'Upload Functions to AWS',
+      task: () => upload(functions).tap((funcs) => { uploadedFuncs = funcs })
     }
-  })
-  .map((func) => buildAndUploadFunction(func).tap(() => done(func)), { concurrency })
-  .then((funcs) => api ? pushAndDeployApi(funcs) : promoteAliases(funcs) )
+  ], opts.quiet)
 
-  function promoteAliases(funcs){
-    return Promise.resolve(funcs)
-    .map((func) => setAlias(func, env) )
-    .tap(() => done(aliasesTask))
-  }
-  function buildAndUploadFunction(name) {
-    return Promise.resolve()
-    .then(() => { if (performBuild === true) return build(name, env) })
-    .then(() => upload(name) )
+  if (api) {
+    tasks.add([
+      {
+        title: 'Upload API.json',
+        task: () => push(api, apiId, region).tap((id) => { apiId = id })
+      }
+    ])
   }
 
-  function pushAndDeployApi(funcs){
-    return pushApi(opts)
-    .tap(() => done(pushApiTask))
-    .then((id) => {
-      return promoteAliases(funcs)
-      .then(() => setPermissions(api, id, env) )
-      .tap(() => done(permissionsTask))
-      .return(id)
-    })
-    .then((id) => deployApi(id, env))
-    .tap(() => done(deployTask))
-  }
-}
+  tasks.add([
+    {
+      title: 'Promote Function Aliases',
+      task: () => promoteAliases(uploadedFuncs, env)
+    }
+  ])
 
-function setPermissions(api, id, env){
-  let promises = []
-  for (var path in api.paths){
-    for (var method in api.paths[path]){
-      if (api.paths[path][method]['x-amazon-apigateway-integration'].type === 'aws') {
-        const uri = api.paths[path][method]['x-amazon-apigateway-integration'].uri.split(':')
-        promises.push(setPermission({
-          env,
-          region: uri[8],
-          accountId: uri[9],
-          apiId: id,
-          name: uri[11]
-        }))
+  if (api) {
+    tasks.add([
+      {
+        title: 'Setup Lambda Permissions',
+        task: () => setPermissions(api, apiId, env)
+      },
+      {
+        title: 'Deploy API',
+        task: () => deploy(apiId, env)
       }
-    }
+    ])
   }
-  return Promise.all(promises)
+
+  return tasks.run()
 }
diff --git a/src/generate-endpoint/index.js b/src/generate-endpoint/index.js
new file mode 100644
index 0000000..7c22469
--- /dev/null
+++ b/src/generate-endpoint/index.js
@@ -0,0 +1,62 @@
+import generateFunction from '../generate-function'
+import { writeJSON } from '../util/modules/fs'
+import { cors } from './templates'
+import genName from '../util/generate-name'
+import listr from '../util/modules/listr'
+import * as load from '../util/load'
+
+const integration = 'x-amazon-apigateway-integration'
+
+module.exports = function (opts) {
+  let accountId = opts.accountId
+  let path = opts.path
+  let method = opts.method
+
+  if (!accountId) {
+    throw new Error('Unable to determine your AWS Account ID. Please set it in the `shep` section of package.json')
+  }
+
+  const api = load.api()
+
+  const name = `${path} ${method}`
+  const { shortName, fullName } = genName(name)
+
+  const tasks = listr([
+    {
+      title: `Generate Function ${shortName}`,
+      task: () => generateFunction({ name, quiet: true })
+    },
+    {
+      title: 'Setup Endpoint',
+      task: () => addPath(api, path, method, accountId, fullName)
+    },
+    {
+      title: 'Setup CORS',
+      task: () => setupCORS(api, path)
+    },
+    {
+      title: 'Write api.json',
+      task: () => writeJSON('api.json', api, { spaces: 2 })
+    }
+  ], opts.quiet)
+
+  return tasks.run()
+}
+
+function addPath (api, path, method, accountId, functionName) {
+  if (method === 'any') { method = 'x-amazon-apigateway-any-method' }
+
+  api.paths[path] = api.paths[path] || {}
+  if (api.paths[path][method] !== undefined) { throw new Error(`Method '${method}' on path '${path}' already exists`) }
+  api.paths[path][method] = api.paths[path][method] || {}
+  api.paths[path][method][integration] = {
+    uri: `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:${accountId}:function:${functionName}:\${stageVariables.functionAlias}/invocations`,
+    passthroughBehavior: 'when_no_match',
+    httpMethod: 'POST',
+    type: 'aws_proxy'
+  }
+}
+
+function setupCORS (api, path) {
+  api.paths[path].options = api.paths[path].options || cors
+}
diff --git a/src/generate-endpoint/templates.js b/src/generate-endpoint/templates.js
new file mode 100644
index 0000000..70d7003
--- /dev/null
+++ b/src/generate-endpoint/templates.js
@@ -0,0 +1,35 @@
+export const cors = {
+  'responses': {
+    '200': {
+      'description': '200 response',
+      'headers': {
+        'Access-Control-Allow-Origin': {
+          'type': 'string'
+        },
+        'Access-Control-Allow-Methods': {
+          'type': 'string'
+        },
+        'Access-Control-Allow-Headers': {
+          'type': 'string'
+        }
+      }
+    }
+  },
+  'x-amazon-apigateway-integration': {
+    'responses': {
+      'default': {
+        'statusCode': '200',
+        'responseParameters': {
+          'method.response.header.Access-Control-Allow-Methods': "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
+          'method.response.header.Access-Control-Allow-Headers': "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'",
+          'method.response.header.Access-Control-Allow-Origin': "'*'"
+        }
+      }
+    },
+    'requestTemplates': {
+      'application/json': '{"statusCode": 200}'
+    },
+    'passthroughBehavior': 'when_no_match',
+    'type': 'mock'
+  }
+}
diff --git a/src/generate-function/index.js b/src/generate-function/index.js
new file mode 100644
index 0000000..70d31b3
--- /dev/null
+++ b/src/generate-function/index.js
@@ -0,0 +1,34 @@
+import { mkdir, writeFile } from '../util/modules/fs'
+import genName from '../util/generate-name'
+import * as templates from './templates'
+import listr from '../util/modules/listr'
+import Promise from 'bluebird'
+
+export default function (opts) {
+  const { shortName, fullName } = genName(opts.name)
+
+  const tasks = listr([
+    {
+      title: `Create functions/${shortName}/`,
+      task: () => mkdir(`./functions/${shortName}`)
+    },
+    {
+      title: `Create functions/${shortName}/events`,
+      task: () => mkdir(`./functions/${shortName}/events`)
+    },
+    {
+      title: 'Create files',
+      task: () => createFiles(shortName)
+    }
+  ], opts.quiet)
+
+  return tasks.run()
+
+  function createFiles () {
+    return Promise.all([
+      writeFile(`./functions/${shortName}/index.js`, templates.index()),
+      writeFile(`./functions/${shortName}/lambda.json`, templates.lambda(fullName)),
+      writeFile(`./functions/${shortName}/events/default.json`, templates.event())
+    ])
+  }
+}
diff --git a/src/generate-function/templates.js b/src/generate-function/templates.js
new file mode 100644
index 0000000..85fa10d
--- /dev/null
+++ b/src/generate-function/templates.js
@@ -0,0 +1,35 @@
+export function index () {
+  return `import config from 'shep-config'
+
+export function handler(event, context, callback) {
+  // Replace below with your own code!
+  console.log(event)
+  console.log(context)
+  console.log(config)
+
+  callback(null, { statusCode: 200, headers: {}, body: "success!" })
+}`
+}
+
+export function event () {
+  let obj = {
+    resource: '',
+    path: '',
+    httpMethod: 'GET',
+    headers: {},
+    queryStringParameters: {},
+    pathParameters: {},
+    stageVariables: {},
+    body: null
+  }
+
+  return JSON.stringify(obj, null, 2)
+}
+
+export function lambda (name) {
+  let obj = {
+    FunctionName: name
+  }
+
+  return JSON.stringify(obj, null, 2)
+}
diff --git a/src/generate-webpack/index.js b/src/generate-webpack/index.js
new file mode 100644
index 0000000..4390f37
--- /dev/null
+++ b/src/generate-webpack/index.js
@@ -0,0 +1,8 @@
+import { writeFile } from '../util/modules/fs'
+import { webpack } from '../new/templates'
+
+export default function run (opts) {
+  const output = opts.output
+
+  return writeFile(output, webpack())
+}
diff --git a/src/generate/endpoint/index.js b/src/generate/endpoint/index.js
deleted file mode 100644
index 8485a65..0000000
--- a/src/generate/endpoint/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { mapping } from './templates'
-import generateFunction from '../function'
-import fs from 'fs-extra-promise'
-import { set } from 'lodash'
-
-module.exports = function(opts){
-  const api = fs.readJSONSync('api.json')
-
-  if (opts.createFunction) { generateFunction(opts) }
-
-  set(api, ['paths', opts.path, opts.method], buildEndpoint(opts) )
-
-  return fs.writeJSONAsync('api.json', api)
-}
-
-function buildEndpoint(opts){
-  let endpoint = {}
-
-  endpoint.consumes = endpoint.consumes || []
-  endpoint.produces = endpoint.produces || []
-  endpoint.responses = endpoint.responses || {}
-
-  endpoint.consumes.push(opts.contentType)
-  endpoint.produces.push(opts.contentType)
-
-  endpoint.responses = endpoint.responses || {}
-
-  endpoint.responses[opts.responseCode] = { description: `${opts.responseCode} response` }
-
-  endpoint['x-amazon-apigateway-integration'] = {
-    responses : {
-      default: {
-        statusCode: opts.statusCode
-      }
-    },
-    requestTemplates : {
-      [opts.contentType]: mapping
-    },
-    uri : `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:{AWS_ACCOUNT_ID}:function:${opts.functionName}:\${stageVariables.functionAlias}/invocations`,
-    passthroughBehavior : 'when_no_match',
-    httpMethod : 'POST',
-    type : 'aws'
-  }
-
-  return endpoint
-}
diff --git a/src/generate/endpoint/templates.js b/src/generate/endpoint/templates.js
deleted file mode 100644
index 7fdb595..0000000
--- a/src/generate/endpoint/templates.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export function mapping(){
-  return `{
-    "headers": {
-    #foreach( $key in $input.params().header.keySet() )
-      "$key": "$util.escapeJavaScript($input.params().header.get($key)).replaceAll("\\'", "'")"#if( $foreach.hasNext ),#end
-    #end
-    },
-    "pathParameters": {
-    #foreach( $key in $input.params().path.keySet() )
-      "$key": "$util.escapeJavaScript($input.params().path.get($key)).replaceAll("\\'", "'")"#if( $foreach.hasNext ),#end
-    #end
-    },
-    "queryParameters": {
-    #foreach( $key in $input.params().querystring.keySet() )
-      "$key": "$util.escapeJavaScript($input.params().querystring.get($key)).replaceAll("\\'", "'")"#if( $foreach.hasNext ),#end
-    #end
-    },
-    "body": $input.json('$')
-  }`
-}
diff --git a/src/generate/function/index.js b/src/generate/function/index.js
deleted file mode 100644
index e11462f..0000000
--- a/src/generate/function/index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { all } from 'bluebird'
-import fs from 'fs-extra-promise'
-import * as templates from './templates'
-
-export default function(opts) {
-
-  const name = opts.name
-
-  return createDirs()
-  .then(createFiles)
-
-  function createDirs(){
-    return fs.ensureDirAsync(`./functions/${name}`)
-    .then(() => fs.ensureDirAsync(`./functions/${name}/events`) )
-  }
-
-  function createFiles(){
-    return all([
-      fs.writeFileAsync(`./functions/${name}/index.js`, templates.index()),
-      fs.writeFileAsync(`./functions/${name}/lambda.json`, templates.lambda(name)),
-      fs.writeFileAsync(`./functions/${name}/events/default.json`, templates.event())
-    ])
-  }
-}
diff --git a/src/generate/function/templates.js b/src/generate/function/templates.js
deleted file mode 100644
index 6a1c487..0000000
--- a/src/generate/function/templates.js
+++ /dev/null
@@ -1,31 +0,0 @@
-export function index(){
-  return `const config = require('shep-config')
-
-exports.handler = function(event, context, callback) {
-  // Replace below with your own code!
-  console.log(event)
-  console.log(context)
-  console.log(config)
-
-  callback(null, 'success!')
-}`
-}
-
-export function event(){
-  let obj = {
-    headers: {},
-    pathParameters: {},
-    queryParameters: {},
-    body: {}
-  }
-
-  return JSON.stringify(obj, null, 2)
-}
-
-export function lambda(name){
-  let obj = {
-    FunctionName: name
-  }
-
-  return JSON.stringify(obj, null, 2)
-}
diff --git a/src/generate/index.js b/src/generate/index.js
deleted file mode 100644
index 6a50400..0000000
--- a/src/generate/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import fn from './function'
-import endpoint from './endpoint'
-
-export {
-  fn as function,
-  endpoint
-}
diff --git a/src/index.js b/src/index.js
index 3f58e68..5007dda 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,3 +4,6 @@ export { default as push } from './push'
 export { default as run } from './run'
 export { default as deploy } from './deploy'
 export { default as build } from './build'
+export { default as generateFunction } from './generate-function'
+export { default as generateEndpoint } from './generate-endpoint'
+export { default as generateWebpack } from './generate-webpack'
diff --git a/src/new/index.js b/src/new/index.js
index 34f5b9d..cf0de56 100644
--- a/src/new/index.js
+++ b/src/new/index.js
@@ -1,37 +1,63 @@
-import Promise from 'bluebird'
-import fs from 'fs-extra-promise'
+import { mkdir, writeFile } from '../util/modules/fs'
 import * as templates from './templates'
+import Promise from 'bluebird'
+import exec from '../util/modules/exec'
+import listr from '../util/modules/listr'
 
-/**
- * @param opts Options for this command.
- * @param opts.path The path where the project should be generated.
- */
-export default function run(opts) {
+export default function run (opts) {
   const path = opts.path
 
-  return createFolders()
-  .then(createFiles)
+  const tasks = listr([
+    {
+      title: `Create ${path}/`,
+      task: () => mkdir(path)
+    },
+    {
+      title: 'Create Subdirectories',
+      task: () => createSubDirs(path)
+    },
+    {
+      title: 'Create Files',
+      task: () => createFiles(path)
+    },
+    {
+      title: 'Install Depedencies',
+      task: () => npmInstall(path)
+    },
+    {
+      title: 'Initialize Git',
+      task: () => initGit(path)
+    }
+  ], opts.quiet)
+
+  return tasks.run()
+}
+
+function createSubDirs (path) {
+  return Promise.all([
+    mkdir(path + '/functions'),
+    mkdir(path + '/config')
+  ])
+}
 
-  function createFolders(){
-    return Promise.all([
-      fs.ensureDirAsync(path + '/functions'),
-      fs.ensureDirAsync(path + '/config')
-    ])
-  }
+function createFiles (path) {
+  return Promise.all([
+    writeFile(path + '/package.json', templates.pkg(path)),
+    writeFile(path + '/config/development.js', templates.env('development')),
+    writeFile(path + '/config/beta.js', templates.env('beta')),
+    writeFile(path + '/config/production.js', templates.env('production')),
+    writeFile(path + '/.gitignore', templates.gitignore()),
+    writeFile(path + '/README.md', templates.readme(path)),
+    writeFile(path + '/lambda.json', templates.lambda()),
+    writeFile(path + '/api.json', templates.api(path)),
+    writeFile(path + '/webpack.config.js', templates.webpack())
+  ])
+}
 
-  function createFiles() {
-    let files = [
-      fs.outputFileAsync(path + '/package.json', templates.pkg(path, opts.api)),
-      fs.outputFileAsync(path + '/config/development.js', templates.env('development')),
-      fs.outputFileAsync(path + '/config/beta.js', templates.env('beta')),
-      fs.outputFileAsync(path + '/config/production.js', templates.env('production')),
-      fs.outputFileAsync(path + '/.gitignore', templates.gitignore()),
-      fs.outputFileAsync(path + '/README.md', templates.readme(path)),
-      fs.outputFileAsync(path + '/lambda.json', templates.lambda()),
-      fs.outputFileAsync(path + '/api.json', templates.api(path)),
-      fs.outputFileAsync(path + '/webpack.config.js', templates.webpack())
-    ]
+function npmInstall (path) {
+  return exec('npm', ['install'], { cwd: path })
+}
 
-    return Promise.all(files)
-  }
+function initGit (path) {
+  return exec('git', ['init'], { cwd: path })
 }
diff --git a/src/new/templates.js b/src/new/templates.js
index 4e31cdd..9db9c0a 100644
--- a/src/new/templates.js
+++ b/src/new/templates.js
@@ -1,4 +1,4 @@
-export function api(apiName)  {
+export function api (apiName) {
   return `{
   "swagger": "2.0",
   "info": {
@@ -9,74 +9,87 @@ export function api(apiName)  {
 }`
 }
 
-export function env(env)  {
+export function env (env) {
   return `module.exports = {
   env: "${env}",
   secretkey: "${env}-secret-key"
 }`
 }
 
-export function gitignore()  {
+export function gitignore () {
   return `dist/*
 node_modules/*
-functions/*/node_modules/*
 config/*`
 }
 
-export function lambda(){
+export function lambda () {
   let obj = {
-    Handler: "index.handler",
+    Handler: 'index.handler',
     MemorySize: 128,
-    Role: "",
+    Role: '',
     Timeout: 10,
-    Runtime: "nodejs4.3"
+    Runtime: 'nodejs4.3'
   }
 
   return JSON.stringify(obj, null, 2)
 }
 
-export function pkg(apiName, api)  {
+export function pkg (apiName) {
   let obj = {
     name: apiName,
-    version: "1.0.0",
+    version: '1.0.0',
     private: true,
-    description: "",
-    license: "",
-    dependencies: {},
-    shep: {}
+    description: '',
+    license: '',
+    devDependencies: {
+      webpack: '2.1.0-beta.25',
+      minimatch: '3.0.3'
+    },
+    shep: {
+      region: '',
+      accountId: '',
+      apiId: ''
+    }
   }
 
-  if (api === false) { obj.shep.api = false }
-
   return JSON.stringify(obj, null, 2)
 }
 
-export function readme(apiName){
+export function readme (apiName) {
   return `# ${apiName}`
 }
 
-export function webpack(){
-  return `const path = require('path')
+export function webpack () {
+  return `/*
+ * WARNING: Tampering with how entry is populated could affect pattern matching
+ */
 
-module.exports = function(name, env) {
-  return {
-    target: 'node',
-    entry: {
-      [name]: path.resolve(\`functions/\${name}/index.js\`)
-    },
-    output: {
-      path: path.resolve(\`dist/\${name}\`),
-      filename: 'index.js',
-      chunkFilename: '[name]/[id].js',
-      libraryTarget: 'commonjs2'
-    },
-    resolve: {
-      modules: [ 'node_modules', 'lib' ],
-      alias: { 'shep-config': path.resolve(\`config/\${env}.js\`) }
-    },
-    externals: { 'aws-sdk': 'aws-sdk' },
-    module: {
-      loaders: []
+const fs = require('fs')
+const path = require('path')
+const minimatch = require('minimatch')
+
+const env = process.env.NODE_ENV || 'development'
+const pattern = process.env.PATTERN || '*'
+
+const entry = fs.readdirSync('functions')
+.filter(minimatch.filter(pattern))
+.reduce((map, funcName) => {
+  map[funcName] = path.resolve(\`functions/\${funcName}/index\`)
+  return map
+}, {})
+
+module.exports = {
+  target: 'node',
+  entry,
+  output: {
+    path: 'dist',
+    filename: '[name]/index.js',
+    libraryTarget: 'commonjs2'
+  },
+  resolve: {
+    modules: [ 'node_modules', 'lib' ],
+    alias: {
+      'shep-config': path.resolve(\`config/\${env}.js\`)
     }
   }
 }
diff --git a/src/pull/index.js b/src/pull/index.js
index 9b308f2..bba4f97 100644
--- a/src/pull/index.js
+++ b/src/pull/index.js
@@ -1,23 +1,32 @@
-import fs from '../util/fs'
-import { getExport } from '../util/api-gateway'
-import { update } from '../util/shep-config'
+import { writeFile } from '../util/modules/fs'
+import { exportStage } from '../util/aws/api-gateway'
+import { update } from '../util/pkg-config'
 import AWS from 'aws-sdk'
+import listr from '../util/modules/listr'
+
+export default function (opts) {
+  const apiId = opts.apiId
+  const stage = opts.stage
+  const region = opts.region
 
-export default function(opts) {
   AWS.config.update({region: opts.region})
 
-  const params = {
-    restApiId: opts.apiId,
-    stageName: opts.stage,
-    exportType: 'swagger',
-    accepts: 'json',
-    parameters: {
-      extensions: 'integrations,authorizers'
+  let exportedApi
+
+  const tasks = listr([
+    {
+      title: `Export API currenly on ${stage}`,
+      task: () => {
+        return exportStage(apiId, stage)
+        .tap((api) => { exportedApi = api })
+        .tap(() => update({ apiId, region }))
+      }
+    },
+    {
+      title: 'Write to api.json',
+      task: () => writeFile('api.json', exportedApi, { spaces: 2 })
     }
-  }
+  ], opts.quiet)
 
-  return getExport(params)
-  .get('body')
-  .tap(() => update({ apiId: opts.apiId, region: opts.region }))
-  .then((api) => fs.writeFileAsync(opts.output || 'api.json', api))
+  return tasks.run()
 }
diff --git a/src/push/index.js b/src/push/index.js
index b78165b..aeb79e2 100644
--- a/src/push/index.js
+++ b/src/push/index.js
@@ -1,14 +1,21 @@
-import { pushApi } from '../util/api-gateway'
-import requireProject from '../util/require-project'
-import { update } from '../util/shep-config'
+import push from '../util/push-api'
+import * as load from '../util/load'
+import listr from '../util/modules/listr'
 import AWS from 'aws-sdk'
 
-export default function(opts){
+export default function (opts) {
   AWS.config.update({region: opts.region})
 
-  const api = requireProject(`api.json`)
+  const apiId = opts.apiId
+  const region = opts.region
+  const api = load.api()
 
-  return pushApi(api, opts.apiId)
-  .get('id')
-  .tap((id) => update({ apiId: id, region: opts.region }) )
+  const tasks = listr([
+    {
+      title: `Upload api.json to AWS`,
+      task: () => push(api, apiId, region)
+    }
+  ], opts.quiet)
+
+  return tasks.run()
 }
diff --git a/src/run/index.js b/src/run/index.js
index c82cabc..8bf6785 100644
--- a/src/run/index.js
+++ b/src/run/index.js
@@ -1,7 +1,6 @@
 import requireProject from '../util/require-project'
-import loadLambdaConfig from '../util/load-lambda-config'
-import loadEvents from '../util/load-events'
-import build from '../build'
+import * as load from '../util/load'
+import build from '../util/build-functions'
 import Promise from 'bluebird'
 import chalk from 'chalk'
 import AWS from 'aws-sdk'
@@ -9,30 +8,32 @@ import AWS from 'aws-sdk'
 import cliui from 'cliui'
 const ui = cliui({ width: 80 })
 
-const results = { success: 'SUCCESS' , error: 'ERROR', exception: 'EXCEPTION' }
+const results = { success: 'SUCCESS', error: 'ERROR', exception: 'EXCEPTION' }
 
 const awsNodeVersion = '4.3.2'
-const processVersion = process.versions.node
 
-export default function(opts){
+export default function (opts) {
   AWS.config.update({region: opts.region})
 
-  if (processVersion !== awsNodeVersion ){
+  const processVersion = process.versions.node
+
+  if (processVersion !== awsNodeVersion) {
     console.log(`Warning: Lambda currently runs node v${awsNodeVersion} but you are using v${processVersion}`)
   }
 
+  const verbose = opts.v
   const performBuild = opts.build
   const name = opts.name
   const env = opts.environemnt || 'development'
-  const lambdaConfig = loadLambdaConfig(name)
-  const events = loadEvents(name, opts.event)
+  const lambdaConfig = load.lambdaConfig(name)
+  const events = load.events(name, opts.event)
   const [ fileName, handler ] = lambdaConfig.Handler.split('.')
 
   const context = {}
 
   return Promise.resolve()
-  .then(() => { if (performBuild === true) return build({ functions: name, env: env }) })
-  .then(() => requireProject(`dist/${name}/${fileName}`)[handler] )
+  .then(() => { if (performBuild === true) return build(name, env) })
+  .then(() => requireProject(`dist/${name}/${fileName}`)[handler])
   .then((func) => {
     return Promise.map(events, (eventFilename) => {
       const event = requireProject(`functions/${name}/events/${eventFilename}`)
@@ -40,9 +41,9 @@ export default function(opts){
         let output = { name: eventFilename }
         output.start = new Date()
         try {
-          func(event, context, (err, res)=>{
+          func(event, context, (err, res) => {
             output.end = new Date()
-            if (err){
+            if (err) {
               output.result = results.error
               output.response = err
             } else {
@@ -51,7 +52,7 @@ export default function(opts){
             }
             resolve(output)
           })
-        } catch (e){
+        } catch (e) {
           output.end = new Date()
           output.result = results.exception
           output.response = e
@@ -61,60 +62,75 @@ export default function(opts){
     })
   })
   .tap(logOutput)
-  .map(formatOutput)
+  .map((out) => formatOutput(out, verbose))
   .then(() => console.log(ui.toString()))
 }
 
-function logOutput(outputs){
-  if(outputs.length === 1){
+function logOutput (outputs) {
+  if (outputs.length === 1) {
     console.log(outputs[0].response)
   }
 }
 
-function formatOutput(output){
-    ui.div(
-      {
-        text: output.name,
-        width: 20,
-      },
-      {
-        text: formatResult(output),
-        width: 15
-      },
-      {
-        text: formatDate(output),
-        width: 10
-      },
-      {
-        text: formatResponse(output)
-      }
+function formatOutput (output, verbose) {
+  ui.div(
+    {
+      text: output.name,
+      width: 20
+    },
+    {
+      text: formatResult(output),
+      width: 15
+    },
+    {
+      text: formatDate(output),
+      width: 10
+    },
+    {
+      text: (verbose ? splitAt(formatResponse(output), ',', 30) : formatResponse(output).slice(0, 30))
+    }
     )
 }
 
-function formatResponse({ result, response }){
-  if (response){
-    if (result === results.success ){
-      return JSON.stringify(response).trim(30)
+function formatResponse ({ result, response }) {
+  if (response) {
+    if (result === results.success) {
+      return JSON.stringify(response)
     } else if (result === results.error) {
-      return JSON.stringify(response).trim(30)
-    } else if (result === results.exception){
-      return `${response.name} ${response.message}`.trim(30)
+      return JSON.stringify(response)
+    } else if (result === results.exception) {
+      return `${response.name} ${response.message}`
     }
   } else {
-    return ""
+    return ''
   }
 }
 
-function formatDate({ start, end }){
-  return `${end-start}ms`
+function formatDate ({ start, end }) {
+  return `${end - start}ms`
 }
 
-function formatResult({ result }){
-  if (result === results.success ){
+function formatResult ({ result }) {
+  if (result === results.success) {
     return chalk.green(results.success)
   } else if (result === results.error) {
     return chalk.yellow(results.error)
-  } else if (result === results.exception){
+  } else if (result === results.exception) {
     return chalk.red(results.exception)
   }
 }
+
+function splitAt (str, token, width) {
+  return str.split(token)
+  .reduce((sum, curr) => {
+    let lastLine = sum.slice(-1)[0]
+
+    if (!lastLine || curr.length > width || lastLine.length + curr.length >= width) {
+      sum.push(curr)
+    } else {
+      sum[sum.length - 1] = [lastLine, curr].join(token)
+    }
+
+    return sum
+  }, [])
+}
diff --git a/src/util/api-gateway.js b/src/util/api-gateway.js
deleted file mode 100644
index bff8e06..0000000
--- a/src/util/api-gateway.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import AWS from 'aws-sdk'
-import Promise from 'bluebird'
-AWS.config.setPromisesDependency(Promise)
-
-export const createDeployment = promisify('createDeployment')
-export const getExport = promisify('getExport')
-export const importRestApi = promisify('importRestApi')
-export const putRestApi = promisify('putRestApi')
-
-function promisify(method){
-  return function(params){
-    const apiGateway = new AWS.APIGateway()
-    return apiGateway[method](params).promise()
-  }
-}
-
-export function deployApi(id, env){
-  const apiGateway = new AWS.APIGateway()
-  return apiGateway.createDeployment({restApiId: id, stageName: env, variables: { functionAlias: env }}).promise()
-}
-
-export function pushApi(api, id){
-  let params = {
-    body: JSON.stringify(api),
-    failOnWarnings: true
-  }
-
-  if (id){
-    params.mode = 'overwrite'
-    params.restApiId = id
-    return putRestApi(params)
-  } else {
-    return importRestApi(params)
-  }
-}
diff --git a/src/util/aws/api-gateway.js b/src/util/aws/api-gateway.js
new file mode 100644
index 0000000..6a1e80c
--- /dev/null
+++ b/src/util/aws/api-gateway.js
@@ -0,0 +1,39 @@
+import AWS from './'
+
+export function exportStage (restApiId, stageName) {
+  const apiGateway = new AWS.APIGateway()
+  const params = {
+    restApiId,
+    stageName,
+    exportType: 'swagger',
+    accepts: 'json',
+    parameters: {
+      extensions: 'integrations,authorizers'
+    }
+  }
+
+  return apiGateway.getExport(params).promise()
+  .get('body')
+}
+
+export function deploy (id, env) {
+  const apiGateway = new AWS.APIGateway()
+  return apiGateway.createDeployment({restApiId: id, stageName: env, variables: { functionAlias: env }}).promise()
+}
+
+export function pushApi (api, id) {
+  const apiGateway = new AWS.APIGateway()
+
+  let params = {
+    body: JSON.stringify(api),
+    failOnWarnings: true
+  }
+
+  if (id) {
+    params.mode = 'overwrite'
+    params.restApiId = id
+    return apiGateway.putRestApi(params).promise().get('id')
+  } else {
+    return apiGateway.importRestApi(params).promise().get('id')
+  }
+}
diff --git a/src/util/aws/index.js b/src/util/aws/index.js
new file mode 100644
index 0000000..55e299f
--- /dev/null
+++ b/src/util/aws/index.js
@@ -0,0 +1,6 @@
+import AWS from 'aws-sdk'
+import Promise from 'bluebird'
+
+AWS.config.setPromisesDependency(Promise)
+
+export default AWS
diff --git a/src/util/aws/lambda.js b/src/util/aws/lambda.js
new file mode 100644
index 0000000..649f500
--- /dev/null
+++ b/src/util/aws/lambda.js
@@ -0,0 +1,63 @@
+import AWS from './'
+import merge from 'lodash.merge'
+
+export function putFunction (config, ZipFile) {
+  const lambda = new AWS.Lambda()
+
+  validateConfig(config)
+
+  const FunctionName = config.FunctionName
+  const Publish = true
+
+  return lambda.getFunction({ FunctionName }).promise()
+  .then(() => lambda.updateFunctionCode({ ZipFile, FunctionName, Publish }).promise())
+  .then(() => lambda.updateFunctionConfiguration(config).promise())
+  .catch({ code: 'ResourceNotFoundException' }, () => {
+    const params = merge(config, { Publish, Code: { ZipFile } })
+    return lambda.createFunction(params).promise()
+  })
+}
+
+export function setAlias ({ Version, FunctionName }, Name) {
+  const lambda = new AWS.Lambda()
+
+  let params = {
+    FunctionName,
+    Name
+  }
+
+  return lambda.getAlias(params).promise()
+  .then(() => {
+    params.FunctionVersion = Version
+    return lambda.updateAlias(params).promise()
+  })
+  .catch({ code: 'ResourceNotFoundException' }, () => {
+    params.FunctionVersion = Version
+    return lambda.createAlias(params).promise()
+  })
+}
+
+export function setPermission ({ name, region, env, apiId, accountId }) {
+  const lambda = new AWS.Lambda()
+
+  let params = {
+    Action: 'lambda:InvokeFunction',
+    Qualifier: env,
+    FunctionName: name,
+    Principal: 'apigateway.amazonaws.com',
+    StatementId: `api-gateway-${apiId}`,
+    SourceArn: `arn:aws:execute-api:${region}:${accountId}:${apiId}/*`
+  }
+
+  return lambda.addPermission(params).promise()
+  .catch((err) => {
+    // Swallow errors if permission already exists
+    if (err.code !== 'ResourceConflictException') { throw err }
+  })
+}
+
+function validateConfig (config) {
+  if (!config.Role) {
+    throw new Error('You need to specify a valid Role for your lambda functions. See the shep README for details.')
+  }
+}
diff --git a/src/util/build-function.js b/src/util/build-function.js
deleted file mode 100644
index a11b330..0000000
--- a/src/util/build-function.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import webpack from 'webpack'
-import Promise from 'bluebird'
-import fs from 'fs-extra-promise'
-import requireProject from '../util/require-project'
-
-import { update } from './tasks'
-
-const wp = Promise.promisify(webpack)
-
-export default function(name, env){
-  const config = requireProject('./webpack.config.js')
-
-  return Promise.try(() => update(name, 'Cleaning files'))
-  .then(() => fs.removeAsync(`dist/${name}`)  )
-  .then(() => update(name, 'Running Webpack'))
-  .then(() => wp(config(name,env)) )
-  .then((stats) => {
-
-    // TODO Handle errors from webpack better
-    
-    const jsonStats = stats.toJson()
-    if(jsonStats.errors.length > 0) console.log(jsonStats.errors)
-    // if(jsonStats.warnings.length > 0) jsonStats.warnings.map(console.log); throw new Error('Webpack Warnings!')
-  })
-}
diff --git a/src/util/build-functions.js b/src/util/build-functions.js
new file mode 100644
index 0000000..f1d8e01
--- /dev/null
+++ b/src/util/build-functions.js
@@ -0,0 +1,5 @@
+import exec from './modules/exec'
+
+export default function (PATTERN, NODE_ENV) {
+  return exec('webpack', [], { env: { PATTERN, NODE_ENV } })
+}
diff --git a/src/util/exec.js b/src/util/exec.js
deleted file mode 100644
index ea953aa..0000000
--- a/src/util/exec.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { promisify } from 'bluebird'
-import { exec } from 'child_process'
-
-export default promisify(exec);
diff --git a/src/util/fs.js b/src/util/fs.js
deleted file mode 100644
index 0bb213a..0000000
--- a/src/util/fs.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import fs from 'fs-extra-promise'
-
-export default fs;
diff --git a/src/util/generate-name.js b/src/util/generate-name.js
new file mode 100644
index 0000000..dc5bec5
--- /dev/null
+++ b/src/util/generate-name.js
@@ -0,0 +1,9 @@
+import { pkg } from './load'
+import kebabCase from 'lodash.kebabcase'
+
+export default function (str) {
+  return {
+    shortName: kebabCase(str),
+    fullName: kebabCase(`${pkg().name} ${str}`)
+  }
+}
diff --git a/src/util/lambda.js b/src/util/lambda.js
deleted file mode 100644
index eb99d17..0000000
--- a/src/util/lambda.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import AWS from 'aws-sdk'
-import Promise from 'bluebird'
-AWS.config.setPromisesDependency(Promise)
-
-function promisify(method){
-  return function(params){
-    const lambda = new AWS.Lambda()
-    return lambda[method](params).promise()
-  }
-}
-
-export const getFunction = promisify('getFunction')
-export const createFunction = promisify('createFunction')
-export const updateFunctionCode = promisify('updateFunctionCode')
-export const updateFunctionConfiguration = promisify('updateFunctionConfiguration')
-export const addPermission = promisify('addPermission')
-export const getAlias = promisify('getAlias')
-export const createAlias = promisify('createAlias')
-export const updateAlias = promisify('updateAlias')
-
-export function setAlias(func, name){
-  let params = {
-    FunctionName: func.FunctionName,
-    Name: name
-  }
-
-  return getAlias(params)
-  .then(()=>{
-    params.FunctionVersion = func.Version
-    return updateAlias(params)
-  })
-  .catch({ code: 'ResourceNotFoundException' }, ()=>{
-    params.FunctionVersion = func.Version
-    return createAlias(params)
-  })
-}
-
-
-export function setPermission({ name, region, env, apiId, accountId }){
-
-  let params = {
-    Action: 'lambda:InvokeFunction',
-    Qualifier: env,
-    FunctionName: name,
-    Principal: 'apigateway.amazonaws.com',
-    StatementId: `api-gateway-${apiId}`,
-    SourceArn: `arn:aws:execute-api:${region}:${accountId}:${apiId}/*`
-  }
-
-  return addPermission(params)
-  .catch((err) => {
-    // Swallow errors if permission already exists
-    if (err.code !== 'ResourceConflictException'){ throw err }
-  })
-}
diff --git a/src/util/load-envs.js b/src/util/load-envs.js
deleted file mode 100644
index 55951c8..0000000
--- a/src/util/load-envs.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const glob = require('glob')
-
-module.exports = function(){
-  return glob.sync('config/*')
-  .map((path) => path.split('/').pop())
-  .map((file) => file.split('.').shift())
-}
diff --git a/src/util/load-events.js b/src/util/load-events.js
deleted file mode 100644
index 037109a..0000000
--- a/src/util/load-events.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import globby from 'globby'
-
-export default function(func, eventName){
-
-  const eventDir = `functions/${func}/events`
-  let events = globby.sync('*', { cwd: eventDir })
-  if (eventName){
-    events = events.filter((event) => event === `${eventName}.json`)
-    if (events.length === 0) {
-      throw new Error(`No event in '${eventDir}' called ${eventName}`)
-    }
-  }
-
-  return events
-}
diff --git a/src/util/load-funcs.js b/src/util/load-funcs.js
deleted file mode 100644
index cf7befc..0000000
--- a/src/util/load-funcs.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import globby from 'globby'
-
-export default function(inputs){
-  const patterns = inputs && inputs.length !== 0 ? inputs : ['*']
-  const funcs = globby.sync(patterns, { cwd: 'functions' })
-  if (funcs.length === 0) {
-    throw new Error(`No functions found matching patterns: ${JSON.stringify(funcs)}`)
-  } else {
-    return funcs
-  }
-}
diff --git a/src/util/load-lambda-config.js b/src/util/load-lambda-config.js
deleted file mode 100644
index 96330fb..0000000
--- a/src/util/load-lambda-config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import fs from 'fs-extra-promise'
-import { merge } from 'lodash'
-
-export default function(name){
-  const functionLambda = fs.readJsonSync(`functions/${name}/lambda.json`)
-  const projectLambda = fs.readJsonSync('lambda.json')
-
-  return merge({}, projectLambda, functionLambda)
-}
diff --git a/src/util/load.js b/src/util/load.js
new file mode 100644
index 0000000..45fd035
--- /dev/null
+++ b/src/util/load.js
@@ -0,0 +1,44 @@
+import { readdirSync, readJSONSync } from './modules/fs'
+import minimatch from 'minimatch'
+
+export function envs () {
+  return readdirSync('config')
+  .map((file) => file.split('.').shift())
+}
+
+export function events (func, eventName) {
+  const eventDir = `functions/${func}/events`
+  let events = readdirSync(`functions/${func}/events`)
+  if (eventName) {
+    events = events.filter((event) => event === `${eventName}.json`)
+    if (events.length === 0) {
+      throw new Error(`No event in '${eventDir}' called ${eventName}`)
+    }
+  }
+
+  return events
+}
+
+export function funcs (pattern = '*') {
+  const funcs = readdirSync('functions').filter(minimatch.filter(pattern))
+  if (funcs.length === 0) {
+    throw new Error(`No functions found matching patterns: ${JSON.stringify(funcs)}`)
+  } else {
+    return funcs
+  }
+}
+
+export function lambdaConfig (name) {
+  const functionConfig = readJSONSync(`functions/${name}/lambda.json`)
+  const projectConfig = readJSONSync(`lambda.json`)
+
+  return Object.assign(projectConfig, functionConfig)
+}
+
+export function pkg () {
+  return readJSONSync('package.json')
+}
+
+export function api () {
+  return readJSONSync('api.json')
+}
diff --git a/src/util/modules/exec.js b/src/util/modules/exec.js
new file mode 100644
index 0000000..7b2c886
--- /dev/null
+++ b/src/util/modules/exec.js
@@ -0,0 +1,2 @@
+import execa from 'execa'
+export default execa
diff --git a/src/util/modules/fs.js b/src/util/modules/fs.js
new file mode 100644
index 0000000..fc50e51
--- /dev/null
+++ b/src/util/modules/fs.js
@@ -0,0 +1,8 @@
+import fs from 'fs-extra-promise'
+
+export const mkdir = fs.mkdirAsync
+export const writeFile = fs.writeFileAsync
+export const writeJSON = fs.writeJSONAsync
+export const readJSON = fs.readJSONAsync
+export const readJSONSync = fs.readJSONSync
+export const readdirSync = fs.readdirSync
diff --git a/src/util/modules/listr.js b/src/util/modules/listr.js
new file mode 100644
index 0000000..5af2470
--- /dev/null
+++ b/src/util/modules/listr.js
@@ -0,0 +1,7 @@
+import Listr from 'listr'
+
+export default function listr (tasks, quiet, concurrent) {
+  const renderer = quiet ? 'silent' : 'default'
+
+  return new Listr(tasks, { renderer, concurrent })
+}
diff --git a/src/util/pkg-config.js b/src/util/pkg-config.js
new file mode 100644
index 0000000..fb94a7a
--- /dev/null
+++ b/src/util/pkg-config.js
@@ -0,0 +1,9 @@
+import { writeJSON } from './modules/fs'
+import * as load from './load'
+
+export function update (obj) {
+  let pkg = load.pkg()
+  if (!pkg.shep) { pkg.shep = {} }
+  Object.keys(obj).forEach((key) => { pkg.shep[key] = obj[key] })
+  return writeJSON('package.json', pkg, { spaces: 2 })
+}
diff --git a/src/util/promote-aliases.js b/src/util/promote-aliases.js
new file mode 100644
index 0000000..45afbf0
--- /dev/null
+++ b/src/util/promote-aliases.js
@@ -0,0 +1,7 @@
+import { setAlias } from './aws/lambda'
+import Promise from 'bluebird'
+
+export default function (funcs, env) {
+  return Promise.resolve(funcs)
+  .map((func) => setAlias(func, env))
+}
diff --git a/src/util/push-api.js b/src/util/push-api.js
new file mode 100644
index 0000000..a23a3b1
--- /dev/null
+++ b/src/util/push-api.js
@@ -0,0 +1,7 @@
+import { pushApi } from './aws/api-gateway'
+import { update } from './pkg-config'
+
+export default function (api, apiId, region) {
+  return pushApi(api, apiId)
+  .tap((id) => update({ apiId: id, region }))
+}
diff --git a/src/util/require-project.js b/src/util/require-project.js
index c204bb5..a95215e 100644
--- a/src/util/require-project.js
+++ b/src/util/require-project.js
@@ -1,5 +1,5 @@
-const path = require('path')
+import path from 'path'
 
-module.exports = function(projectPath){
+export default function (projectPath) {
   return require(path.join(process.cwd(), projectPath))
 }
diff --git a/src/util/set-permissions.js b/src/util/set-permissions.js
new file mode 100644
index 0000000..01277fa
--- /dev/null
+++ b/src/util/set-permissions.js
@@ -0,0 +1,23 @@
+import { setPermission } from './aws/lambda'
+import Promise from 'bluebird'
+
+export default function (api, id, env) {
+  let promises = []
+  Object.keys(api.paths).map((path) => {
+    Object.keys(api.paths[path]).map((method) => {
+      const uri = api.paths[path][method]['x-amazon-apigateway-integration'].uri
+      if (uri && uri.includes('aws:lambda')) {
+        const uriParts = uri.split(':')
+        promises.push(setPermission({
+          env,
+          region: uriParts[8],
+          accountId: uriParts[9],
+          apiId: id,
+          name: uriParts[11]
+        }))
+      }
+    })
+  })
+
+  return Promise.all(promises)
+}
diff --git a/src/util/shep-config.js b/src/util/shep-config.js
deleted file mode 100644
index 05a958c..0000000
--- a/src/util/shep-config.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const fs = require('./fs')
-const { forEach, set } = require('lodash')
-const path = require('path')
-const Promise = require('bluebird')
-
-module.exports.load = function(){
-  let pkg = fs.readJSONSync(path.join( process.cwd(), 'package.json'))
-  return pkg.shep || {}
-}
-
-module.exports.update = function(obj){
-  try {
-    let pkg = fs.readJSONSync(path.join( process.cwd(), 'package.json'))
-    forEach(obj, (val, key) => set(pkg, `shep.${key}`, val))
-    return fs.writeJSONAsync(path.join( process.cwd(), 'package.json'), pkg)
-  } catch (e) {
-    return Promise.resolve()
-  }
-}
diff --git a/src/util/tasks.js b/src/util/tasks.js
deleted file mode 100644
index 587e336..0000000
--- a/src/util/tasks.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import observatory from 'observatory'
-import chalk from 'chalk'
-
-let tasks = {}
-
-export function queue(name){
-  if (tasks[name]){
-    return tasks[name]
-  } else {
-    tasks[name] = observatory.add(name).status(chalk.grey('Queued'))
-    return tasks[name]
-  }
-}
-
-
-export function done(name){
-  if (tasks[name]){
-    tasks[name].done()
-  } else {
-    throw new Error(`No task named ${name} has been started`)
-  }
-}
-
-export function update(name, text){
-  if (tasks[name]){
-    tasks[name].status(text)
-  } else {
-    throw new Error(`No task named ${name} has been started`)
-  }
-}
-
-export function details(name, text){
-  if (tasks[name]){
-    tasks[name].details(text)
-  } else {
-    throw new Error(`No task named ${name} has been started`)
-  }
-}
diff --git a/src/util/upload-function.js b/src/util/upload-function.js
deleted file mode 100644
index b348dc8..0000000
--- a/src/util/upload-function.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import Promise from 'bluebird'
-import { clone } from 'lodash'
-import { getFunction, updateFunctionCode, createFunction, updateFunctionConfiguration } from '../util/lambda'
-import zipDir from '../util/zip-dir'
-import { update } from './tasks'
-import loadLambdaConfig from '../util/load-lambda-config'
-
-export default function(name){
-  const lambdaConfig = loadLambdaConfig(name)
-
-  update(name, 'Zipping files')
-
-  return zipDir(`dist/${name}`)
-  .tap(() => update(name, 'Uploading to AWS') )
-  .then(upload)
-
-  function upload(zip){
-    return get()
-    .then(update)
-    .catch({ code: 'ResourceNotFoundException' }, create)
-
-    function create() {
-      var params = clone(lambdaConfig)
-
-      params.Code = { ZipFile: zip }
-      params.Publish = true
-
-      return createFunction(params)
-    }
-
-    function get(){
-      return getFunction({FunctionName: lambdaConfig.FunctionName})
-      .catch((err)=> {
-        if (err.code === 'ResourceNotFoundException'){
-          return Promise.resolve()
-        } else {
-          return Promise.reject(err)
-        }
-      })
-    }
-
-    function update() {
-      return updateCode().tap(updateConfig)
-    }
-
-    function updateCode() {
-      var params = { ZipFile: zip, FunctionName: lambdaConfig.FunctionName, Publish: true }
-      return updateFunctionCode(params)
-    }
-
-    function updateConfig() {
-      var params = clone(lambdaConfig)
-      params.FunctionName = lambdaConfig.FunctionName
-      return updateFunctionConfiguration(params)
-    }
-  }
-}
diff --git a/src/util/upload-functions.js b/src/util/upload-functions.js
new file mode 100644
index 0000000..4fe0954
--- /dev/null
+++ b/src/util/upload-functions.js
@@ -0,0 +1,15 @@
+import Promise from 'bluebird'
+import zipDir from './zip-dir'
+import { putFunction } from './aws/lambda'
+import { lambdaConfig, funcs } from './load'
+
+export default function (pattern) {
+  return Promise.resolve(funcs(pattern))
+  .map((func) => {
+    return Promise.join(
+      lambdaConfig(func),
+      zipDir(`dist/${func}`),
+      putFunction
+    )
+  })
+}
diff --git a/src/util/zip-dir.js b/src/util/zip-dir.js
index 5ea595e..8e22675 100644
--- a/src/util/zip-dir.js
+++ b/src/util/zip-dir.js
@@ -2,13 +2,13 @@ import archiver from 'archiver'
 import streamBuffers from 'stream-buffers'
 import Promise from 'bluebird'
 
-export default function(path){
-  return new Promise((resolve, reject)=>{
+export default function (path) {
+  return new Promise((resolve, reject) => {
     const archive = archiver.create('zip')
     let output = new streamBuffers.WritableStreamBuffer()
 
-    output.on('finish', function() { resolve(output.getContents()) })
-    archive.on('error', function(err) { reject(err) })
+    output.on('finish', function () { resolve(output.getContents()) })
+    archive.on('error', function (err) { reject(err) })
     archive.pipe(output)
     archive.directory(path, '/').finalize()
   })


Test patch:
diff --git a/test/build/index.js b/test/build/index.js
index a173f29..ae97167 100644
--- a/test/build/index.js
+++ b/test/build/index.js
@@ -1,24 +1,9 @@
 import test from 'ava'
-import { create } from '../helpers/fixture'
-import build from '../../src/build'
-import generateFunction from '../../src/generate/function'
-import dirExists from '../helpers/dir-exists'
-import fileExists from '../helpers/file-exists'
-
-// Supress observetory output
-require('observatory').settings({ write: () => {} })
-
-const env = 'development'
+import { didExec } from '../helpers/exec'
 
 test.before(() => {
-  return create('build')
-  .then(() => generateFunction({ name: 'foo' }) )
-  .then(() => build({env}) )
+  const shep = require('../../src')
+  return shep.build({ quiet: true })
 })
 
-test(() => dirExists('dist/foo') )
-test(() => fileExists('dist/foo/index.js') )
-
-test('Throws error with no matches', (t) => {
-  t.throws(() => build({ env, functions: ['not-found'] }), /No functions found matching patterns/ )
-})
+test(didExec, 'webpack')
diff --git a/test/cli.js b/test/cli.js
new file mode 100644
index 0000000..84ab51a
--- /dev/null
+++ b/test/cli.js
@@ -0,0 +1,6 @@
+import test from 'ava'
+import execa from 'execa'
+
+test('it runs', () => {
+  return execa('../cli.js', ['--help'])
+})
diff --git a/test/deploy/exec-no-api.js.old b/test/deploy/exec-no-api.js.old
deleted file mode 100644
index 3030a64..0000000
--- a/test/deploy/exec-no-api.js.old
+++ /dev/null
@@ -1,44 +0,0 @@
-// import test from 'ava'
-//
-// const td = require('testdouble')
-// const Promise = require('bluebird')
-//
-// let apiGateway, lambda, deployFunction
-//
-// test.before(()=> {
-//   process.chdir('../fixtures/test-api')
-//
-//   apiGateway = td.replace('../../src/util/api-gateway')
-//   lambda = td.replace('../../src/util/lambda')
-//   deployFunction = td.replace('../../src/deploy-function/exec')
-//
-//   td.when(lambda.publishVersion(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-//   td.when(lambda.updateAlias(td.matchers.isA(Object))).thenReturn(Promise.resolve({ AliasArn: 'arn:aws:lambda:us-east-1:654843952338:function:graph-api-graph-ql:'}))
-//   td.when(lambda.getAlias(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-//   td.when(deployFunction(td.matchers.isA(Object)), { ignoreExtraArgs: true }).thenReturn(Promise.resolve({ FunctionName: 'test' }))
-//
-//   const deploy = require('../../src/deploy/exec')
-//
-//   return deploy({ region: 'us-east-1', api: false })
-// })
-//
-// test('Deploys functions', () => {
-//   td.verify(deployFunction(td.matchers.isA(Object)), { times: 2, ignoreExtraArgs: true })
-// })
-//
-// test('Publishes functions', ()=> {
-//   td.verify(lambda.publishVersion(td.matchers.isA(Object)), { times: 2 })
-// })
-//
-// test('Sets function alias', ()=> {
-//   td.verify(lambda.getAlias(td.matchers.isA(Object)), { times: 2 })
-//   td.verify(lambda.updateAlias(td.matchers.isA(Object)), { times: 2 })
-// })
-//
-// test('Does not set permissions', ()=> {
-//   td.verify(lambda.addPermission(), { times: 0, ignoreExtraArgs: true })
-// })
-//
-// test('Does not create new API deployment', ()=> {
-//   td.verify(apiGateway.createDeployment(), { times: 0, ignoreExtraArgs: true })
-// })
diff --git a/test/deploy/exec.js.old b/test/deploy/exec.js.old
deleted file mode 100644
index d54ea3a..0000000
--- a/test/deploy/exec.js.old
+++ /dev/null
@@ -1,46 +0,0 @@
-import test from 'ava'
-
-const td = require('testdouble')
-const Promise = require('bluebird')
-
-let apiGateway, lambda, deployFunction
-
-test.before(()=> {
-  process.chdir('../fixtures/test-api')
-
-  apiGateway = td.replace('../../src/util/api-gateway')
-  lambda = td.replace('../../src/util/lambda')
-  deployFunction = td.replace('../../src/deploy-function/exec')
-
-  td.when(apiGateway.createDeployment(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-  td.when(lambda.publishVersion(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-  td.when(lambda.addPermission(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-  td.when(lambda.updateAlias(td.matchers.isA(Object))).thenReturn(Promise.resolve({ AliasArn: 'arn:aws:lambda:us-east-1:654843952338:function:graph-api-graph-ql:'}))
-  td.when(lambda.getAlias(td.matchers.isA(Object))).thenReturn(Promise.resolve({}))
-  td.when(deployFunction(td.matchers.isA(Object)), { ignoreExtraArgs: true }).thenReturn(Promise.resolve({ FunctionName: 'test' }))
-
-  const deploy = require('../../src/deploy/exec')
-
-  return deploy({ region: 'us-east-1'})
-})
-
-test('Deploys functions', () => {
-  td.verify(deployFunction(td.matchers.isA(Object)), { times: 2, ignoreExtraArgs: true })
-})
-
-test('Publishes functions', ()=> {
-  td.verify(lambda.publishVersion(td.matchers.isA(Object)), { times: 2 })
-})
-
-test('Sets function alias', ()=> {
-  td.verify(lambda.getAlias(td.matchers.isA(Object)), { times: 2 })
-  td.verify(lambda.updateAlias(td.matchers.isA(Object)), { times: 2 })
-})
-
-test('Sets permissions', ()=> {
-  td.verify(lambda.addPermission(td.matchers.isA(Object)), { times: 2 })
-})
-
-test('Creates new API deployment', ()=> {
-  td.verify(apiGateway.createDeployment(td.matchers.isA(Object)))
-})
diff --git a/test/deploy/index-no-api.js b/test/deploy/index-no-api.js
new file mode 100644
index 0000000..4dfb5c5
--- /dev/null
+++ b/test/deploy/index-no-api.js
@@ -0,0 +1,43 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const functions = 'foo-*'
+const uploadedFuncs = ['foo', 'bar']
+const env = 'beta'
+
+const build = td.replace('../../src/util/build-functions')
+const apiGateway = td.replace('../../src/util/aws/api-gateway')
+const promoteAliases = td.replace('../../src/util/promote-aliases')
+const setPermissions = td.replace('../../src/util/set-permissions')
+const push = td.replace('../../src/util/push-api')
+
+const load = td.replace('../../src/util/load')
+td.when(load.api()).thenReturn()
+
+const upload = td.replace('../../src/util/upload-functions')
+td.when(upload(functions)).thenResolve(uploadedFuncs)
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.deploy({ env, functions, quiet: true })
+})
+
+test('Builds functions', () => {
+  td.verify(build(functions, env))
+})
+
+test('Does not deploy API', () => {
+  td.verify(apiGateway.deploy(), { times: 0, ignoreExtraArgs: true })
+})
+
+test('Does not push API defenitin', () => {
+  td.verify(push(), { times: 0, ignoreExtraArgs: true })
+})
+
+test('Does promote function aliases', () => {
+  td.verify(promoteAliases(uploadedFuncs, env))
+})
+
+test('Does not setup function permissions', () => {
+  td.verify(setPermissions(), { times: 0, ignoreExtraArgs: true })
+})
diff --git a/test/deploy/index-no-build.js b/test/deploy/index-no-build.js
new file mode 100644
index 0000000..b8983fc
--- /dev/null
+++ b/test/deploy/index-no-build.js
@@ -0,0 +1,43 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const functions = 'foo-*'
+const uploadedFuncs = ['foo', 'bar']
+const env = 'beta'
+const api = { paths: {} }
+const apiId = 'test-id'
+
+const build = td.replace('../../src/util/build-functions')
+const apiGateway = td.replace('../../src/util/aws/api-gateway')
+const promoteAliases = td.replace('../../src/util/promote-aliases')
+const setPermissions = td.replace('../../src/util/set-permissions')
+
+const load = td.replace('../../src/util/load')
+td.when(load.api()).thenReturn(api)
+
+const push = td.replace('../../src/util/push-api')
+td.when(push(api), { ignoreExtraArgs: true }).thenResolve(apiId)
+
+const upload = td.replace('../../src/util/upload-functions')
+td.when(upload(functions)).thenResolve(uploadedFuncs)
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.deploy({ build: false, env, functions, quiet: true })
+})
+
+test('Builds functions', () => {
+  td.verify(build(), { times: 0, ignoreExtraArgs: true })
+})
+
+test('Deploys API', () => {
+  td.verify(apiGateway.deploy(apiId, env))
+})
+
+test('Promote function aliases', () => {
+  td.verify(promoteAliases(uploadedFuncs, env))
+})
+
+test('Setup function permissions', () => {
+  td.verify(setPermissions(api, apiId, env))
+})
diff --git a/test/deploy/index.js b/test/deploy/index.js
new file mode 100644
index 0000000..092ee26
--- /dev/null
+++ b/test/deploy/index.js
@@ -0,0 +1,43 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const functions = 'foo-*'
+const uploadedFuncs = ['foo', 'bar']
+const env = 'beta'
+const api = { paths: {} }
+const apiId = 'test-id'
+
+const build = td.replace('../../src/util/build-functions')
+const apiGateway = td.replace('../../src/util/aws/api-gateway')
+const promoteAliases = td.replace('../../src/util/promote-aliases')
+const setPermissions = td.replace('../../src/util/set-permissions')
+
+const load = td.replace('../../src/util/load')
+td.when(load.api()).thenReturn(api)
+
+const push = td.replace('../../src/util/push-api')
+td.when(push(api), { ignoreExtraArgs: true }).thenResolve(apiId)
+
+const upload = td.replace('../../src/util/upload-functions')
+td.when(upload(functions)).thenResolve(uploadedFuncs)
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.deploy({ env, functions, quiet: true })
+})
+
+test('Builds functions', () => {
+  td.verify(build(functions, env))
+})
+
+test('Deploys API', () => {
+  td.verify(apiGateway.deploy(apiId, env))
+})
+
+test('Promote function aliases', () => {
+  td.verify(promoteAliases(uploadedFuncs, env))
+})
+
+test('Setup function permissions', () => {
+  td.verify(setPermissions(api, apiId, env))
+})
diff --git a/test/fixtures/config/beta.js b/test/fixtures/config/beta.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/config/prod.js b/test/fixtures/config/prod.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/functions/bar/.gitkeep b/test/fixtures/functions/bar/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/functions/foo/events/custom.json b/test/fixtures/functions/foo/events/custom.json
new file mode 100644
index 0000000..1797133
--- /dev/null
+++ b/test/fixtures/functions/foo/events/custom.json
@@ -0,0 +1,3 @@
+{
+  
+}
diff --git a/test/fixtures/functions/foo/events/default.json b/test/fixtures/functions/foo/events/default.json
new file mode 100644
index 0000000..1797133
--- /dev/null
+++ b/test/fixtures/functions/foo/events/default.json
@@ -0,0 +1,3 @@
+{
+  
+}
diff --git a/test/fixtures/functions/foo/lambda.json b/test/fixtures/functions/foo/lambda.json
new file mode 100644
index 0000000..5b0083a
--- /dev/null
+++ b/test/fixtures/functions/foo/lambda.json
@@ -0,0 +1,4 @@
+{
+  "Name": "foo",
+  "Memory": 5
+}
diff --git a/test/fixtures/lambda.json b/test/fixtures/lambda.json
new file mode 100644
index 0000000..eb3cf68
--- /dev/null
+++ b/test/fixtures/lambda.json
@@ -0,0 +1,4 @@
+{
+  "Role": "admin",
+  "Memory": 100
+}
diff --git a/test/generate-endpoint/index.js b/test/generate-endpoint/index.js
new file mode 100644
index 0000000..3a36511
--- /dev/null
+++ b/test/generate-endpoint/index.js
@@ -0,0 +1,26 @@
+import test from 'ava'
+import { fs } from '../helpers/fs'
+import td from '../helpers/testdouble'
+
+const generateFunction = td.replace('../../src/generate-function')
+const load = td.replace('../../src/util/load')
+
+const path = '/foo'
+const method = 'get'
+const accountId = 'testid'
+
+td.when(fs.readJSONSync('package.json')).thenReturn({ name: 'bar' })
+td.when(load.api()).thenReturn({ paths: {} })
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.generateEndpoint({ accountId, path, method, quiet: true })
+})
+
+test('Writes a new api.json', () => {
+  td.verify(fs.writeJSON('api.json'), { ignoreExtraArgs: true })
+})
+
+test('Generates a new function', () => {
+  td.verify(generateFunction(td.matchers.contains({ name: '/foo get' })))
+})
diff --git a/test/generate-function/index.js b/test/generate-function/index.js
new file mode 100644
index 0000000..8baac4a
--- /dev/null
+++ b/test/generate-function/index.js
@@ -0,0 +1,18 @@
+import test from 'ava'
+import { createdDir, wroteFile, fs } from '../helpers/fs'
+import td from '../helpers/testdouble'
+
+const name = 'foo-func'
+
+td.when(fs.readJSONSync('package.json')).thenReturn({ name: 'bar' })
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.generateFunction({ name, quiet: true })
+})
+
+test(createdDir, `functions/${name}`)
+test(createdDir, `functions/${name}/events`)
+test(wroteFile, `functions/${name}/events/default.json`)
+test(wroteFile, `functions/${name}/index.js`)
+test(wroteFile, `functions/${name}/lambda.json`)
diff --git a/test/generate-webpack/index.js b/test/generate-webpack/index.js
new file mode 100644
index 0000000..190aec2
--- /dev/null
+++ b/test/generate-webpack/index.js
@@ -0,0 +1,12 @@
+import test from 'ava'
+import { wroteFile } from '../helpers/fs'
+import generateWebpack from '../../src/generate-webpack'
+
+const output = 'foo'
+
+test.before(() => {
+  return generateWebpack({ output })
+})
+
+test(wroteFile, output)
+
diff --git a/test/generate/function/index.js b/test/generate/function/index.js
deleted file mode 100644
index c736521..0000000
--- a/test/generate/function/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import test from 'ava'
-import { create } from '../../helpers/fixture'
-import dirExists from '../../helpers/dir-exists'
-import fileExists from '../../helpers/file-exists'
-
-const name = 'foo'
-
-test.before(() => {
-  return create('generate-function')
-  .then(() => {
-    const generateFunction  = require('../../../src/generate/function')
-    return generateFunction({ name })
-  })
-})
-
-test(() => dirExists(`functions/${name}`))
-test(() => dirExists(`functions/${name}/events`))
-test(() => fileExists(`functions/${name}/events/default.json`))
-test(() => fileExists(`functions/${name}/index.js`))
-test(() => fileExists(`functions/${name}/lambda.json`))
diff --git a/test/helpers/dir-exists.js b/test/helpers/dir-exists.js
deleted file mode 100644
index ff991af..0000000
--- a/test/helpers/dir-exists.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import fs from 'fs-extra-promise'
-
-export default function dirExists(path){
-  return fs.statAsync(path).then((stat) => stat.isDirectory())
-}
diff --git a/test/helpers/exec.js b/test/helpers/exec.js
new file mode 100644
index 0000000..06b9778
--- /dev/null
+++ b/test/helpers/exec.js
@@ -0,0 +1,12 @@
+import td from 'testdouble'
+
+const exec = td.replace('../../src/util/modules/exec')
+
+function didExec (t, cmd, args, options = {}) {
+  const secondArg = args ? td.matchers.contains(args) : []
+  td.verify(exec(td.matchers.contains(cmd), secondArg, td.matchers.contains(options)))
+}
+
+didExec.title = (providedTitle, cmd, subcmd) => `Executed ${cmd} ${subcmd || ''}`
+
+export { exec, didExec }
diff --git a/test/helpers/file-exists.js b/test/helpers/file-exists.js
deleted file mode 100644
index 2884a46..0000000
--- a/test/helpers/file-exists.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import fs from 'fs-extra-promise'
-
-export default function fileExists(path){
-  return fs.statAsync(path).then((stat) => stat.isFile())
-}
diff --git a/test/helpers/fixture.js b/test/helpers/fixture.js
deleted file mode 100644
index ca583f1..0000000
--- a/test/helpers/fixture.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as shep from '../../src'
-import path from 'path'
-import fs from 'fs-extra-promise'
-
-export function create(name){
-  const p = path.join(__dirname, `../../tmp/tests/${name}`)
-  fs.removeSync(p)
-  return shep.new({ path: p }).then(() => process.chdir(p) ).return(p)
-}
diff --git a/test/helpers/fs.js b/test/helpers/fs.js
new file mode 100644
index 0000000..745fe05
--- /dev/null
+++ b/test/helpers/fs.js
@@ -0,0 +1,15 @@
+import td from 'testdouble'
+
+const fs = td.replace('../../src/util/modules/fs')
+
+function createdDir (t, dirName) {
+  td.verify(fs.mkdir(td.matchers.contains(dirName)))
+}
+createdDir.title = (providedTitle, dirName) => `Created dir ${dirName}`
+
+function wroteFile (t, fileName) {
+  td.verify(fs.writeFile(td.matchers.contains(fileName), td.matchers.isA(String)))
+}
+wroteFile.title = (providedTitle, fileName) => `Created file ${fileName}`
+
+export { fs, wroteFile, createdDir }
diff --git a/test/helpers/testdouble.js b/test/helpers/testdouble.js
new file mode 100644
index 0000000..1c8dc27
--- /dev/null
+++ b/test/helpers/testdouble.js
@@ -0,0 +1,6 @@
+import td from 'testdouble'
+import Promise from 'bluebird'
+
+td.config({ promiseConstructor: Promise })
+
+export default td
diff --git a/test/new/index.js b/test/new/index.js
index 954a300..a3e6233 100644
--- a/test/new/index.js
+++ b/test/new/index.js
@@ -1,15 +1,24 @@
 import test from 'ava'
-import { create } from '../helpers/fixture'
-import dirExists from '../helpers/dir-exists'
-import fileExists from '../helpers/file-exists'
-
-test.before(() => create('new'))
-
-test(() => dirExists('functions'))
-test(() => dirExists('config'))
-test(() => fileExists('lambda.json'))
-test(() => fileExists('api.json'))
-test(() => fileExists('package.json'))
-test(() => fileExists('.gitignore'))
-test(() => fileExists('README.md'))
-test(() => fileExists('webpack.config.js'))
+import { createdDir, wroteFile } from '../helpers/fs'
+import { didExec } from '../helpers/exec'
+
+const path = 'foo-api'
+
+test.before(() => {
+  const shep = require('../../src/index')
+  return shep.new({ path, quiet: true })
+})
+
+test(createdDir, `${path}/functions`)
+test(createdDir, `${path}/functions`)
+test(createdDir, `${path}/config`)
+
+test(wroteFile, `${path}/lambda.json`)
+test(wroteFile, `${path}/api.json`)
+test(wroteFile, `${path}/package.json`)
+test(wroteFile, `${path}/.gitignore`)
+test(wroteFile, `${path}/README.md`)
+test(wroteFile, `${path}/webpack.config.js`)
+
+test(didExec, 'npm', 'install')
+test(didExec, 'git', 'init')
diff --git a/test/pull/index.js b/test/pull/index.js
index ec459b4..3e3453e 100644
--- a/test/pull/index.js
+++ b/test/pull/index.js
@@ -1,39 +1,26 @@
 import test from 'ava'
-import { create } from '../helpers/fixture'
-import td from 'testdouble'
-import Promise from 'bluebird'
-import fs from 'fs-extra-promise'
+import td from '../helpers/testdouble'
 
-td.config({ promiseConstructor: Promise })
-
-let apiGateway
-
-const response = { body: '{ "foo": "bar" }' }
+const response = '{ "foo": "bar" }'
 const apiId = 'test'
-const region = 'east'
 const stage = 'prod'
+const region = 'east'
 
-const apiGatewayOpts = { restApiId: apiId, stageName: stage }
-
-test.before(() => {
-  apiGateway = td.replace('../../src/util/api-gateway')
-  td.when(apiGateway.getExport(td.matchers.contains(apiGatewayOpts))).thenResolve(response)
+const apiGateway = td.replace('../../src/util/aws/api-gateway')
+const fs = td.replace('../../src/util/modules/fs')
+const pkgConfig = td.replace('../../src/util/pkg-config')
 
-  return create('pull')
-  .then(() => {
-    const pull  = require('../../src/pull')
-    return pull({ apiId, region, stage })
-  })
-})
+td.when(apiGateway.exportStage(apiId, stage)).thenResolve(response)
 
-test('Calls API Gateway', ()=>{
-  td.verify(apiGateway.getExport(td.matchers.contains(apiGatewayOpts)), { times: 1 })
+test.before(() => {
+  const shep = require('../../src')
+  return shep.pull({ apiId, region, stage, quiet: true })
 })
 
-test('Writes api.json', t =>{
-  t.deepEqual(fs.readJSONSync('api.json'), { foo: "bar" })
+test('Writes api.json', () => {
+  td.verify(fs.writeFile('api.json', response, td.matchers.isA(Object)))
 })
 
-test('Updates package.json', t =>{
-  t.deepEqual(fs.readJSONSync('package.json').shep, { apiId, region })
+test('Updates package.json', () => {
+  td.verify(pkgConfig.update({apiId, region}))
 })
diff --git a/test/push/index.js b/test/push/index.js
index 4e46dbb..c252fa6 100644
--- a/test/push/index.js
+++ b/test/push/index.js
@@ -1,33 +1,25 @@
 import test from 'ava'
-import { create } from '../helpers/fixture'
-import td from 'testdouble'
-import Promise from 'bluebird'
-import fs from 'fs-extra-promise'
+import td from '../helpers/testdouble'
+import { fs } from '../helpers/fs'
 
-td.config({ promiseConstructor: Promise })
-
-let apiGateway
-
-const id = 'newApi'
-const response = { id }
+const apiId = 'foo'
 const region = 'test'
+const api = {
+  swagger: '2.0',
+  paths: {}
+}
 
-test.before(() => {
-  apiGateway = td.replace('../../src/util/api-gateway')
-  td.when(apiGateway.pushApi(td.matchers.contains({swagger: "2.0"}), undefined )).thenResolve(response)
+const apiGateway = td.replace('../../src/util/aws/api-gateway')
+const pkgConfig = td.replace('../../src/util/pkg-config')
 
-  return create('push')
-  .then(() => {
-    const push  = require('../../src/push')
-    return push({ region })
-  })
-})
+td.when(fs.readJSONSync('api.json')).thenReturn(api)
+td.when(apiGateway.pushApi(td.matchers.isA(Object), undefined)).thenResolve(apiId)
 
-test('Calls API Gateway', ()=>{
-  td.verify(apiGateway.pushApi(td.matchers.contains({swagger: "2.0"}), undefined), { times: 1 })
+test.before(() => {
+  const shep = require('../../src')
+  return shep.push({ region, quiet: true })
 })
 
-
-test('Updates package.json', t =>{
-  t.deepEqual(fs.readJSONSync('package.json').shep, { apiId: id, region })
+test('Updates package.json', () => {
+  td.verify(pkgConfig.update({apiId, region}))
 })
diff --git a/test/run/index.js b/test/run/index.js
index 90c5728..d841045 100644
--- a/test/run/index.js
+++ b/test/run/index.js
@@ -1,29 +1,25 @@
 import test from 'ava'
-import { create } from '../helpers/fixture'
-import generateFunction from '../../src/generate/function'
-import td from 'testdouble'
-import Promise from 'bluebird'
-import run from '../../src/run'
+import td from '../helpers/testdouble'
 
-// Supress observetory output
-require('observatory').settings({ write: () => {} })
+const funcName = 'foo'
+const handler = 'handler'
+const config = { Handler: `index.${handler}` }
+const events = [{}]
+const lambdaFunc = td.object([handler])
+td.when(lambdaFunc[handler](td.matchers.isA(Object), td.matchers.isA(Object))).thenCallback(null, 'bar')
 
-td.config({ promiseConstructor: Promise })
+const load = td.replace('../../src/util/load')
+td.when(load.lambdaConfig(funcName)).thenReturn(config)
+td.when(load.events(funcName, td.matchers.anything())).thenReturn(events)
 
-test.before(() => {
-  return create('run')
-  .then(() => generateFunction({ name: 'foo' }) )
-  .then(() => generateFunction({ name: 'bar' }) )
-})
+const requireProject = td.replace('../../src/util/require-project')
+td.when(requireProject(td.matchers.contains(funcName))).thenReturn(lambdaFunc)
 
-test('Calls the function', (t)=>{
-  t.truthy(run({ name: 'foo', build: true }))
+test.before(() => {
+  return require('../../src/run/index')({ name: funcName, build: false })
 })
 
-test('Throws an exception for an event that does not exist', (t)=>{
-  t.throws(() => run({ name: 'foo', build: true, event: 'doesnotexist' }))
+test('Calls the function', () => {
+  td.verify(lambdaFunc[handler](), { ignoreExtraArgs: true })
 })
 
-test('Throws an exception for unbuilt function', (t)=>{
-  t.throws(run({ name: 'bar', build: false }))
-})
diff --git a/test/util/load-envs.js b/test/util/load-envs.js
deleted file mode 100644
index afc0631..0000000
--- a/test/util/load-envs.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import test from 'ava'
-import loadEnvs from '../../src/util/load-envs'
-import { create } from '../helpers/fixture'
-
-test.before(() => create('load-envs'))
-
-test('Loads the possible envs', (t) => {
-  t.deepEqual(loadEnvs().sort(), ['beta', 'development','production'])
-})
diff --git a/test/util/load-events.js b/test/util/load-events.js
deleted file mode 100644
index 98152bb..0000000
--- a/test/util/load-events.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import test from 'ava'
-import loadEvents from '../../src/util/load-events'
-import { create } from '../helpers/fixture'
-import generateFunction from '../../src/generate/function'
-import fs from 'fs-extra-promise'
-
-test.before(() => {
-  return create('load-events')
-  .then(() => generateFunction({ name: 'foo' }) )
-  .then(() => fs.writeJsonAsync('functions/foo/events/success.json', {}))
-})
-
-test('Loads all events', (t) => {
-  t.deepEqual(loadEvents('foo').sort(), ['default.json', 'success.json'])
-})
-
-test('Loads a single event', (t) => {
-  t.deepEqual(loadEvents('foo', 'success'), ['success.json'])
-})
-
-test('No events exist', (t) => {
-  t.throws(() => loadEvents('foo', 'no-match'), /No event in/)
-})
diff --git a/test/util/load-funcs.js b/test/util/load-funcs.js
deleted file mode 100644
index 8e2a908..0000000
--- a/test/util/load-funcs.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import test from 'ava'
-import loadFuncs from '../../src/util/load-funcs'
-import { create } from '../helpers/fixture'
-import generateFunction from '../../src/generate/function'
-
-test.before(() => {
-  return create('load-funcs')
-  .then(() => generateFunction({ name: 'foo' }) )
-  .then(() => generateFunction({ name: 'bar' }) )
-
-})
-
-test('Loads all functions', (t) => {
-  t.deepEqual(loadFuncs().sort(), ['bar', 'foo'])
-})
-
-test('Loads matching functions', (t) => {
-  t.deepEqual(loadFuncs('*ar').sort(), ['bar'])
-})
-
-test('No functions exist', (t) => {
-  t.throws(() => loadFuncs('no_match'), /No functions found/)
-})
diff --git a/test/util/load.js b/test/util/load.js
new file mode 100644
index 0000000..c051fca
--- /dev/null
+++ b/test/util/load.js
@@ -0,0 +1,27 @@
+import test from 'ava'
+import * as load from '../../src/util/load'
+
+test.before(() => process.chdir('../fixtures'))
+
+test('Loads environemnts', (t) => {
+  t.deepEqual(load.envs().sort(), [ 'beta', 'prod' ])
+})
+
+test('Loads functions', (t) => {
+  t.deepEqual(load.funcs().sort(), [ 'bar', 'foo' ])
+})
+
+test('Loads functions by pattern', (t) => {
+  t.deepEqual(load.funcs('f*'), [ 'foo' ])
+})
+
+test('Loads function events', (t) => {
+  t.deepEqual(load.events('foo').sort(), [ 'custom.json', 'default.json' ])
+})
+
+test('Loads lambda config', (t) => {
+  const config = load.lambdaConfig('foo')
+  t.is(config.Name, 'foo')
+  t.is(config.Role, 'admin')
+  t.is(config.Memory, 5)
+})
diff --git a/test/util/set-permissions.js b/test/util/set-permissions.js
new file mode 100644
index 0000000..51d5023
--- /dev/null
+++ b/test/util/set-permissions.js
@@ -0,0 +1,37 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const lambda = td.replace('../../src/util/aws/lambda')
+
+let setPermissions
+
+const apiId = 'api-id'
+const env = 'prod'
+const region = 'narnia'
+const accountId = '123456'
+const name = 'foo'
+
+const api = {
+  paths: {
+    '/': {
+      get: {
+        'x-amazon-apigateway-integration': {
+          uri: `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:${region}:${accountId}:function:${name}:\${stageVariables.functionAlias}/invocations`
+        }
+      }
+    }
+  }
+}
+
+test.before(() => { setPermissions = require('../../src/util/set-permissions') })
+
+test('Calls setPermission', () => {
+  return setPermissions(api, apiId, env).then(() => {
+    td.verify(lambda.setPermission(td.matchers.contains({
+      apiId,
+      accountId,
+      region,
+      name
+    })))
+  })
+})
diff --git a/test/util/shep-config.js b/test/util/shep-config.js
deleted file mode 100644
index 852c5cb..0000000
--- a/test/util/shep-config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import test from 'ava'
-import { load, update } from '../../src/util/shep-config'
-import { create } from '../helpers/fixture'
-import fs from 'fs-extra-promise'
-import path from 'path'
-
-test.before(() => {
-  return create('shep-config')
-})
-
-test('Loads all functions', (t) => {
-  t.deepEqual(load(), {})
-})
-
-test('Updates package.json', (t) => {
-  t.plan(1)
-
-  return update({foo: 'bar'}).then(()=>{
-    const pkg = fs.readJSONSync(path.join( process.cwd(), 'package.json'))
-    t.is(pkg.shep.foo, 'bar')
-  })
-})