Repository: bustlelabs/shep
Pull request #144 (2017-01-03 23:43:43)

Log stream
Functionally everything is here.

Premerge tasks:
- [x] verify that cli options work as intended
- [x] clean up comments
- [x] update docs


Patch:
diff --git a/README.md b/README.md
index 333be45..3ec0557 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Amazon Web Services [API gateway](https://aws.amazon.com/api-gateway/) and [Lamb
 
 ### Prerequisites
 
-It will be helpful to have some existing experience with API gateway and Lambda. If you have never used either of these tools before, it is recommended to setup a function manually to see how things are done. Please refer to Amazon's own [getting started guide](http://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started.html)
+It will be helpful to have some existing experience with API gateway and Lambda. If you have never used either of these tools before, it is recommended to setup a function manually to see how things are done. Please refer to Amazon's own [getting started guide](http://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started.html)  
 
 ### AWS credentials
 
@@ -21,22 +21,7 @@ Shep will require your amazon credentials and will load them using the same meth
 
 ### Installation
 
-```bash
-npm install -g shep
-npm install --save-dev shep // the global shep will run the local shep
-```
-
-Add a few lines to your `package.json`. Your [account id](https://console.aws.amazon.com/billing/home?#/account),
-
-```json
-{
-  "name": "my-great-package",
-  "shep": {
-    "accountId": "XXXXX",
-    "region": "us-east-1"
-  }
-}
-```
+`npm install -g shep@beta`
 
 ### Creating a new API
 
@@ -57,6 +42,7 @@ 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
+  logs [stage] [name]       Streams logs from the specified version of a function
   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
@@ -164,6 +150,22 @@ Examples:
   shep build beta create-user  Build only the create-user function
   shep build beta *-user       Build functions matching the pattern *-user
 ```
+#### `shep logs`
+```
+shep logs [stage] [name]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+  --stage    Name of stage to use
+  --name     Name of function to use
+  --region   Name of region to use, uses region in `package.json` if not given
+  --stream                                                                                     [boolean] [default: true]
+
+Examples:
+  shep logs                 Launch an interactive CLI
+  shep logs production foo  Shows logs for the `foo` function in the production environment
+```
 #### `shep generate function`
 ```
 shep generate function [name]
@@ -222,5 +224,3 @@ Pull requests welcome!
 Test: `npm test`
 
 Rebuild on file change: `npm run compile -- -w`
-
-Publish: `npm run pub` "publish" is reserved by npm
diff --git a/cli.js b/cli.js
index 42a24e9..4b7306b 100755
--- a/cli.js
+++ b/cli.js
@@ -1,21 +1,2 @@
 #! /usr/bin/env node
-var resolve = require('resolve').sync
-
-var shepGlobalPath = './lib/cli'
-var shepGlobalVersion = require('./package.json').version
-try {
-  var shepLocalPath = resolve('shep/lib/cli', {basedir: process.cwd()})
-  var shepLocalVersion = require(resolve('shep/package.json', {basedir: process.cwd()})).version
-} catch (e) { }
-
-if (process.argv.indexOf('--version') !== -1) {
-  console.log('shep/cli', shepGlobalVersion)
-  console.log('shep', shepLocalVersion || shepGlobalVersion)
-  process.exit()
-}
-
-var cliPath = shepLocalPath || shepGlobalPath
-var cli = require(cliPath)
-if (typeof cli === 'function') {
-  cli()
-}
+require('./lib/cli')
diff --git a/migration.md b/migration.md
index 83179ac..6897ead 100644
--- a/migration.md
+++ b/migration.md
@@ -21,8 +21,7 @@ example:
 
 ```json
 {
-  "FunctionName": "$PROJECT_NAME-$FUNCTION_NAME",
-  "Description": ""
+  "FunctionName": "$PROJECT_NAME-$FUNCTION_NAME"
 }
 ```
 
diff --git a/package.json b/package.json
index ec02f13..7c02e05 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,13 @@
 {
   "name": "shep",
-  "version": "2.0.2",
+  "version": "1.0.1",
   "description": "A framework for building JavaScript APIs with AWS API Gateway and Lambda",
   "main": "lib/index.js",
   "scripts": {
     "compile": "babel -d lib/ src/",
     "test": "ava",
     "lint": "standard",
-    "prepublish": "npm run compile",
-    "pub": "np"
-  },
-  "engines": {
-    "node": ">=4.0.0"
+    "prepublish": "npm run compile"
   },
   "keywords": [
     "serverless",
@@ -38,31 +34,28 @@
   "dependencies": {
     "archiver": "^1.0.0",
     "aws-sdk": "^2.4.13",
-    "bluebird": "^3.4.7",
+    "bluebird": "^3.4.0",
     "chalk": "^1.1.3",
     "cliui": "^3.2.0",
-    "execa": "^0.5.0",
+    "execa": "^0.4.0",
     "fs-extra-promise": "^0.4.1",
-    "inquirer": "^2.0.0",
-    "listr": "^0.9.0",
+    "inquirer": "^1.0.3",
+    "listr": "^0.7.0",
     "lodash.kebabcase": "^4.1.1",
     "lodash.merge": "^4.6.0",
     "loud-rejection": "^1.6.0",
     "minimatch": "^3.0.3",
-    "resolve": "^1.1.7",
     "stream-buffers": "^3.0.0",
     "yargs": "^6.0.0"
   },
   "devDependencies": {
-    "ava": "^0.17.0",
-    "babel-cli": "^6.18.0",
+    "ava": "^0.16.0",
+    "babel-cli": "^6.5.1",
     "babel-plugin-add-module-exports": "^0.2.1",
-    "babel-plugin-transform-async-to-module-method": "^6.16.0",
-    "babel-preset-es2015-node4": "^2.1.1",
-    "babel-register": "^6.18.0",
-    "np": "^2.12.0",
-    "standard": "^8.6.0",
-    "testdouble": "^1.10.1"
+    "babel-preset-es2015-node4": "^2.1.0",
+    "babel-register": "^6.7.2",
+    "standard": "^8.5.0",
+    "testdouble": "^1.6.1"
   },
   "babel": {
     "presets": [
@@ -70,14 +63,7 @@
     ],
     "plugins": [
       "add-module-exports",
-      "transform-object-rest-spread",
-      [
-        "transform-async-to-module-method",
-        {
-          "method": "coroutine",
-          "module": "bluebird"
-        }
-      ]
+      "transform-object-rest-spread"
     ]
   },
   "ava": {
diff --git a/src/cli.js b/src/cli.js
index a624a5a..7cc6a44 100644
--- a/src/cli.js
+++ b/src/cli.js
@@ -1,16 +1,12 @@
 import loudRejection from 'loud-rejection'
-import yargs from 'yargs'
 
-export default function cli () {
-  loudRejection()
+loudRejection()
 
-  yargs
-    .wrap(120)
-    .usage('Usage: $0 <command> [options]')
-    .demand(1)
-    .commandDir('./commands')
-    .version()
-    .help()
-    .strict()
-    .argv
-}
+require('yargs')
+  .wrap(120)
+  .usage('Usage: $0 <command> [options]')
+  .demand(1)
+  .commandDir('./commands')
+  .version()
+  .help()
+  .argv
diff --git a/src/commands/generate.js b/src/commands/generate.js
index b952849..e3e5534 100644
--- a/src/commands/generate.js
+++ b/src/commands/generate.js
@@ -3,8 +3,6 @@ export const desc = 'Run `shep generate --help` for additional information'
 export function builder (yargs) {
   return yargs
   .commandDir('./generate')
-  .demand(1)
-  .strict()
 }
 
 export function handler (args) {
diff --git a/src/commands/logs.js b/src/commands/logs.js
new file mode 100644
index 0000000..aa38e5a
--- /dev/null
+++ b/src/commands/logs.js
@@ -0,0 +1,39 @@
+import inquirer from 'inquirer'
+import logs from '../logs'
+import * as load from '../util/load'
+import merge from 'lodash.merge'
+
+export const command = 'logs [stage] [name]'
+export const desc = 'Streams logs from the specified version of a function'
+export function builder (yargs) {
+  return yargs
+  .pkgConf('shep', process.cwd())
+  .describe('stage', 'Name of stage to use')
+  .describe('name', 'Name of function to use')
+  .describe('region', 'Name of region to use, uses region in `package.json` if not given')
+  .boolean('stream', 'Stream logs')
+  .default('stream', true)
+  .example('shep logs', 'Launch an interactive CLI')
+  .example('shep logs production foo', 'Shows logs for the `foo` function in the production environment')
+}
+
+export function handler (opts) {
+  const questions = [
+    {
+      name: 'stage',
+      message: 'Stage',
+      type: 'list',
+      choices: () => load.envs()
+    },
+    {
+      name: 'name',
+      message: 'Function',
+      type: 'list',
+      choices: () => load.funcs()
+    }
+  ]
+
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
+  .then(logs)
+}
diff --git a/src/generate-endpoint/index.js b/src/generate-endpoint/index.js
index da6dea6..7c22469 100644
--- a/src/generate-endpoint/index.js
+++ b/src/generate-endpoint/index.js
@@ -16,7 +16,7 @@ module.exports = function (opts) {
     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 api = load.api()
 
   const name = `${path} ${method}`
   const { shortName, fullName } = genName(name)
@@ -46,10 +46,6 @@ module.exports = function (opts) {
 function addPath (api, path, method, accountId, functionName) {
   if (method === 'any') { method = 'x-amazon-apigateway-any-method' }
 
-  if (!api.paths) {
-    api.paths = {}
-  }
-
   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] || {}
diff --git a/src/generate-function/index.js b/src/generate-function/index.js
index cb4ca45..70d31b3 100644
--- a/src/generate-function/index.js
+++ b/src/generate-function/index.js
@@ -1,4 +1,4 @@
-import { mkdirp, writeFile } from '../util/modules/fs'
+import { mkdir, writeFile } from '../util/modules/fs'
 import genName from '../util/generate-name'
 import * as templates from './templates'
 import listr from '../util/modules/listr'
@@ -10,11 +10,11 @@ export default function (opts) {
   const tasks = listr([
     {
       title: `Create functions/${shortName}/`,
-      task: () => mkdirp(`./functions/${shortName}`)
+      task: () => mkdir(`./functions/${shortName}`)
     },
     {
       title: `Create functions/${shortName}/events`,
-      task: () => mkdirp(`./functions/${shortName}/events`)
+      task: () => mkdir(`./functions/${shortName}/events`)
     },
     {
       title: 'Create files',
diff --git a/src/generate-function/templates.js b/src/generate-function/templates.js
index c7af45d..85fa10d 100644
--- a/src/generate-function/templates.js
+++ b/src/generate-function/templates.js
@@ -3,11 +3,11 @@ export function index () {
 
 export function handler(event, context, callback) {
   // Replace below with your own code!
-  console.log(JSON.stringify(event))
-  console.log(JSON.stringify(context))
-  console.log(JSON.stringify(config))
+  console.log(event)
+  console.log(context)
+  console.log(config)
 
-  callback(null, { statusCode: 200, headers: {}, body: 'success!' })
+  callback(null, { statusCode: 200, headers: {}, body: "success!" })
 }`
 }
 
@@ -28,8 +28,7 @@ export function event () {
 
 export function lambda (name) {
   let obj = {
-    FunctionName: name,
-    Description: ''
+    FunctionName: name
   }
 
   return JSON.stringify(obj, null, 2)
diff --git a/src/index.js b/src/index.js
index 3924dd0..92df1d6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,8 +4,7 @@ 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 logs } from './logs'
 export { default as generateFunction } from './generate-function'
 export { default as generateEndpoint } from './generate-endpoint'
 export { default as generateWebpack } from './generate-webpack'
-export { version } from '../package.json'
-
diff --git a/src/logs/index.js b/src/logs/index.js
new file mode 100644
index 0000000..5a999fd
--- /dev/null
+++ b/src/logs/index.js
@@ -0,0 +1,40 @@
+import Promise from 'bluebird'
+import AWS from '../util/aws'
+import { getLogGroup } from '../util/aws/cloudwatch-logs'
+import { getAliasVersion } from '../util/aws/lambda'
+import getLogs from '../util/get-logs'
+import { pkg } from '../util/load'
+import genName from '../util/generate-name'
+
+export default function (opts) {
+  const functionName = genName(opts.name).fullName
+  const aliasName = opts.stage
+  const stream = opts.stream
+  const region = opts.region || pkg().shep.region
+
+  AWS.config.update({ region })
+
+  return Promise.join(getLogGroup({ functionName }), getAliasVersion({ functionName, aliasName }),
+    (logGroupName, functionVersion) => getLogs({ logGroupName, functionVersion, stream }))
+  .then(printEventsLoop)
+}
+
+function printEventsLoop ({ events, nextLogCall }) {
+  const rate = 500
+
+  if (events !== undefined && events.length !== 0) {
+    console.log(events.map(formatEvent).join(''))
+  }
+
+  if (nextLogCall === undefined) {
+    return Promise.resolve()
+  }
+
+  return Promise.delay(rate)
+  .then(nextLogCall)
+  .then(printEventsLoop)
+}
+
+function formatEvent ({ timestamp, message }) {
+  return `[${new Date(timestamp).toTimeString()}]: ${message}`
+}
diff --git a/src/new/index.js b/src/new/index.js
index b9d7a22..90ae279 100644
--- a/src/new/index.js
+++ b/src/new/index.js
@@ -1,4 +1,4 @@
-import { mkdirp, writeFile } from '../util/modules/fs'
+import { mkdir, writeFile } from '../util/modules/fs'
 import { getRole, createRole, attachPolicy } from '../util/aws/iam'
 import * as templates from './templates'
 import Promise from 'bluebird'
@@ -17,7 +17,7 @@ export default function run (opts) {
     },
     {
       title: `Create ${path}/`,
-      task: () => mkdirp(path)
+      task: () => mkdir(path)
     },
     {
       title: 'Create Subdirectories',
@@ -30,6 +30,10 @@ export default function run (opts) {
     {
       title: 'Install Depedencies',
       task: npmInstall
+    },
+    {
+      title: 'Initialize Git',
+      task: initGit
     }
   ]
 
@@ -59,8 +63,8 @@ function setupIam (context) {
 
 function createSubDirs ({ path }) {
   return Promise.all([
-    mkdirp(path + '/functions'),
-    mkdirp(path + '/config')
+    mkdir(path + '/functions'),
+    mkdir(path + '/config')
   ])
 }
 
@@ -84,3 +88,6 @@ function npmInstall ({ path }) {
   return exec('npm', ['install'], { cwd: path })
 }
 
+function initGit ({ path }) {
+  return exec('git', ['init'], { cwd: path })
+}
diff --git a/src/new/templates.js b/src/new/templates.js
index 2cd6542..10592ab 100644
--- a/src/new/templates.js
+++ b/src/new/templates.js
@@ -6,23 +6,20 @@ export function api (apiName) {
   },
   "schemes": [ "https" ],
   "paths": {}
-}
-`
+}`
 }
 
 export function env (env) {
   return `module.exports = {
   env: "${env}",
   secretkey: "${env}-secret-key"
-}
-`
+}`
 }
 
 export function gitignore () {
   return `dist/*
 node_modules/*
-config/*
-`
+config/*`
 }
 
 export function lambda (arn = '') {
@@ -34,7 +31,7 @@ export function lambda (arn = '') {
     Runtime: 'nodejs4.3'
   }
 
-  return JSON.stringify(obj, null, 2) + '\n'
+  return JSON.stringify(obj, null, 2)
 }
 
 export function lambdaRole () {
@@ -53,7 +50,6 @@ export function lambdaRole () {
 }
 
 export function pkg ({ apiName, accountId = '', region = '' }) {
-  const version = require('../index').version
   let obj = {
     name: apiName,
     version: '1.0.0',
@@ -62,8 +58,7 @@ export function pkg ({ apiName, accountId = '', region = '' }) {
     license: '',
     devDependencies: {
       webpack: '2.1.0-beta.25',
-      minimatch: '3.0.3',
-      shep: version
+      minimatch: '3.0.3'
     },
     shep: {
       region: region,
@@ -72,12 +67,11 @@ export function pkg ({ apiName, accountId = '', region = '' }) {
     }
   }
 
-  return JSON.stringify(obj, null, 2) + '\n'
+  return JSON.stringify(obj, null, 2)
 }
 
 export function readme (apiName) {
-  return `# ${apiName}
-`
+  return `# ${apiName}`
 }
 
 export function webpack () {
@@ -90,23 +84,14 @@ const path = require('path')
 const minimatch = require('minimatch')
 
 const env = process.env.NODE_ENV || 'development'
-
-const shepConfig = path.resolve(\`config/$\{env}.js\`)
-
-try {
-  fs.statSync(shepConfig)
-} catch (e) {
-  throw new Error(\`cannot read config file for environment "$\{env}": $\{e.message}\`)
-}
-
 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
-  }, {})
+.filter(minimatch.filter(pattern))
+.reduce((map, funcName) => {
+  map[funcName] = path.resolve(\`functions/\${funcName}/index\`)
+  return map
+}, {})
 
 module.exports = {
   target: 'node',
@@ -119,7 +104,8 @@ module.exports = {
   resolve: {
     modules: [ 'node_modules', 'lib' ],
     alias: {
-      'shep-config': shepConfig
+      'shep-config': path.resolve(\`config/\${env}.js\`)
+    }
   }
 }
 `
diff --git a/src/run/index.js b/src/run/index.js
index 7e996cb..8bf6785 100644
--- a/src/run/index.js
+++ b/src/run/index.js
@@ -12,7 +12,7 @@ const results = { success: 'SUCCESS', error: 'ERROR', exception: 'EXCEPTION' }
 
 const awsNodeVersion = '4.3.2'
 
-export default async function (opts) {
+export default function (opts) {
   AWS.config.update({region: opts.region})
 
   const processVersion = process.versions.node
@@ -24,48 +24,46 @@ export default async function (opts) {
   const verbose = opts.v
   const performBuild = opts.build
   const name = opts.name
-  const env = opts.environment || 'development'
+  const env = opts.environemnt || 'development'
   const lambdaConfig = load.lambdaConfig(name)
   const events = load.events(name, opts.event)
   const [ fileName, handler ] = lambdaConfig.Handler.split('.')
 
   const context = {}
 
-  if (performBuild) {
-    await build(name, env)
-  }
-
-  const func = requireProject(`dist/${name}/${fileName}`)[handler]
-
-  const out = await Promise.map(events, (eventFilename) => {
-    const event = requireProject(`functions/${name}/events/${eventFilename}`)
-    return new Promise((resolve) => {
-      const output = { name: eventFilename }
-      output.start = new Date()
-      try {
-        func(event, context, (err, res) => {
+  return Promise.resolve()
+  .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}`)
+      return new Promise((resolve) => {
+        let output = { name: eventFilename }
+        output.start = new Date()
+        try {
+          func(event, context, (err, res) => {
+            output.end = new Date()
+            if (err) {
+              output.result = results.error
+              output.response = err
+            } else {
+              output.result = results.success
+              output.response = res
+            }
+            resolve(output)
+          })
+        } catch (e) {
           output.end = new Date()
-          if (err) {
-            output.result = results.error
-            output.response = err
-          } else {
-            output.result = results.success
-            output.response = res
-          }
+          output.result = results.exception
+          output.response = e
           resolve(output)
-        })
-      } catch (e) {
-        output.end = new Date()
-        output.result = results.exception
-        output.response = e
-        resolve(output)
-      }
+        }
+      })
     })
   })
-
-  logOutput(out)
-  out.map((out) => formatOutput(out, verbose))
-  console.log(ui.toString())
+  .tap(logOutput)
+  .map((out) => formatOutput(out, verbose))
+  .then(() => console.log(ui.toString()))
 }
 
 function logOutput (outputs) {
diff --git a/src/util/aws/cloudwatch-logs.js b/src/util/aws/cloudwatch-logs.js
new file mode 100644
index 0000000..1b1124b
--- /dev/null
+++ b/src/util/aws/cloudwatch-logs.js
@@ -0,0 +1,51 @@
+import AWS from './'
+
+export function getLogGroup ({ functionName }) {
+  const cwLogs = new AWS.CloudWatchLogs()
+  const expetedName = `/aws/lambda/${functionName}`
+
+  const params = {
+    logGroupNamePrefix: expetedName
+  }
+
+  return cwLogs.describeLogGroups(params).promise().get('logGroups')
+  .then((groups) => groups.filter((logGroup) => logGroup.logGroupName === expetedName))
+  .get(0)
+  .get('logGroupName')
+  .catch((e) => {
+    return Promise.reject({ code: 'ResourceNotFoundException',
+      message: 'No log groups found for specified function' })
+  })
+}
+
+export function getLogStreams ({ logGroupName, functionVersion }) {
+  const cwLogs = new AWS.CloudWatchLogs()
+  const versionRegExp = new RegExp(`\\[${functionVersion}\\]`)
+
+  // LastEventTime isn't always accurate
+  const params = {
+    logGroupName,
+    orderBy: 'LastEventTime',
+    descending: true
+  }
+
+  return cwLogs.describeLogStreams(params).promise()
+  .get('logStreams')
+  .filter((stream) => versionRegExp.test(stream.logStreamName))
+  .map((x) => x.logStreamName)
+}
+
+export function getLogEvents ({ logGroupName, logStreamNames, start, end }) {
+  const cwLogs = new AWS.CloudWatchLogs()
+
+  const params = {
+    logGroupName,
+    logStreamNames,
+    startTime: start,
+    endTime: end,
+    interleaved: true
+  }
+
+  return cwLogs.filterLogEvents(params).promise()
+  .get('events')
+}
diff --git a/src/util/aws/lambda.js b/src/util/aws/lambda.js
index b289765..72ab006 100644
--- a/src/util/aws/lambda.js
+++ b/src/util/aws/lambda.js
@@ -18,6 +18,18 @@ export function putFunction (config, ZipFile) {
   })
 }
 
+export function getAliasVersion ({ functionName, aliasName }) {
+  const lambda = new AWS.Lambda()
+
+  const params = {
+    FunctionName: functionName,
+    Name: aliasName
+  }
+
+  return lambda.getAlias(params).promise()
+  .get('FunctionVersion')
+}
+
 export function setAlias ({ Version, FunctionName }, Name) {
   const lambda = new AWS.Lambda()
 
diff --git a/src/util/build-functions.js b/src/util/build-functions.js
index 7aeb533..f1d8e01 100644
--- a/src/util/build-functions.js
+++ b/src/util/build-functions.js
@@ -1,5 +1,5 @@
 import exec from './modules/exec'
 
 export default function (PATTERN, NODE_ENV) {
-  return exec('webpack', ['--bail'], { env: { PATTERN, NODE_ENV } })
+  return exec('webpack', [], { env: { PATTERN, NODE_ENV } })
 }
diff --git a/src/util/generate-name.js b/src/util/generate-name.js
index 2df2327..dc5bec5 100644
--- a/src/util/generate-name.js
+++ b/src/util/generate-name.js
@@ -2,9 +2,8 @@ import { pkg } from './load'
 import kebabCase from 'lodash.kebabcase'
 
 export default function (str) {
-  const expandedStr = /^\/\s/.test(str) ? str.replace('/', 'root') : str
   return {
-    shortName: kebabCase(expandedStr),
-    fullName: kebabCase(`${pkg().name} ${expandedStr}`)
+    shortName: kebabCase(str),
+    fullName: kebabCase(`${pkg().name} ${str}`)
   }
 }
diff --git a/src/util/get-logs.js b/src/util/get-logs.js
new file mode 100644
index 0000000..3f2af0e
--- /dev/null
+++ b/src/util/get-logs.js
@@ -0,0 +1,28 @@
+import { getLogStreams, getLogEvents } from './aws/cloudwatch-logs'
+
+export default function getLogs ({ logGroupName, functionVersion, stream, start = Date.now() }) {
+  let recievedEvents
+
+  return getLogStreams({ logGroupName, functionVersion })
+  .then((logStreamNames) => getLogEvents({ logGroupName, logStreamNames, start }))
+  .tap((events) => { recievedEvents = events })
+  .reduce(maxTimestamp, start)
+  .then((timestamp) => timestamp === start ? timestamp : timestamp + 1) // If timestamp from new event, increase start time to avoid duplicate events
+  .then((lastTimestamp) => {
+    return Promise.resolve({
+      events: recievedEvents,
+      nextLogCall: tailCallGenerator({ logGroupName, functionVersion, stream, lastTimestamp })
+    })
+  })
+}
+
+function tailCallGenerator ({ logGroupName, functionVersion, stream, lastTimestamp }) {
+  if (stream) {
+    return () => getLogs({ logGroupName, functionVersion, stream, start: lastTimestamp })
+  }
+  return () => Promise.resolve({})
+}
+
+function maxTimestamp (latest, event) {
+  return Math.max(latest, event.timestamp)
+}
diff --git a/src/util/load.js b/src/util/load.js
index 9fff7d3..45fd035 100644
--- a/src/util/load.js
+++ b/src/util/load.js
@@ -40,9 +40,5 @@ export function pkg () {
 }
 
 export function api () {
-  try {
-    return readJSONSync('api.json')
-  } catch (e) {
-    return null
-  }
+  return readJSONSync('api.json')
 }
diff --git a/src/util/modules/fs.js b/src/util/modules/fs.js
index a4c33a5..fc50e51 100644
--- a/src/util/modules/fs.js
+++ b/src/util/modules/fs.js
@@ -1,6 +1,6 @@
 import fs from 'fs-extra-promise'
 
-export const mkdirp = fs.mkdirpAsync
+export const mkdir = fs.mkdirAsync
 export const writeFile = fs.writeFileAsync
 export const writeJSON = fs.writeJSONAsync
 export const readJSON = fs.readJSONAsync


Test patch:
diff --git a/test/build/index.js b/test/build/index.js
index 66405f1..ae97167 100644
--- a/test/build/index.js
+++ b/test/build/index.js
@@ -6,4 +6,4 @@ test.before(() => {
   return shep.build({ quiet: true })
 })
 
-test(didExec, 'webpack', ['--bail'])
+test(didExec, 'webpack')
diff --git a/test/cli.js b/test/cli.js
index 4a3dd71..84ab51a 100644
--- a/test/cli.js
+++ b/test/cli.js
@@ -2,5 +2,5 @@ import test from 'ava'
 import execa from 'execa'
 
 test('it runs', () => {
-  return execa('./cli.js', ['--help'])
+  return execa('../cli.js', ['--help'])
 })
diff --git a/test/helpers/exec.js b/test/helpers/exec.js
index 70c9da9..06b9778 100644
--- a/test/helpers/exec.js
+++ b/test/helpers/exec.js
@@ -3,7 +3,8 @@ import td from 'testdouble'
 const exec = td.replace('../../src/util/modules/exec')
 
 function didExec (t, cmd, args, options = {}) {
-  td.verify(exec(cmd, args, td.matchers.contains(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 || ''}`
diff --git a/test/helpers/fs.js b/test/helpers/fs.js
index e5f558e..745fe05 100644
--- a/test/helpers/fs.js
+++ b/test/helpers/fs.js
@@ -3,7 +3,7 @@ import td from 'testdouble'
 const fs = td.replace('../../src/util/modules/fs')
 
 function createdDir (t, dirName) {
-  td.verify(fs.mkdirp(td.matchers.contains(dirName)))
+  td.verify(fs.mkdir(td.matchers.contains(dirName)))
 }
 createdDir.title = (providedTitle, dirName) => `Created dir ${dirName}`
 
diff --git a/test/logs/index.js b/test/logs/index.js
new file mode 100644
index 0000000..373230b
--- /dev/null
+++ b/test/logs/index.js
@@ -0,0 +1,40 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+import Promise from 'bluebird'
+
+const pkg = {
+  name: 'foo',
+  shep: {
+    region: 'us-east-1'
+  }
+}
+const env = 'development'
+const functionName = 'bar'
+const callback = td.function('callback')
+const getLogResponse = {
+  events: [],
+  nextLogCall: callback
+}
+
+const load = td.replace('../../src/util/load')
+td.when(load.pkg()).thenReturn(pkg)
+
+const cloudwatchLogs = td.replace('../../src/util/aws/cloudwatch-logs')
+const lambda = td.replace('../../src/util/aws/lambda')
+
+const getLogs = td.replace('../../src/util/get-logs')
+td.when(getLogs(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve(getLogResponse))
+
+test.before(() => {
+  const shep = require('../../src/index')
+  shep.logs({ env, name: functionName, stream: false })
+})
+
+test('Gets alias version', () => {
+  td.verify(lambda.getAliasVersion(td.matchers.anything()))
+})
+
+test('Gets log group', () => {
+  td.verify(cloudwatchLogs.getLogGroup(td.matchers.anything()))
+})
+
diff --git a/test/new/index.js b/test/new/index.js
index 03d92a4..91cb22b 100644
--- a/test/new/index.js
+++ b/test/new/index.js
@@ -27,5 +27,6 @@ test(wroteFile, `${path}/.gitignore`)
 test(wroteFile, `${path}/README.md`)
 test(wroteFile, `${path}/webpack.config.js`)
 
-test(didExec, 'npm', ['install'])
+test(didExec, 'npm', 'install')
+test(didExec, 'git', 'init')
 
diff --git a/test/util/load.js b/test/util/load.js
index 64fe648..c051fca 100644
--- a/test/util/load.js
+++ b/test/util/load.js
@@ -1,7 +1,7 @@
 import test from 'ava'
 import * as load from '../../src/util/load'
 
-test.before(() => process.chdir('./test/fixtures'))
+test.before(() => process.chdir('../fixtures'))
 
 test('Loads environemnts', (t) => {
   t.deepEqual(load.envs().sort(), [ 'beta', 'prod' ])
@@ -25,8 +25,3 @@ test('Loads lambda config', (t) => {
   t.is(config.Role, 'admin')
   t.is(config.Memory, 5)
 })
-
-test('Is ok with no api config', (t) => {
-  const config = load.api()
-  t.is(config, null)
-})