[
  {
    "path": ".gitignore",
    "content": "node_modules/\nnpm-debug.log\n.DS_Store\n"
  },
  {
    "path": ".npmrc",
    "content": "registry=http://registry.npmjs.org/\nsave-exact=true\nprogress=false\n\n"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: false\nlanguage: node_js\ncache:\n  directories:\n    - node_modules\nnotifications:\n  email: true\nnode_js:\n  - '6'\nbefore_script:\n  - npm prune\nscript:\n  - ./next-update-travis.sh\n  - npm test\n  # disable deploys for now - need new domain\n  # because current domain used for testing bahmutov.com\n  # is now a redirect to glebbahmutov.com :(\n  # - cd test\n  # - npm run deploy\n  # - npm run list\n  # - npm run prune\n  # - echo There should be no deploys now\n  # - npm run list\n  # # back to main folder\n  # - cd ..\nafter_success:\n  - npm run semantic-release\nbranches:\n  except:\n    - /^v\\d+\\.\\d+\\.\\d+$/\n"
  },
  {
    "path": "README.md",
    "content": "# now-pipeline\n\n> Single CI command to deploy new code to Zeit Now\n> Includes e2e tests and the alias switch\n\n[![NPM][npm-icon] ][npm-url]\n\n[![Build status][ci-image] ][ci-url]\n[![semantic-release][semantic-image] ][semantic-url]\n[![js-standard-style][standard-image]][standard-url]\n[![first-timers-only](http://img.shields.io/badge/first--timers--only-friendly-blue.svg)](https://github.com/bahmutov/now-pipeline/labels/first-timers-only)\n[![next-update-travis badge][nut-badge]][nut-readme]\n\n## First time contributors\n\nThis repo is [first time OSS contributor friendly](http://www.firsttimersonly.com/).\nSee [these issues](https://github.com/bahmutov/now-pipeline/labels/first-timers-only)\nto contribute in meaningful way.\n\n## What and why\n\nI am [super excited](https://glebbahmutov.com/blog/think-inside-the-box/)\nabout [Zeit Now](https://zeit.co/now) tool; this is the \"missing CI tool\"\nfor it. A single command `now-pipeline`\n\n- deploys new version\n- tests it\n- switches alias to the new deployment\n- takes down the old deployment\n\nShould be enough to automatically update the server or service running in\nthe cloud without breaking anything.\n\n## Install and use\n\n```sh\nnpm i -g now-pipeline\n```\n\nSet `NOW_TOKEN` CI environment variable with a token that you can get from\n[Zeit account page](https://zeit.co/account#api-tokens)\n\nAdd CI command to `now-pipeline`. By default it will execute `npm test`\nand will pass the deployed url as `NOW_URL` environment variable. You can\ncustomize everything.\n\n## Example\n\nSimple Travis commands\n\n```yml\nscript:\n  # after unit tests\n  - npm i -g now-pipeline\n  - now-pipeline\n```\n\nPrune existing deploys (if they do not have an alias) and show the deploy.\n\n```yml\nscript:\n  - npm i -g now-pipeline\n  - now-pipeline-prune\n  - now-pipeline\n  - now-pipeline-list\n```\n\nSet [domain alias](https://zeit.co/world) if there is no existing one\n\n```yml\nscript:\n  - npm i -g now-pipeline\n  - now-pipeline --alias foo.domain.com\n```\n\nPass in path to be used as deploy directory\n\n```yml\nscript:\n  - npm i -g now-pipeline\n  - now-pipeline --dir your/directory\n```\n\nPass test command and name of the environment variable for deployed url\n\n```yml\nscript:\n  - npm i -g now-pipeline\n  - now-pipeline --as HOST --test \"npm run e2e\"\n```\n\n## Example projects\n\n* [todomvc-express](https://github.com/bahmutov/todomvc-express/blob/master/.travis.yml)\n* [express-sessions-tutorial](https://github.com/bahmutov/express-sessions-tutorial/blob/master/.travis.yml)\n* [test-semantic-deploy-with-now](https://github.com/bahmutov/test-semantic-deploy-with-now)\n\n## Additional bin commands\n\n* `now-pipeline-list` - see the current deploys for the current project\n* `now-pipeline-prune` - remove all non-aliased deploys for the current project\n\nYou can pass custom test command to the pipeline to be used after deploying\nfresh install using `--test \"command\"` argument. The command will get `NOW_URL`\nenvironment variable with new install. For example\n\n```sh\nnpm i -g now-pipeline\nnow-pipeline --test \"npm run prod-test\"\n```\n\nwhere the `package.json` has\n\n```json\n{\n  \"scripts\": {\n    \"prod-test\": \"e2e-test $NOW_URL\"\n  }\n}\n```\n\n## Debugging\n\nYou can see verbose log messages by running this tool with environment variable `DEBUG=now-pipeline`\n\n## Details\n\n* `now-pipeline` uses [Zeit API](https://zeit.co/api) via [now-client](https://github.com/zeit/now-client).\n* You can see the list of recent actions at [Zeit dashboard](https://zeit.co/dashboard).\n* It discovers files to send using [pkgd](https://github.com/inikulin/pkgd),\n  you can see the files by using the following command\n  (read [Smaller published NPM modules](https://glebbahmutov.com/blog/smaller-published-NPM-modules/) for more details)\n```sh\nt=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\"\n```\n* file `.npmignore` is considered an optional file\n\n## Related\n\n* [next-update](https://github.com/bahmutov/next-update) is a similar\n  \"if tests pass, upgrade\" tool for your NPM dependencies.\n\n### Small print\n\nAuthor: Gleb Bahmutov &lt;gleb.bahmutov@gmail.com&gt; &copy; 2016\n\n* [@bahmutov](https://twitter.com/bahmutov)\n* [glebbahmutov.com](http://glebbahmutov.com)\n* [blog](http://glebbahmutov.com/blog)\n\nLicense: MIT - do anything with the code, but don't blame me if it does not work.\n\nSupport: if you find any problems with this module, email / tweet /\n[open issue](https://github.com/bahmutov/now-pipeline/issues) on Github\n\n## MIT License\n\nCopyright (c) 2016 Gleb Bahmutov &lt;gleb.bahmutov@gmail.com&gt;\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n[npm-icon]: https://nodei.co/npm/now-pipeline.svg?downloads=true\n[npm-url]: https://npmjs.org/package/now-pipeline\n[ci-image]: https://travis-ci.org/bahmutov/now-pipeline.svg?branch=master\n[ci-url]: https://travis-ci.org/bahmutov/now-pipeline\n[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg\n[semantic-url]: https://github.com/semantic-release/semantic-release\n[standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg\n[standard-url]: http://standardjs.com/\n[nut-badge]: https://img.shields.io/badge/next--update--travis-ok-green.svg\n[nut-readme]: https://github.com/bahmutov/next-update-travis#readme\n"
  },
  {
    "path": "bin/list.js",
    "content": "#!/usr/bin/env node\n\n'use strict'\n\nrequire('console.table')\n\nconst nowPipeline = require('..')\nconst pkg = nowPipeline.getPackage()\n\nnowPipeline.deployments(pkg.name)\n  .then(deploys => {\n    if (deploys.length) {\n      deploys.forEach(d => {\n        delete d.created\n      })\n      console.table(deploys)\n    } else {\n      console.log(`Zero deploys for ${pkg.name}`)\n    }\n  })\n  .catch(err => {\n    console.error(err)\n    process.exit(-1)\n  })\n"
  },
  {
    "path": "bin/now-pipeline.js",
    "content": "#!/usr/bin/env node\n\n'use strict'\n\nconst debug = require('debug')('now-pipeline')\nconst initCrashReporter = require('node-sentry-error-reporter')\ninitCrashReporter()\n\nconst R = require('ramda')\nconst runCommand = require('../src/run-command')\nconst is = require('check-more-types')\nconst la = require('lazy-ass')\nconst pkgd = require('pkgd')\nconst argv = require('minimist')(process.argv.slice(2))\nconst path = require('path')\n\nconst envUrlName = 'NOW_URL'\nconst passAsName = argv.as || envUrlName\nconst testCommand = argv.test || 'npm test'\n\nconst nowPipeline = require('..')\nconst pkg = nowPipeline.getPackage()\n\nfunction findFiles () {\n  if ('dir' in argv) {\n    // Pass directory with --dir flag\n    let dirArg = argv.dir\n    la(is.string(dirArg), 'directory path should be a string', dirArg)\n\n    try {\n      // change cwd to the passed directory path\n      process.chdir(path.resolve(argv.dir))\n    } catch (err) {\n      console.error('error changing deploy directory')\n      console.error(err)\n      console.error(`attempted directory: ${dirArg}`)\n      la(new Error(err))\n    }\n  }\n\n  debug('deploying from directory', process.cwd())\n\n  return pkgd(process.cwd())\n    .then(R.prop('files'))\n}\n\nvar start\n\nfunction findDeploy (url) {\n  la(is.url(url), 'expected url', url)\n\n  return nowPipeline.deployments()\n    .then(deploys => {\n      debug('looking for url %s in %d deploys', url, deploys.length)\n\n      const found = deploys.find(d => {\n        return url.endsWith(d.url)\n      })\n      if (!found) {\n        console.error('Could not find deploy for url', url)\n        process.exit(-1)\n      }\n      debug('found deployment', found)\n      return found\n    })\n}\n\nif (process.env[envUrlName]) {\n  const url = process.env[envUrlName]\n  console.log(`found existing env variable ${envUrlName} ${url}`)\n\n  // start = Promise.resolve(url)\n  // todo Find the deployment with given url\n  start = findFiles().then((filenames) => findDeploy(url))\n} else {\n  start = findFiles().then(nowPipeline.deploy)\n}\n\nfunction setFullHost (deploy) {\n  if (!deploy.url) {\n    deploy.url = deploy.host\n  }\n  deploy.url = addHttps(deploy.url)\n\n  console.log('deployed to url', deploy.url)\n  la(is.url(deploy.url), 'expected deploy.url to be full https', deploy.url)\n  return deploy\n}\n\nfunction deployIsWorking (deploy) {\n  console.log(`deployed url ${deploy.url} is working`)\n}\n\nfunction addHttps (url) {\n  return url.startsWith('https://') ? url : 'https://' + url\n}\n\nconst updateAliasIfNecessary = R.curry(\n  function updateAliasIfNecessary (aliasName, deploy) {\n    la(is.maybe.string(aliasName), 'alias name should be a string', aliasName)\n\n    return nowPipeline.deployments(pkg.name)\n    .then(deploys => {\n      return R.filter(R.prop('alias'))(deploys)\n    })\n    .then(deployed => {\n      console.log('found %d deploy(s) with aliases', deployed.length)\n      debug(deployed)\n\n      if (!deployed.length) {\n        console.log('there is no existing alias')\n        if (!aliasName) {\n          console.log('will skip updating alias to', deploy.url)\n          return\n        }\n        console.log('setting new alias to %s', aliasName)\n        return nowPipeline.now.createAlias(deploy.uid, aliasName)\n          .catch(err => {\n            console.error('Could not create alias')\n            console.error(err.message)\n            console.error('Note: you need to create a FIRST alias to domain MANUALLY')\n            console.error('using a command like this:')\n            console.error(`  now alias ${deployed.url} ${aliasName}`)\n            return Promise.reject(err)\n          })\n      }\n\n      if (deployed.length > 1) {\n        console.log('found %d deployed aliases', deployed.length)\n        console.log('not sure which one to update')\n        return Promise.reject(new Error('Multiple aliases'))\n      }\n\n      la(deployed.length === 1, 'expect single alias')\n\n      const alias = deployed[0]\n      if (alias.uid === deploy.uid) {\n        console.log('The current alias %s points at the same deploy %s',\n          alias.alias, deploy.url)\n        console.log('Nothing to do')\n        return\n      }\n\n      console.log('switching alias %s to point at new deploy %s',\n        alias.alias, deploy.url)\n      la(is.unemptyString(alias.alias), 'invalid alias', alias)\n\n      return nowPipeline.now.createAlias(deploy.uid, alias.alias)\n        .then(result => {\n          debug('createAlias result', result)\n          console.log('switched alias %s to point at %s',\n            alias.alias, deploy.url)\n          console.log('taking down previously aliased deploy',\n            alias.uid)\n          return nowPipeline.remove(alias.uid)\n        })\n        .catch(error => {\n          console.error('error switching alias', deploy.uid, alias.alias)\n          console.error(error)\n          throw error\n        })\n    })\n  })\n\nfunction testDeploy (deploy) {\n  la(is.object(deploy), 'wrong deploy object', deploy)\n  console.log('testing url %s', deploy.url)\n  console.log('passing it as env variable %s', passAsName)\n  console.log('test command \"%s\"', testCommand)\n  la(is.url(deploy.url), 'missing deploy url in', deploy)\n\n  const env = {}\n  env[passAsName] = deploy.url\n  return runCommand(testCommand, env)\n    .then(R.always(deploy))\n}\n\nstart\n  .then(setFullHost)\n  .then(testDeploy)\n  .then(R.tap(deployIsWorking))\n  .then(updateAliasIfNecessary(argv.alias))\n  .catch(err => {\n    console.error('Something went wrong')\n    console.error('Sometimes restarting pipeline can help')\n    console.error(err)\n    process.exit(-1)\n  })\n"
  },
  {
    "path": "bin/prune.js",
    "content": "#!/usr/bin/env node\n\n'use strict'\n\nrequire('console.table')\n\nconst nowPipeline = require('..')\nconst pkg = nowPipeline.getPackage()\n\nfunction nonAliasedDeploys (deploys, aliases) {\n  const aliasedDeploys = aliases.map(alias => alias.deploymentId)\n  return deploys.filter(deploy => {\n    return !aliasedDeploys.includes(deploy.uid)\n  })\n}\n\nPromise.all([\n  nowPipeline.deployments(pkg.name),\n  nowPipeline.aliases(pkg.name)\n]).then(([deploys, aliases]) => {\n  if (deploys.length) {\n    console.table('Deploys', deploys)\n  } else {\n    console.log('No deploys')\n  }\n\n  if (aliases.length) {\n    console.table('Aliases', aliases)\n  } else {\n    console.log('No aliases')\n  }\n\n  const needToPrune = nonAliasedDeploys(deploys, aliases)\n  if (needToPrune.length) {\n    console.table('Will prune deploys', needToPrune)\n  } else {\n    console.log('No deploys to prune')\n  }\n\n  return needToPrune.reduce((prev, deploy) => {\n    return prev.then(() => {\n      console.log(`removing deploy ${deploy.uid} ${deploy.url}`)\n      return nowPipeline.remove(deploy.uid)\n    })\n  }, Promise.resolve())\n}).then(() => {\n  console.log('Done pruning deploys')\n}).catch(err => {\n  console.error(err)\n  process.exit(-1)\n})\n"
  },
  {
    "path": "next-update-travis.sh",
    "content": "#!/bin/bash\n\nset -e\n\nif [ \"$TRAVIS_EVENT_TYPE\" = \"cron\" ]; then\n  if [ \"$GH_TOKEN\" = \"\" ]; then\n    echo \"\"\n    echo \"⛔️ Cannot find environment variable GH_TOKEN ⛔️\"\n    echo \"Please set it up for this script to be able\"\n    echo \"to push results to GitHub\"\n    echo \"ℹ️ The best way is to use semantic-release to set it up\"\n    echo \"\"\n    echo \"  https://github.com/semantic-release/semantic-release\"\n    echo \"\"\n    echo \"npm i -g semantic-release-cli\"\n    echo \"semantic-release-cli setup\"\n    echo \"\"\n    exit 1\n  fi\n\n  echo \"Upgrading dependencies using next-update\"\n  npm i -g next-update\n\n  # you can edit options to allow only some updates\n  # --allow major | minor | patch\n  # --latest true | false\n  # see all options by installing next-update\n  # and running next-update -h\n  next-update --allow minor --latest false\n\n  git status\n  # if package.json is modified we have\n  # new upgrades\n  if git diff --name-only | grep package.json > /dev/null; then\n    echo \"There are new versions of dependencies 💪\"\n    git add package.json\n    echo \"----------- package.json diff -------------\"\n    git diff --staged\n    echo \"-------------------------------------------\"\n    git config --global user.email \"next-update@ci.com\"\n    git config --global user.name \"next-update\"\n    git commit -m \"chore(deps): upgrade dependencies using next-update\"\n    # push back to GitHub using token\n    git remote remove origin\n    # TODO read origin from package.json\n    # or use github api module github\n    # like in https://github.com/semantic-release/semantic-release/blob/caribou/src/post.js\n    git remote add origin https://next-update:$GH_TOKEN@github.com/bahmutov/now-pipeline.git\n    git push origin HEAD:master\n  else\n    echo \"No new versions found ✋\"\n  fi\nelse\n  echo \"Not a cron job, normal test\"\nfi\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"now-pipeline\",\n  \"version\": \"0.0.0-semantic-release\",\n  \"description\": \"Single CI command to deploy new version to Zeit Now, including e2e tests and alias switch\",\n  \"author\": \"Gleb Bahmutov <gleb.bahmutov@gmail.com>\",\n  \"bugs\": \"https://github.com/bahmutov/now-pipeline/issues\",\n  \"config\": {\n    \"pre-git\": {\n      \"commit-msg\": \"simple\",\n      \"pre-commit\": [\n        \"npm prune\",\n        \"npm run deps\",\n        \"npm test\",\n        \"npm run ban\"\n      ],\n      \"pre-push\": [\n        \"npm run secure\",\n        \"npm run license\",\n        \"npm run ban -- --all\",\n        \"npm run size\"\n      ],\n      \"post-commit\": [],\n      \"post-merge\": []\n    }\n  },\n  \"engines\": {\n    \"node\": \">=6\"\n  },\n  \"files\": [\n    \"bin\",\n    \"src/*.js\",\n    \"!src/*-spec.js\"\n  ],\n  \"bin\": {\n    \"now-pipeline\": \"bin/now-pipeline.js\",\n    \"now-pipeline-list\": \"bin/list.js\",\n    \"now-pipeline-prune\": \"bin/prune.js\"\n  },\n  \"homepage\": \"https://github.com/bahmutov/now-pipeline#readme\",\n  \"keywords\": [\n    \"ci\",\n    \"now\",\n    \"test\",\n    \"tool\",\n    \"util\",\n    \"zeit\"\n  ],\n  \"license\": \"MIT\",\n  \"main\": \"src/\",\n  \"noScopeName\": \"now-pipeline\",\n  \"publishConfig\": {\n    \"registry\": \"http://registry.npmjs.org/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bahmutov/now-pipeline.git\"\n  },\n  \"scripts\": {\n    \"ban\": \"ban\",\n    \"deps\": \"deps-ok\",\n    \"issues\": \"git-issues\",\n    \"license\": \"license-checker --production --onlyunknown --csv\",\n    \"lint\": \"standard --verbose --fix src/*.js bin/*.js\",\n    \"pretest\": \"npm run lint\",\n    \"secure\": \"nsp check\",\n    \"size\": \"t=\\\"$(npm pack .)\\\"; wc -c \\\"${t}\\\"; tar tvf \\\"${t}\\\"; rm \\\"${t}\\\";\",\n    \"test\": \"npm run unit\",\n    \"unit\": \"mocha src/*-spec.js\",\n    \"semantic-release\": \"semantic-release pre && npm publish && semantic-release post\"\n  },\n  \"devDependencies\": {\n    \"ban-sensitive-files\": \"1.9.2\",\n    \"deps-ok\": \"1.4.1\",\n    \"git-issues\": \"1.3.1\",\n    \"github-post-release\": \"1.13.1\",\n    \"license-checker\": \"15.0.0\",\n    \"mocha\": \"4.1.0\",\n    \"next-update-travis\": \"1.7.1\",\n    \"nsp\": \"3.2.1\",\n    \"pre-git\": \"3.17.1\",\n    \"semantic-release\": \"6.3.6\",\n    \"simple-commit-message\": \"3.3.2\",\n    \"standard\": \"10.0.3\"\n  },\n  \"dependencies\": {\n    \"check-more-types\": \"2.24.0\",\n    \"console.table\": \"0.10.0\",\n    \"cross-spawn\": \"5.1.0\",\n    \"debug\": \"3.2.6\",\n    \"lazy-ass\": \"1.6.0\",\n    \"minimist\": \"1.2.0\",\n    \"moment\": \"2.24.0\",\n    \"node-sentry-error-reporter\": \"1.8.0\",\n    \"now-client\": \"0.7.0\",\n    \"pkgd\": \"1.1.2\",\n    \"ramda\": \"0.26.1\"\n  },\n  \"release\": {\n    \"analyzeCommits\": \"simple-commit-message\",\n    \"generateNotes\": \"github-post-release\"\n  }\n}\n"
  },
  {
    "path": "src/deploys-with-aliases.js",
    "content": "'use strict'\n\nconst is = require('check-more-types')\nconst la = require('lazy-ass')\nconst debug = require('debug')('now-pipeline')\n\nfunction combineDeploysAndAliases ({deploys, aliases}) {\n  la(is.array(deploys), 'list of deploys missing', deploys)\n  la(is.array(aliases), 'list of aliases missing', aliases)\n\n  debug('matching %d deploys with %d aliases', deploys.length, aliases.length)\n\n  return deploys.map(deploy => {\n    const alias = aliases.find(a => a.deploymentId === deploy.uid)\n    if (alias) {\n      deploy.alias = alias.alias\n      deploy.aliasId = alias.uid\n      debug('deploy %s matched alias %s', deploy.url, deploy.alias)\n    }\n    return deploy\n  })\n}\n\nmodule.exports = combineDeploysAndAliases\n"
  },
  {
    "path": "src/index.js",
    "content": "'use strict'\n\nrequire('console.table')\n\nconst debug = require('debug')('now-pipeline')\nconst R = require('ramda')\nconst path = require('path')\nconst fs = require('fs')\nconst is = require('check-more-types')\nconst la = require('lazy-ass')\nconst Now = require('now-client')\nconst combineDeploysAndAliases = require('./deploys-with-aliases')\nconst moment = require('moment')\n\nfunction getPackage () {\n  const packageFilename = path.join(process.cwd(), 'package.json')\n  const pkg = require(packageFilename)\n  return pkg\n}\n\nfunction addRelativeTimes (deploys) {\n  return deploys.map(deploy => {\n    // relative time without \"ago\" suffix\n    deploy.when = moment(Number(deploy.created)).fromNow(true)\n    return deploy\n  })\n}\n\nfunction sortByAge (deploys) {\n  return R.sortBy(\n    R.compose(Number, R.prop('created'))\n  )(deploys)\n}\n\nfunction nowApi () {\n  const authToken = process.env.NOW_TOKEN\n  if (!authToken) {\n    console.log('WARNING: Cannot find NOW_TOKEN environment variable')\n  }\n\n  const now = Now(authToken)\n\n  function wait (seconds) {\n    return new Promise((resolve, reject) => {\n      setTimeout(resolve, seconds * 1000)\n    })\n  }\n\n  function checkDeploy (id) {\n    la(is.unemptyString(id), 'expected deploy id', id)\n    return now.getDeployment(id)\n  }\n\n  function getDeploysAndAliases () {\n    return Promise.all([\n      now.getDeployments(),\n      now.getAliases()\n    ]).then(([deploys, aliases]) => combineDeploysAndAliases({deploys, aliases}))\n  }\n\n  function isDeploying (state) {\n    return state === 'DEPLOYING' || state === 'BOOTED' || state === 'BUILDING'\n  }\n\n  function waitUntilDeploymentReady (id, secondsRemaining) {\n    la(is.number(secondsRemaining), 'wrong waiting limit', secondsRemaining)\n    const sleepSeconds = 5\n    return checkDeploy(id)\n      .then(r => {\n        console.log(r.state, r.host, 'limit', secondsRemaining, 'seconds')\n        if (r.state === 'READY') {\n          return r\n        }\n        if (isDeploying(r.state)) {\n          if (secondsRemaining < sleepSeconds) {\n            throw new Error('Deploy timed out\\n' + JSON.stringify(r))\n          }\n          return wait(sleepSeconds)\n            .then(() => waitUntilDeploymentReady(id, secondsRemaining - sleepSeconds))\n        }\n        throw new Error('Something went wrong with the deploy\\n' + JSON.stringify(r))\n      })\n  }\n\n  const api = {\n    now, // expose the actual now client\n    getPackage,\n    // lists current deploy optionally limited with given predicate\n    deployments (filter) {\n      if (is.string(filter)) {\n        filter = R.propEq('name', filter)\n      }\n      filter = filter || R.T\n      return getDeploysAndAliases()\n        .then(sortByAge)\n        .then(addRelativeTimes)\n        .then(R.filter(filter))\n    },\n    aliases () {\n      return now.getAliases()\n    },\n    remove (id) {\n      la(is.unemptyString(id), 'expected deployment id', id)\n      debug('deleting deployment %s', id)\n      return now.deleteDeployment(id)\n    },\n    /**\n      deploys given filenames. Returns object with result\n      {\n        uid: 'unique id',\n        host: 'now-pipeline-test-lqsibottrb.now.sh',\n        state: 'READY'\n      }\n    */\n    deploy (filenames) {\n      debug('deploying %d files', filenames.length)\n      debug(filenames)\n\n      la(is.strings(filenames), 'missing file names', filenames)\n      la(is.not.empty(filenames), 'expected list of files', filenames)\n\n      // Files not required, but might be checked for by la\n      const optionalFiles = ['.npmignore']\n\n      filenames.forEach(name => {\n        if (!(optionalFiles.includes(name))) {\n          la(fs.existsSync(name), 'cannot find file', name)\n        }\n      })\n\n      const isPackageJson = R.test(/package\\.json$/)\n      const packageJsonPresent = R.any(isPackageJson)\n      la(packageJsonPresent(filenames),\n        'missing package.json file in the list', filenames)\n\n      const packageJsonFilename = filenames.find(isPackageJson)\n      const packageJsonFolder = path.dirname(packageJsonFilename)\n      debug('package.json filename is', packageJsonFilename)\n      debug('in folder', packageJsonFolder)\n\n      // TODO make sure all files exist\n\n      const sources = R.map(name => fs.readFileSync(name, 'utf8'))(filenames)\n      const names = R.map(filename => {\n        return path.relative(packageJsonFolder, filename)\n      })(filenames)\n      debug('sending files', names)\n\n      const params = R.zipObj(names, sources)\n      // parsed JSON object\n      params.package = JSON.parse(params['package.json'])\n      delete params['package.json']\n\n      // we do not need dev dependencies in the deployed server\n      delete params.package.devDependencies\n\n      return now.createDeployment(params)\n        .then(r => {\n          // TODO make an option\n          const maxWaitSeconds = 60 * 10\n          return waitUntilDeploymentReady(r.uid, maxWaitSeconds)\n        })\n        .catch(r => {\n          if (is.error(r)) {\n            console.error('error during deployment')\n            console.error(r.message)\n            return Promise.reject(r)\n          }\n          console.error('error')\n          console.error(r.response.data)\n          return Promise.reject(new Error(r.response.data.err.message))\n        })\n    }\n  }\n  return api\n}\n\nconst now = nowApi()\n\nmodule.exports = now\n\n//\n// examples\n//\n// function showDeploysForProject () { // eslint-disable-line no-unused-vars\n//   const name = 'now-pipeline-test'\n//   now.deployments(R.propEq('name', name))\n//     .then(console.table).catch(console.error)\n// }\n\n// function showAllDeploys () { // eslint-disable-line no-unused-vars\n//   now.deployments().then(console.table).catch(console.error)\n// }\n\n// function deployTest () { // eslint-disable-line no-unused-vars\n//   const relative = require('path').join.bind(null, __dirname)\n//   const files = [\n//     relative('../test/package.json'),\n//     relative('../test/index.js')\n//   ]\n//   return now.deploy(files)\n// }\n\n// showAllDeploys()\n// showDeploysForProject()\n\n// deployTest()\n//   .then(result => {\n//     console.log('deployment done with result', result)\n//   })\n//   .catch(console.error)\n"
  },
  {
    "path": "src/now-pipeline-spec.js",
    "content": "'use strict'\n\n/* global describe, it */\ndescribe('now-pipeline', () => {\n  it('write this test!', () => {\n  })\n})\n"
  },
  {
    "path": "src/run-command.js",
    "content": "'use strict'\n\nconst la = require('lazy-ass')\nconst is = require('check-more-types')\nconst spawn = require('cross-spawn')\n\nfunction runCommand (command, extraEnv) {\n  if (is.string(command)) {\n    command = command.split(' ')\n  }\n  la(is.array(command), 'expected command and args array', command)\n  la(command.length > 0, 'missing command, needs at least something', command)\n  la(is.object(extraEnv), 'expected env object', extraEnv)\n\n  return new Promise(function (resolve, reject) {\n    const customEnv = Object.assign({}, process.env, extraEnv)\n\n    const spawnOptions = {\n      env: customEnv,\n      stdio: 'inherit'\n    }\n    const prog = command[0]\n    const args = command.slice(1)\n    console.log(`running \"${prog}\" with extra env keys`,\n      Object.keys(extraEnv))\n\n    const proc = spawn(prog, args, spawnOptions)\n\n    proc.on('error', (err) => {\n      console.error('prog error')\n      console.error(err)\n      reject(err)\n    })\n\n    proc.on('close', (code) => {\n      // debug(`${prog} exit code ${code}`)\n      if (code) {\n        const msg = `${prog} exit code ${code}`\n        console.error(msg)\n        return reject(new Error(msg))\n      }\n      resolve()\n    })\n  })\n}\n\nmodule.exports = runCommand\n"
  },
  {
    "path": "test/index.js",
    "content": "const port = process.env.PORT || 4000\nconst foo = require('./subfolder/foo')\nrequire('http').Server((req, res) => {\n  const msg = \"Hi there! \" + foo + \" node \" + process.versions.node;\n  res.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n  res.end(require(\"sign-bunny\")(msg));\n}).listen(port);\nconsole.log('listening on port', port)\n"
  },
  {
    "path": "test/package.json",
    "content": "{\n  \"name\": \"now-pipeline-test\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"sign-bunny\": \"1.0.0\"\n  },\n  \"scripts\": {\n    \"start\": \"node index.js\",\n    \"test\": \"curl $NOW_URL\",\n    \"list\": \"node ../bin/list\",\n    \"prune\": \"node ../bin/prune\",\n    \"deploy\": \"DEBUG=now-pipeline node ../bin/now-pipeline --test 'npm run prod-test' --alias test.bahmutov.com\",\n    \"prod-test\": \"echo Testing deployed url; curl $NOW_URL\"\n  }\n}\n"
  },
  {
    "path": "test/subfolder/foo.js",
    "content": "module.exports = 'foo'\n"
  }
]