[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"env\", {\n      \"targets\": {\n        \"node\": \"6\"\n      }\n    }]\n  ],\n  \"plugins\": [\n    \"transform-object-rest-spread\"\n  ]\n}"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  extends: ['standard', 'prettier'],\n  plugins: ['prettier'],\n  rules: {\n    'prettier/prettier': [\n      'error',\n      {\n        singleQuote: true,\n        semi: false,\n      },\n    ]\n  },\n  env: { mocha: true }\n}"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: typicode\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.log\nnode_modules\nlib\ndist\nstatic\n"
  },
  {
    "path": ".npmignore",
    "content": "src"
  },
  {
    "path": ".stylelintrc",
    "content": "{\n  \"extends\": [ \"stylelint-config-standard\", \"stylelint-config-recess-order\" ]\n}"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"stable\"\n  - \"6\"\nscript:\n  - npm run build\n  - npm test\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n## 0.8.7\n\n* Fix UI menu overflow\n\n## 0.8.6\n\n* Fix `The listener must be a function` error\n\n## 0.8.5\n\n* Fix colors in output\n\n## 0.8.4\n\n* Fix UI crash\n\n## 0.8.3\n\n* Fix error in Edge\n* Improve bundle size\n\n## 0.8.2 \n\n* UI\n\n## 0.8.1\n\n* Fix error page\n\n## 0.8.0\n\n* Create empty `conf.json` if it doesn't exist\n* Update UI\n  * New 2018 style 🎉\n  * Links now open in new tabs (should improve integration with third-party tools)\n* Update all dependencies\n\n__Breaking__\n\n* Drop Internet Explorer 11 support for the UI\n* Drop Node 4 support\n* Self-signed certicate is now generated locally and can be found in `~/.hotel`. Since it's going to be a new one, you'll need to \"trust\" it again to be able to use `https`\n* __`.localhost` is now the default domain and replaces `.dev` domains__ (if present, remove `\"tld\": \"dev\"` from `~/.hotel/conf.json` to use the new default value, then run `hotel stop && hotel start`)\n\n## 0.7.6\n\n* Fix `package.json` not found error\n\n## 0.7.5\n\n* Add [please-upgrade-node](https://github.com/typicode/please-upgrade-node)\n* Chore: update all dependencies\n\n## 0.7.4\n\n* Remove `util.log` which has been deprecated in Node 6\n\n## 0.7.3\n\n* Prevent `hotel ls` from crashing when listing malformed files [#190](https://github.com/typicode/hotel/pull/190)\n\n## 0.7.2\n\n* Update error page UI\n* Update Self-Signed SSL Certificate (__you may need to add an exception again__)\n* Fix Vue warning message in UI\n\n## 0.7.1\n\n* Fix daemon error\n\n## 0.7.0\n\n* Add `run` command\n* Add `http-proxy-env` flag to `hotel add`\n* Drop Node `0.12` support\n\n__Breaking__\n\n* By default no `HTTP_PROXY` env will be passed to servers. To pass `HTTP_PROXY` you need to set it in your server configuration or use the flag `http-proxy-env` when adding your server.\n\n## 0.6.1\n\n* Prevent using unsupported characters with `hotel add --name` [#100](https://github.com/typicode/hotel/issues/100)\n\n## 0.6.0\n\n* Add `--xfwd` and `--change-origin` flags to `hotel add` command\n* Log proxy errors\n\n__Breaking__\n\n* If you want hotel to add `X-Forwarded-*` headers to requests, you need now to explicitly pass `-x/--xfwd` flags when adding a server.\n\n## 0.5.13\n\n* Fix `hotel add` CLI bug\n\n## 0.5.12\n\n* Add dark theme\n* Update `X-Forwarded-Port` header\n* Improve `ember-cli` and `livereload` support\n\n## 0.5.11\n\n* Add more `X-Forwarded-*` headers\n\n## 0.5.10\n\n* Pass `HTTP_PROXY` env to servers started by hotel\n\n## 0.5.9\n\n* UI bug fix\n\n## 0.5.8\n\n* Add `favicon`\n* Fix Safari and IE bug\n\n## 0.5.6\n\n* Fix Safari bug\n\n## 0.5.5\n\n* Add `X-Forwarded-Proto` header for ssl proxy\n* Support an array of environment variables for the CLI option `--env`\n* UI enhancements\n\n## 0.5.4\n\n* Fix Node 0.12 issue\n\n## 0.5.3\n\n* UI tweaks\n\n## 0.5.2\n\n* Fix option alias issue [#109](https://github.com/typicode/hotel/issues/109)\n\n## 0.5.1\n\n* Fix conf issue\n\n## 0.5.0\n\n* Various UI improvements\n* Add URL mapping support, for example `hotel add http://192.168.1.10 --name remote-server`\n* Change `hotel rm` options\n\n## 0.4.22\n\n* UI tweaks\n\n## 0.4.21\n\n* Fix UI issue with IE\n\n## 0.4.20\n\n* Fix UI issue with Safari 9\n\n## 0.4.19\n\n* Support ANSI colors in the browser\n\n## 0.4.18\n\n* Bug fix\n\n## 0.4.17\n\n* Add `proxy` conf, use it if you're behind a corporate proxy.\n* Bug fix\n\n## 0.4.16\n\n* Fix issue with project names containing characters not allowed for a domain name. By default, `hotel add` will now convert name to lower case and will replace space and `_` characters. However, you can still use `-n` to force a specific name or specific characters.\n\n## 0.4.15\n\n* Fix blank page issue in `v0.4.14`.\n\n## 0.4.14\n\n* Fix UI issues.\n\n## 0.4.13\n\n* Fix issue with Node 0.12.\n\n## 0.4.12\n\n* Add wildcard subdomains `http://*.app.localhost`.\n\n## 0.4.11\n\n* Strip ANSI when viewing logs in the browser.\n\n## 0.4.10\n\n* Fix IE and Safari issue (added fetch polyfill).\n\n## 0.4.9\n\n* Add server logs in the browser.\n* Bundle icons to make them available without network access.\n* Bug fixes.\n\n## 0.4.8\n\n* Bug fix\n\n## 0.4.7\n\n* Bundle front-end dependencies to make homepage work without network access.\n* Support subdomains `http://sub.app.localhost`.\n* Support `https` and `wss`.\n\n## 0.4.6\n\n* Bug fixes (0.4.3 to 0.4.5 deprecated).\n* Added `~/.hotel/daemon.pid` file.\n\n## 0.4.3\n\n* UI update.\n* Added top-level domain configuration option `tld`.\n* Added IE support.\n\n## 0.4.2\n\n* Removed `socket.io` dependency.\n\n## 0.4.1\n\n* Added WebSocket support for projects being accessed using local `.localhost` domain.\n\n## 0.4.0\n\n* Added Local `.localhost` domain support for HTTP requests.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# hotel [![](https://badge.fury.io/js/hotel.svg)](https://www.npmjs.com/package/hotel)\n\n> Start apps from your browser and use local domains/https automatically\n\n![](https://i.imgur.com/eDLgWMj.png)\n\n_Tip: if you don't enable local domains, hotel can still be used as a **catalog of local servers**._\n\nHotel works great on any OS (macOS, Linux, Windows) and with __all servers :heart:__\n* Node (Express, Webpack)\n* PHP (Laravel, Symfony)\n* Ruby (Rails, Sinatra, Jekyll)\n* Python (Django)\n* Docker\n* Go\n* Apache, Nginx\n* ...\n\n_To all the amazing people who have answered the Hotel survey, thanks so much <3 !_\n\n## v0.8.0 upgrade\n\n`.localhost` replaces `.dev` local domain and is the new default. See https://ma.ttias.be/chrome-force-dev-domains-https-via-preloaded-hsts/ for context.\n\nIf you're upgrading, please be sure to:\n1. Remove `\"tld\": \"dev\"` from your `~/.hotel/conf.json` file\n2. Run `hotel stop && hotel start`\n3. Refresh your network settings\n\n## Support\n\nIf you are benefiting from hotel, you can support its development on [Patreon](https://patreon.com/typicode).\n\nYou can view the list of Supporters here https://thanks.typicode.com.\n\n## Video\n\n* [Starting apps with Hotel - Spacedojo Code Kata by Josh Owens](https://www.youtube.com/watch?v=BHW4tzctQ0k)\n\n## Features\n\n* __Local domains__ - `http://project.localhost`\n* __HTTPS via local self-signed SSL certificate__ - `https://project.localhost`\n* __Wildcard subdomains__ - `http://*.project.localhost`\n* __Works everywhere__ - macOS, Linux and Windows\n* __Works with any server__ - Node, Ruby, PHP, ...\n* __Proxy__ - Map local domains to remote servers\n* __System-friendly__ - No messing with `port 80`, `/etc/hosts`, `sudo` or additional software\n* Fallback URL - `http://localhost:2000/project`\n* Servers are only started when you access them\n* Plays nice with other servers (Apache, Nginx, ...)\n* Random or fixed ports\n\n## Install\n\n```sh\nnpm install -g hotel && hotel start\n```\n\nHotel requires Node to be installed, if you don't have it, you can simply install it using one of the following method:\n\n* https://github.com/creationix/nvm `nvm install stable`\n* https://brew.sh `brew install node`\n\nYou can also visit https://nodejs.org.\n\n## Quick start\n\n### Local domains (optional)\n\nTo use local `.localhost` domains, you need to configure your network or browser to use hotel's proxy auto-config file or you can skip this step for the moment and go directly to http://localhost:2000\n\n[__See instructions here__](https://github.com/typicode/hotel/blob/master/docs/README.md).\n\n### Add your servers\n\n```sh\n# Add your server to hotel\n~/projects/one$ hotel add 'npm start'\n# Or start your server in the terminal as usual and get a temporary local domain\n~/projects/two$ hotel run 'npm start' \n```\n\nVisit [localhost:2000](http://localhost:2000) or [http(s)://hotel.localhost](http://hotel.localhost).\n\nAlternatively you can directly go to\n\n```\nhttp://localhost:2000/one\nhttp://localhost:2000/two\n```\n\n```\nhttp(s)://one.localhost\nhttp(s)://two.localhost \n```\n\n#### Popular servers examples\n\nUsing other servers? Here are some examples to get you started :)\n\n```sh\nhotel add 'ember server'                               # Ember\nhotel add 'jekyll serve --port $PORT'                  # Jekyll\nhotel add 'rails server -p $PORT -b 127.0.0.1'         # Rails\nhotel add 'python -m SimpleHTTPServer $PORT'           # static file server (Python)\nhotel add 'php -S 127.0.0.1:$PORT'                     # PHP\nhotel add 'docker-compose up'                          # docker-compose\nhotel add 'python manage.py runserver 127.0.0.1:$PORT' # Django\n# ...\n```\n\nOn __Windows__ use `\"%PORT%\"` instead of `'$PORT'`\n\n[__See a Docker example here.__](https://github.com/typicode/hotel/blob/master/docs/Docker.md).\n\n### Proxy requests to remote servers\n\nAdd your remote servers\n\n```sh\n~$ hotel add http://192.168.1.12:1337 --name aliased-address\n~$ hotel add http://google.com --name aliased-domain \n```\n\nYou can now access them using\n\n```sh\nhttp://aliased-address.localhost # will proxy requests to http://192.168.1.12:1337\nhttp://aliased-domain.localhost # will proxy requests to http://google.com\n```\n\n## CLI usage and options\n\n```sh\nhotel add <cmd|url> [opts]\nhotel run <cmd> [opts]\n\n# Examples\n\nhotel add 'nodemon app.js' --out dev.log  # Set output file (default: none)\nhotel add 'nodemon app.js' --name name    # Set custom name (default: current dir name)\nhotel add 'nodemon app.js' --port 3000    # Set a fixed port (default: random port)\nhotel add 'nodemon app.js' --env PATH     # Store PATH environment variable in server config\nhotel add http://192.168.1.10 --name app  # map local domain to URL\n\nhotel run 'nodemon app.js'                # Run server and get a temporary local domain\n\n# Other commands\n\nhotel ls     # List servers\nhotel rm     # Remove server\nhotel start  # Start hotel daemon\nhotel stop   # Stop hotel daemon\n```\n\nTo get help\n\n```sh\nhotel --help\nhotel --help <cmd>\n```\n\n## Port\n\nFor `hotel` to work, your servers need to listen on the PORT environment variable.\nHere are some examples showing how you can do it from your code or the command-line:\n\n```js\nvar port = process.env.PORT || 3000\nserver.listen(port)\n```\n\n```sh\nhotel add 'cmd -p $PORT'  # OS X, Linux\nhotel add \"cmd -p %PORT%\" # Windows\n```\n\n## Fallback URL\n\nIf you're offline or can't configure your browser to use `.localhost` domains, you can __always__ access your local servers by going to [localhost:2000](http://localhost:2000).\n\n## Configurations, logs and self-signed SSL certificate\n\nYou can find hotel related files in `~/.hotel` :\n\n```sh\n~/.hotel/conf.json\n~/.hotel/daemon.log\n~/.hotel/daemon.pid\n~/.hotel/key.pem\n~/.hotel/cert.pem\n~/.hotel/servers/<app-name>.json\n```\n\nBy default, `hotel` uses the following configuration values:\n\n```js\n{\n  \"port\": 2000,\n  \"host\": '127.0.0.1',\n  \n  // Timeout when proxying requests to local domains\n  \"timeout\": 5000,\n  \n  // Change this if you want to use another tld than .localhost\n  \"tld\": 'localhost', \n  \n  // If you're behind a corporate proxy, replace this with your network proxy IP (example: \"1.2.3.4:5000\")\n  \"proxy\": false\n}\n```\n\nTo override a value, simply add it to `~/.hotel/conf.json` and run `hotel stop && hotel start`\n\n## Third-party tools\n\n* [Hotelier](https://github.com/macav/hotelier) Hotelier (Mac & Windows Tray App)\n* [Hotel Clerk](https://github.com/therealklanni/hotel-clerk) OS X menubar\n* [HotelX](https://github.com/djyde/HotelX) Another OS X menubar (only 1.6MB)\n* [alfred-hotel](https://github.com/exah/alfred-hotel) Alfred 3 workflow\n* [Hotel Manager](https://github.com/hardpixel/hotel-manager) Gnome Shell extension\n\n## FAQ\n\n#### Setting a fixed port\n\n```sh\nhotel add --port 3000 'server-cmd $PORT' \n```\n\n#### Adding `X-Forwarded-*` headers to requests\n\n```sh\nhotel add --xfwd 'server-cmd'\n```\n\n#### Setting `HTTP_PROXY` env\n\nUse `--http-proxy-env` flag when adding your server or edit your server configuration in `~/.hotel/servers`\n\n```sh\nhotel add --http-proxy-env 'server-cmd'\n```\n\n#### Proxying requests to a remote `https` server\n\n```sh\nhotel add --change-origin 'https://jsonplaceholder.typicode.com'\n```\n\n_When proxying to a `https` server, you may get an error because your `.localhost` domain doesn't match the host defined in the server certificate. With this flag, `host` header is changed to match the target URL._\n\n#### `ENOSPC` and `EACCES` errors\n\nIf you're seeing one of these errors in `~/.hotel/daemon.log`, this usually means that there's some permissions issues. `hotel` daemon should be started without `sudo` and `~/.hotel` should belong to `$USER`.\n\n```sh\n# to fix permissions\nsudo chown -R $USER: $HOME/.hotel\n```\n\nSee also, https://docs.npmjs.com/getting-started/fixing-npm-permissions\n\n#### Configuring a network proxy IP\n\nIf you're behind a corporate proxy, replace `\"proxy\"` with your network proxy IP in `~/.hotel/conf.json`. For example:\n\n```json\n{\n  \"proxy\": \"1.2.3.4:5000\"\n}\n```\n\n## License\n\nMIT\n\n[Patreon](https://www.patreon.com/typicode) - [Supporters](https://thanks.typicode.com) ✨\n"
  },
  {
    "path": "appveyor.yml",
    "content": "environment:\n  nodejs_version: '6'\n\ninstall:\n  - ps: Install-Product node $env:nodejs_version\n  - npm install --ignore-scripts\n\ntest_script:\n  - node --version\n  - npm --version\n  - npm test\n\nbuild_script: npm run build\n"
  },
  {
    "path": "bin/uninstall.js",
    "content": "require('../lib/scripts/uninstall')()\n"
  },
  {
    "path": "docs/Docker.md",
    "content": "# Docker\n\n[Docker](https://www.docker.com/) is a software container platform that integrates easily into Hotel.\n\n### Dockerfile\n\nA [Dockerfile](https://docs.docker.com/engine/reference/builder/) is used to create a single container. Here is an example:\n\n```\nFROM httpd:2.4\nCOPY ./public-html/ /usr/local/apache2/htdocs/\n```\n\nTo build this image, run the following in the application directory:\n\n```\ndocker build -t my-apache2 .\n```\n\nTo use Hotel for this example, run the following in the application directory:\n\n```\nhotel add 'docker run -dit --name my-running-app my-apache2'\n```\n\n### Docker Compose\n\n[Compose](https://docs.docker.com/compose/) is a tool for defining and running multi-container Docker applications. Here is a simple example:\n\n```\nversion: '3'\nservices:\n  web:\n    build: .\n    ports:\n     - \"5000:5000\"\n```\n\nThis binds the internal port 5000 on the container to port 5000 on the host machine. To build this image, run the following:\n\n`docker-compose build`\n\nTo use Hotel for this, run the following in the application directory:\n\n```\nhotel add 'docker-compose up' -p 5000\n```\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Configuring local .localhost domains\n\n_This step is totally optional and you can use hotel without it._\n\nTo use local `.localhost` domain, you need to configure your browser or network to use hotel's proxy auto-config file which is available at `http://localhost:2000/proxy.pac` [[view file content](../src/daemon/views/proxy-pac.pug)].\n\n__Important__ hotel MUST be running before configuring your network or browser so that `http://localhost:2000/proxy.pac` is available. If hotel is started after and you can't access `.localhost` domains, simply disable/enable network or restart browser.\n\n## Configuring another .tld\n\nYou can edit `~/.hotel/conf.json` to use another Top-level Domain than `.localhost`.\n\n```json\n{\n  \"tld\": \"test\"\n}\n```\n\n__Important__ Don't forget to restart hotel and reload network or browser configuration.\n\n## System configuration (recommended)\n\n##### macOS\n\n`Network Preferences > Advanced > Proxies > Automatic Proxy Configuration`\n\n##### Windows\n\n`Settings > Network and Internet > Proxy > Use setup script`\n\n##### Linux\n\nOn Ubuntu\n\n`System Settings > Network > Network Proxy > Automatic`\n\nFor other distributions, check your network manager and look for proxy configuration. Use browser configuration as an alternative.\n\n## Browser configuration\n\nBrowsers can be configured to use a specific proxy. Use this method as an alternative to system-wide configuration.\n\n##### Chrome\n\nExit Chrome and start it using the following option:\n\n```sh\n# Linux\n$ google-chrome --proxy-pac-url=http://localhost:2000/proxy.pac\n\n# macOS\n$ open -a \"Google Chrome\" --args --proxy-pac-url=http://localhost:2000/proxy.pac\n```\n\n##### Firefox\n\n`Preferences > Advanced > Network > Connection > Settings > Automatic proxy URL configuration`\n\n##### Internet Explorer\n\nUses system network configuration.\n"
  },
  {
    "path": "nodemon.json",
    "content": "{\n  \"exec\": \"babel-node\",\n  \"ignore\": [\n    \"src/app/**/*\",\n    \"src/daemon/public/**/*\"\n  ]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hotel\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Local domains for everyone and more! \",\n  \"main\": \"lib\",\n  \"bin\": \"lib/cli/bin.js\",\n  \"engines\": {\n    \"node\": \">=6\"\n  },\n  \"scripts\": {\n    \"test\": \"ava && npm run lint\",\n    \"lint\": \"eslint . --ignore-path .gitignore && stylelint './src/app/**/*.css'\",\n    \"fix\": \"eslint . --ignore-path .gitignore --fix && stylelint './src/app/**/*.css' --fix\",\n    \"start\": \"run-p start:*\",\n    \"start:webpack\": \"webpack-dev-server --config webpack.dev.js --open\",\n    \"start:nodemon\": \"nodemon -- src/daemon\",\n    \"prepublishOnly\": \"npm run build && pkg-ok\",\n    \"uninstall\": \"node bin/uninstall.js\",\n    \"build\": \"run-s build:*\",\n    \"build:webpack\": \"rimraf dist && webpack --config webpack.prod.js\",\n    \"build:babel\": \"rimraf lib && babel src -d lib --copy-files --ignore src/app\",\n    \"addFixtures\": \"node src/cli/bin add \\\"node index\\\" --dir ./test/fixtures/app && node src/cli/bin add \\\"node index\\\" --dir ./test/fixtures/verbose && node src/cli/bin add http://some-domain.com --name local-domain \"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/typicode/hotel.git\"\n  },\n  \"keywords\": [\n    \"dev\",\n    \"devtool\",\n    \"domain\",\n    \"host\",\n    \"https\",\n    \"local\",\n    \"localhost\",\n    \"manager\",\n    \"process\",\n    \"proxy\",\n    \"server\"\n  ],\n  \"author\": \"Typicode <typicode@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/typicode/hotel/issues\"\n  },\n  \"homepage\": \"https://github.com/typicode/hotel\",\n  \"dependencies\": {\n    \"after-all\": \"^2.0.2\",\n    \"ansi2html\": \"0.0.1\",\n    \"chalk\": \"^2.3.1\",\n    \"chokidar\": \"^2.0.2\",\n    \"connect-sse\": \"^1.2.0\",\n    \"exit-hook\": \"^1.1.1\",\n    \"express\": \"^4.16.2\",\n    \"get-port\": \"^3.2.0\",\n    \"http-proxy\": \"^1.17.0\",\n    \"matcher\": \"^1.1.0\",\n    \"mkdirp\": \"^0.5.1\",\n    \"once\": \"^1.3.2\",\n    \"please-upgrade-node\": \"^3.0.2\",\n    \"pug\": \"^2.0.0-rc.4\",\n    \"respawn\": \"^2.4.1\",\n    \"selfsigned\": \"^1.10.2\",\n    \"server-ready\": \"^0.3.1\",\n    \"sudo-block\": \"^2.0.0\",\n    \"tildify\": \"^1.1.2\",\n    \"tinydate\": \"^1.0.0\",\n    \"unquote\": \"^1.1.1\",\n    \"untildify\": \"^3.0.2\",\n    \"update-notifier\": \"^2.3.0\",\n    \"user-startup\": \"^0.2.1\",\n    \"vhost\": \"^3.0.2\",\n    \"yargs\": \"^10.1.2\"\n  },\n  \"devDependencies\": {\n    \"@types/classnames\": \"^2.2.3\",\n    \"@types/escape-html\": \"0.0.20\",\n    \"@types/react\": \"^16.0.38\",\n    \"@types/react-dom\": \"^16.0.4\",\n    \"@types/react-icons\": \"^2.2.5\",\n    \"ava\": \"^0.25.0\",\n    \"babel-cli\": \"^6.26.0\",\n    \"babel-plugin-transform-object-rest-spread\": \"^6.26.0\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"classnames\": \"^2.2.5\",\n    \"css-loader\": \"^0.28.10\",\n    \"escape-html\": \"^1.0.3\",\n    \"eslint\": \"^4.18.1\",\n    \"eslint-config-prettier\": \"^2.9.0\",\n    \"eslint-config-standard\": \"^11.0.0\",\n    \"eslint-plugin-import\": \"^2.9.0\",\n    \"eslint-plugin-node\": \"^5.2.1\",\n    \"eslint-plugin-prettier\": \"^2.6.0\",\n    \"eslint-plugin-promise\": \"^3.6.0\",\n    \"eslint-plugin-standard\": \"^3.0.1\",\n    \"html-webpack-plugin\": \"^2.30.1\",\n    \"husky\": \"^0.15.0-rc.8\",\n    \"lodash.uniqueid\": \"^4.0.1\",\n    \"mobx\": \"^3.5.1\",\n    \"mobx-react\": \"^4.4.2\",\n    \"nodemon\": \"^1.15.1\",\n    \"npm-run-all\": \"^4.1.2\",\n    \"pkg-ok\": \"^2.1.0\",\n    \"prettier\": \"^1.10.2\",\n    \"react\": \"^16.2.0\",\n    \"react-dom\": \"^16.2.0\",\n    \"react-icons\": \"^2.2.7\",\n    \"rimraf\": \"^2.6.2\",\n    \"sinon\": \"^4.4.2\",\n    \"style-loader\": \"^0.19.1\",\n    \"stylelint\": \"^8.4.0\",\n    \"stylelint-config-recess-order\": \"^1.2.3\",\n    \"stylelint-config-standard\": \"^18.1.0\",\n    \"supertest\": \"^3.0.0\",\n    \"tempy\": \"^0.2.0\",\n    \"ts-loader\": \"^3.5.0\",\n    \"tslint\": \"^5.9.1\",\n    \"tslint-config-prettier\": \"^1.9.0\",\n    \"tslint-plugin-prettier\": \"^1.3.0\",\n    \"typescript\": \"^2.7.2\",\n    \"uglifyjs-webpack-plugin\": \"^1.2.2\",\n    \"webpack\": \"^3.11.0\",\n    \"webpack-dev-server\": \"^2.11.1\",\n    \"webpack-merge\": \"^4.1.2\"\n  },\n  \"ava\": {\n    \"serial\": true,\n    \"verbose\": true,\n    \"require\": [\n      \"babel-register\",\n      \"./test/_setup\"\n    ],\n    \"babel\": \"inherit\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm test\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/app/Store.ts",
    "content": "import * as uniqueId from 'lodash.uniqueid'\nimport { action, computed, observable } from 'mobx'\nimport * as api from './api'\nimport { formatLines } from './formatter'\n\nexport interface IProxy {\n  target: string\n}\n\nexport interface ILine {\n  id: string\n  html: string\n}\n\nexport interface IMonitor {\n  cwd: string\n  command: string[]\n  status: string\n  output: ILine[]\n  started: Date\n  pid: number\n}\n\nexport const RUNNING = 'running'\nexport const STOPPED = 'stopped'\nconst MAX_OUTPUT_LENGTH = 1000\n\nfunction clear(servers: Map<string, IMonitor | IProxy>, data: any) {\n  servers.forEach((server, id) => {\n    if (!data.hasOwnProperty(id)) {\n      servers.delete(id)\n    }\n  })\n}\n\nexport default class Store {\n  @observable public isLoading: boolean = true\n  @observable public selectedMonitorId: string = ''\n  @observable public monitors: Map<string, IMonitor> = new Map()\n  @observable public proxies: Map<string, IProxy> = new Map()\n\n  constructor() {\n    this.watchServers()\n    this.watchOutput()\n  }\n\n  @action\n  public watchServers() {\n    api.watchServers(data => {\n      // Delete servers that do not exist anymore in Hotel\n      clear(this.monitors, data)\n      clear(this.proxies, data)\n\n      // Create or update servers\n      Object.keys(data).forEach(id => {\n        const server = data[id]\n        if (this.monitors.has(id) || this.proxies.has(id)) {\n          // Update server state\n          if (server.hasOwnProperty('status')) {\n            Object.assign(this.monitors.get(id), server)\n          } else {\n            Object.assign(this.proxies.get(id), server)\n          }\n        } else {\n          // Create new server\n          if (server.hasOwnProperty('status')) {\n            server.output = []\n            this.monitors.set(id, server)\n          } else {\n            this.proxies.set(id, server)\n          }\n        }\n      })\n\n      // Initial data has been loaded\n      this.isLoading = false\n    })\n  }\n\n  @action\n  public watchOutput() {\n    api.watchOutput(data => {\n      const { id, output } = data\n      const lines = formatLines(output).map(html => ({\n        html,\n        id: uniqueId()\n      }))\n\n      lines.forEach(line => {\n        const monitor = this.monitors.get(id)\n        if (monitor) {\n          monitor.output.push(line)\n\n          if (monitor.output.length > MAX_OUTPUT_LENGTH) {\n            monitor.output.shift()\n          }\n        }\n      })\n    })\n  }\n\n  @action\n  public selectMonitor(monitorId: string) {\n    this.selectedMonitorId =\n      this.selectedMonitorId === monitorId ? '' : monitorId\n  }\n\n  @action\n  public toggleMonitor(monitorId: string) {\n    const monitor = this.monitors.get(monitorId)\n\n    if (monitor) {\n      if (monitor.status === RUNNING) {\n        api.stopMonitor(monitorId)\n        monitor.status = STOPPED // optimistic update\n      } else {\n        api.startMonitor(monitorId)\n        monitor.status = RUNNING\n      }\n    }\n  }\n\n  @action\n  public clearOutput(monitorId: string) {\n    const monitor = this.monitors.get(monitorId)\n    if (monitor) {\n      monitor.output = []\n    }\n  }\n}\n"
  },
  {
    "path": "src/app/api.ts",
    "content": "interface IEvent {\n  data: string\n}\n\nexport function fetchServers() {\n  return window.fetch('/_/servers').then(response => response.json())\n}\n\nexport function watchServers(cb: (data: any) => void) {\n  if (window.EventSource) {\n    new window.EventSource('/_/events').onmessage = (event: IEvent) => {\n      const data = JSON.parse(event.data)\n      cb(data)\n    }\n  } else {\n    setInterval(() => {\n      window\n        .fetch('/_/servers')\n        .then(response => response.json())\n        .then(data => cb(data))\n    }, 1000)\n  }\n}\n\nexport function watchOutput(cb: (data: any) => void) {\n  if (window.EventSource) {\n    new window.EventSource('/_/events/output').onmessage = (event: IEvent) => {\n      const data = JSON.parse(event.data)\n      cb(data)\n    }\n  } else {\n    window.alert(\"Sorry, server logs aren't supported on this browser :(\")\n  }\n}\n\nexport function startMonitor(id: string) {\n  return window.fetch(`/_/servers/${id}/start`, { method: 'POST' })\n}\n\nexport function stopMonitor(id: string) {\n  return window.fetch(`/_/servers/${id}/stop`, { method: 'POST' })\n}\n"
  },
  {
    "path": "src/app/components/App/index.css",
    "content": "* {\n  box-sizing: border-box;\n}\n\n:root {\n  --primary-light: #484848;\n  --primary: #212121;\n  --primary: #282c2f;\n  --primary-dark: #000;\n  --accent: #4c9e97;\n}\n\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: 'Roboto', sans-serif;\n  color: white;\n  background-color: var(--primary-dark);\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  color: #6c6c6f;\n  text-decoration: none;\n}\n\na:hover {\n  color: white;\n  text-decoration: underline;\n}\n\n.nav {\n  height: 100vh;\n}\n\n/* 640px */\n@media (min-width: 40rem) {\n  .container {\n    display: grid;\n    grid-template-columns: 1fr 3fr;\n  }\n}\n"
  },
  {
    "path": "src/app/components/App/index.tsx",
    "content": "import { observer } from 'mobx-react'\nimport * as React from 'react'\nimport Store from '../../Store'\nimport Content from '../Content'\nimport Nav from '../Nav'\nimport Splash from '../Splash'\nimport './index.css'\n\nexport interface IProps {\n  store: Store\n}\n\nfunction App({ store }: IProps) {\n  return (\n    <div className=\"container\">\n      <Nav store={store} />\n      {store.selectedMonitorId ? <Content store={store} /> : <Splash />}\n    </div>\n  )\n}\n\nexport default observer(App)\n"
  },
  {
    "path": "src/app/components/Content/index.css",
    "content": ".content {\n  height: 100vh;\n  overflow-y: scroll;\n  background-color: var(--primary);\n}\n\n.content a {\n  color: white;\n}\n\n.content-bar {\n  position: sticky;\n  top: 0;\n  left: 0;\n  display: flex;\n  justify-content: space-between;\n  padding: 0 0 0 1rem;\n  background-color: var(--primary-dark);\n}\n\n.content-bar > span {\n  align-self: center;\n}\n\npre {\n  padding: 1rem;\n  margin: 0;\n  word-break: break-all;\n}\n\nbutton {\n  padding: 1rem;\n  color: white;\n  cursor: pointer;\n  background: none;\n  border: none;\n  outline: none;\n}\n\nbutton:hover {\n  background: var(--primary);\n}\n"
  },
  {
    "path": "src/app/components/Content/index.tsx",
    "content": "import { observer } from 'mobx-react'\nimport * as React from 'react'\nimport * as MdArrowDownward from 'react-icons/lib/md/arrow-downward'\nimport * as MdClearAll from 'react-icons/lib/md/clear-all'\nimport Link from '../Link'\n\nimport Store from '../../Store'\nimport './index.css'\n\nexport interface IProps {\n  store: Store\n}\n\n@observer\nclass Content extends React.Component<IProps, {}> {\n  private el: HTMLDivElement | null = null\n  private atBottom: boolean = true\n\n  public componentWillUpdate() {\n    if (this.el) {\n      this.atBottom = this.isAtBottom()\n    }\n  }\n\n  public componentDidUpdate() {\n    if (this.atBottom) {\n      this.scrollToBottom()\n    }\n  }\n\n  public isAtBottom() {\n    if (this.el) {\n      const { scrollHeight, scrollTop, clientHeight } = this.el\n      return scrollHeight - scrollTop === clientHeight\n    } else {\n      return true\n    }\n  }\n\n  public scrollToBottom() {\n    if (this.el) {\n      this.el.scrollTop = this.el.scrollHeight\n    }\n  }\n\n  public onScroll() {\n    this.atBottom = this.isAtBottom()\n  }\n\n  public render() {\n    const { store } = this.props\n    const monitor = store.monitors.get(store.selectedMonitorId)\n    return (\n      <div\n        className=\"content\"\n        onScroll={() => this.onScroll()}\n        ref={el => {\n          this.el = el\n        }}\n      >\n        <div className=\"content-bar\">\n          <span>\n            <Link id={store.selectedMonitorId} />\n          </span>\n          <span>\n            <button\n              title=\"Clear output\"\n              onClick={() => store.clearOutput(store.selectedMonitorId)}\n            >\n              <MdClearAll />\n            </button>\n            <button\n              title=\"Scroll to bottom\"\n              onClick={() => this.scrollToBottom()}\n            >\n              <MdArrowDownward />\n            </button>\n          </span>\n        </div>\n        <pre>\n          {monitor &&\n            monitor.output.map(line => (\n              <div\n                key={line.id}\n                dangerouslySetInnerHTML={{ __html: line.html }}\n              />\n            ))}\n        </pre>\n      </div>\n    )\n  }\n}\n\nexport default Content\n"
  },
  {
    "path": "src/app/components/Link/index.tsx",
    "content": "import * as React from 'react'\nimport { IMonitor, IProxy } from '../../Store'\n\nfunction href(id: string) {\n  const { protocol, hostname } = window.location\n  if (/hotel\\./.test(hostname)) {\n    // Accessed using hotel.tld\n    const tld = hostname.split('.').slice(-1)[0]\n    return `${protocol}//${id}.${tld}`\n  } else {\n    // Accessed using localhost\n    return `/${id}`\n  }\n}\n\ninterface IProps {\n  id: string\n}\n\nfunction Link({ id }: IProps) {\n  return (\n    <a href={href(id)} target=\"_blank\" onClick={e => e.stopPropagation()}>\n      {id}\n    </a>\n  )\n}\n\nexport default Link\n"
  },
  {
    "path": "src/app/components/Nav/index.css",
    "content": ".nav {\n  display: flex;\n  flex-direction: column;\n  min-height: 100vh;\n  border-right: 1px solid var(--primary);\n}\n\nheader {\n  padding: 1rem;\n  font-size: 1rem;\n  line-height: 1rem;\n  text-transform: capitalize;\n  border-bottom: 1px solid var(--primary);\n}\n\n.menu {\n  flex: 1;\n  overflow-y: scroll;\n}\n\n.menu.hidden {\n  visibility: hidden;\n}\n\nfooter {\n  padding: 1rem;\n}\n\nh2 {\n  padding: 1rem;\n  margin: 0;\n  font-size: 1rem;\n  font-weight: normal;\n  text-transform: uppercase;\n}\n\nul {\n  padding: 0;\n  margin: 0 0 2rem 0;\n  list-style: none;\n}\n\nli {\n  display: flex;\n  justify-content: space-between;\n  height: 3rem;\n  color: var(--primary-light);\n  transition: border-color 0.2s;\n}\n\nli:focus {\n  outline: none;\n}\n\nli.running * {\n  color: white;\n}\n\nli.monitor:hover {\n  cursor: pointer;\n  background: var(--primary);\n}\n\nli.selected {\n  background: var(--primary);\n}\n\nli > span {\n  align-self: center;\n  padding: 0 1rem;\n}\n\n/* Align Switch vertically */\nli > span:nth-last-child() {\n  line-height: 0;\n}\n\np {\n  padding: 1rem;\n}\n"
  },
  {
    "path": "src/app/components/Nav/index.tsx",
    "content": "import * as classNames from 'classnames'\nimport { observer } from 'mobx-react'\nimport * as React from 'react'\nimport Store, { RUNNING } from '../../Store'\nimport Link from '../Link'\nimport Switch from '../Switch'\nimport './index.css'\n\nconst examples = `~/app$ hotel add 'cmd'\n~/app$ hotel add 'cmd -p $PORT'\n~/app$ hotel add http://192.16.1.2:3000`\n\nexport interface IProps {\n  store: Store\n}\n\nfunction Nav({ store }: IProps) {\n  const { isLoading, selectedMonitorId, monitors, proxies } = store\n  return (\n    <div className=\"nav\">\n      <header>hotel</header>\n      <div className={classNames('menu', { hidden: isLoading })}>\n        {monitors.size === 0 &&\n          proxies.size === 0 && (\n            <div>\n              <p>To add a server, use hotel add</p>\n              <pre>\n                <code>{examples}</code>\n              </pre>\n            </div>\n          )}\n\n        {monitors.size > 0 && (\n          <div>\n            <h2>monitors</h2>\n            <ul>\n              {Array.from(monitors).map(([id, monitor]) => {\n                return (\n                  <li\n                    key={id}\n                    className={classNames('monitor', {\n                      running: monitor.status === RUNNING,\n                      selected: id === selectedMonitorId\n                    })}\n                    onClick={() => store.selectMonitor(id)}\n                  >\n                    <span>\n                      <Link id={id} />\n                    </span>\n                    <span>\n                      <Switch\n                        onClick={() => store.toggleMonitor(id)}\n                        checked={monitor.status === RUNNING}\n                      />\n                    </span>\n                  </li>\n                )\n              })}\n            </ul>\n          </div>\n        )}\n\n        {proxies.size > 0 && (\n          <div>\n            <h2>proxies</h2>\n            <ul>\n              {Array.from(proxies).map(([id, proxy]) => {\n                return (\n                  <li key={id}>\n                    <span>\n                      <Link id={id} />\n                    </span>\n                  </li>\n                )\n              })}\n            </ul>\n          </div>\n        )}\n      </div>\n      <footer>\n        <a href=\"https://github.com/typicode/hotel\" target=\"_blank\">\n          README\n        </a>\n      </footer>\n    </div>\n  )\n}\n\nexport default observer(Nav)\n"
  },
  {
    "path": "src/app/components/Splash/index.css",
    "content": ".splash {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 100vh;\n}\n"
  },
  {
    "path": "src/app/components/Splash/index.tsx",
    "content": "import * as React from 'react'\nimport './index.css'\n\nfunction Splash() {\n  return <div className=\"splash\">{/* Hotel ホテル */}</div>\n}\n\nexport default Splash\n"
  },
  {
    "path": "src/app/components/Switch/index.css",
    "content": ".switch {\n  position: relative;\n  display: inline-block;\n  width: 30px;\n  height: 17px;\n}\n\n.switch input {\n  display: none;\n}\n\n.slider {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  cursor: pointer;\n  background-color: #ccc;\n  transition: 0.4s;\n}\n\n.slider::before {\n  position: absolute;\n  bottom: 2px;\n  left: 2px;\n  width: 13px;\n  height: 13px;\n  content: \"\";\n  background-color: white;\n  transition: 0.4s;\n}\n\ninput:checked + .slider {\n  background-color: var(--accent);\n}\n\ninput:focus + .slider {\n  box-shadow: 0 0 1px var(--primary-dark); /* works ? */\n}\n\ninput:checked + .slider::before {\n  transform: translateX(13px);\n}\n\n/* Rounded sliders */\n.slider.round {\n  border-radius: 17px;\n}\n\n.slider.round::before {\n  border-radius: 50%;\n}\n"
  },
  {
    "path": "src/app/components/Switch/index.tsx",
    "content": "import * as React from 'react'\nimport './index.css'\n\nexport interface IProps {\n  onClick?: () => void\n  checked?: boolean\n}\n\nfunction Switch({ onClick = () => null, checked }: IProps) {\n  return (\n    <label\n      className=\"switch\"\n      onClick={e => {\n        e.stopPropagation()\n        e.preventDefault()\n        onClick()\n      }}\n    >\n      <input type=\"checkbox\" checked={checked} />\n      <span className=\"slider\" />\n    </label>\n  )\n}\n\nexport default Switch\n"
  },
  {
    "path": "src/app/formatter.ts",
    "content": "import * as ansi2HTML from 'ansi2html'\nimport * as escapeHTML from 'escape-html'\nimport { IMonitor } from './Store'\n\nfunction blankLine(val: string) {\n  return val.trim() === '' ? '&nbsp;' : val\n}\n\nexport function formatLines(str: string): string[] {\n  return str\n    .replace(/\\n$/, '')\n    .split('\\n')\n    .map(escapeHTML)\n    .map(blankLine)\n    .map(ansi2HTML)\n}\n\nexport function statusTitle(monitor: IMonitor) {\n  return monitor.pid\n    ? `PID ${monitor.pid}\\nStarted since ${new Date(\n        monitor.started\n      ).toLocaleString()}`\n    : ''\n}\n"
  },
  {
    "path": "src/app/global.d.ts",
    "content": "declare module 'ansi2html'\ndeclare module 'lodash.uniqueid'\ndeclare module 'react-icons/lib/md/arrow-downward'\ndeclare module 'react-icons/lib/md/clear-all'\ninterface Window { EventSource: any }\n"
  },
  {
    "path": "src/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"/>\n    <title>Hotel</title>\n    <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"https://fonts.googleapis.com/css?family=Roboto\">\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>"
  },
  {
    "path": "src/app/index.tsx",
    "content": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport App from './components/App'\nimport Store from './Store'\n\nconst store = new Store()\nReactDOM.render(<App store={store} />, document.getElementById('root'))\n"
  },
  {
    "path": "src/cli/bin.js",
    "content": "#!/usr/bin/env node\nconst pkg = require('../../package.json')\nrequire('please-upgrade-node')(pkg)\n\nconst updateNotifier = require('update-notifier')\nconst sudoBlock = require('sudo-block')\n\nsudoBlock('\\nShould not be run as root, please retry without sudo.\\n')\nupdateNotifier({ pkg }).notify()\nrequire('./')(process.argv)\n"
  },
  {
    "path": "src/cli/daemon.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst mkdirp = require('mkdirp')\nconst startup = require('user-startup')\nconst common = require('../common')\nconst conf = require('../conf')\nconst uninstall = require('../scripts/uninstall')\n\nmodule.exports = {\n  start,\n  stop\n}\n\n// Start daemon in background\nfunction start() {\n  const node = process.execPath\n  const daemonFile = path.join(__dirname, '../daemon')\n  const startupFile = startup.getFile('hotel')\n\n  startup.create('hotel', node, [daemonFile], common.logFile)\n\n  // Save startup file path in ~/.hotel\n  // Will be used later by uninstall script\n  mkdirp.sync(common.hotelDir)\n  fs.writeFileSync(common.startupFile, startupFile)\n\n  console.log(`Started http://localhost:${conf.port}`)\n}\n\n// Stop daemon\nfunction stop() {\n  startup.remove('hotel')\n  // kills process and clean stuff in ~/.hotel\n  uninstall()\n  console.log('Stopped')\n}\n"
  },
  {
    "path": "src/cli/index.js",
    "content": "const yargs = require('yargs')\nconst servers = require('./servers')\nconst run = require('./run')\nconst daemon = require('./daemon')\nconst pkg = require('../../package.json')\n\nconst addOptions = {\n  name: {\n    alias: 'n',\n    describe: 'Server name'\n  },\n  port: {\n    alias: 'p',\n    describe: 'Set PORT environment variable',\n    number: true\n  },\n  out: {\n    alias: 'o',\n    describe: 'Output file'\n  },\n  env: {\n    alias: 'e',\n    describe: 'Additional environment variables',\n    array: true\n  },\n  xfwd: {\n    alias: 'x',\n    describe: 'Adds x-forward headers',\n    default: false,\n    boolean: true\n  },\n  'change-origin': {\n    alias: 'co',\n    describe: 'Changes the origin of the host header to the target URL',\n    default: false,\n    boolean: true\n  },\n  'http-proxy-env': {\n    describe: 'Adds HTTP_PROXY environment variable',\n    default: false,\n    boolean: true\n  },\n  dir: {\n    describe: 'Server directory',\n    string: true\n  }\n}\n\nmodule.exports = processArgv =>\n  yargs(processArgv.slice(2))\n    .version(pkg.version)\n    .alias('v', 'version')\n    .help('h')\n    .alias('h', 'help')\n    .command(\n      'add <cmd_or_url> [options]',\n      'Add server or proxy',\n      yargs => yargs.options(addOptions),\n      // .demand(1),\n      argv => servers.add(argv['cmd_or_url'], argv)\n    )\n    .command(\n      'run <cmd> [options]',\n      'Run server and get a temporary local domain',\n      yargs => {\n        const runOptions = { ...addOptions }\n        delete runOptions['out']\n        return yargs.options(runOptions)\n        // TODO demand(1) ?\n      },\n      argv => run.spawn(argv['cmd'], argv)\n    )\n    .command(\n      'rm [options]',\n      'Remove server or proxy',\n      yargs => {\n        yargs.option('name', {\n          alias: 'n',\n          describe: 'Name'\n        })\n      },\n      argv => servers.rm(argv)\n    )\n    .command('ls', 'List servers', {}, argv => servers.ls(argv))\n    .command('start', 'Start daemon', {}, () => daemon.start())\n    .command('stop', 'Stop daemon', {}, () => daemon.stop())\n    .example('$0 add --help')\n    .example('$0 add nodemon')\n    .example('$0 add npm start')\n    .example(\"$0 add 'cmd -p $PORT'\")\n    .example(\"$0 add 'cmd -p $PORT' --port 4000\")\n    .example(\"$0 add 'cmd -p $PORT' --out app.log\")\n    .example(\"$0 add 'cmd -p $PORT' --name app\")\n    .example(\"$0 add 'cmd -p $PORT' --env PATH\")\n    .example('$0 add http://192.168.1.10 -n app ')\n    .example('$0 rm')\n    .example('$0 rm -n app')\n    .epilog('https://github.com/typicode/hotel')\n    .demand(1)\n    .strict()\n    .help().argv\n"
  },
  {
    "path": "src/cli/run.js",
    "content": "const cp = require('child_process')\nconst getPort = require('get-port')\nconst servers = require('./servers')\nconst getCmd = require('../get-cmd')\n\nconst signals = ['SIGINT', 'SIGTERM', 'SIGHUP']\n\nmodule.exports = {\n  // For testing purpose, allows stubbing cp.spawnSync\n  _spawnSync(...args) {\n    cp.spawnSync(...args)\n  },\n\n  // For testing purpose, allows stubbing process.exit\n  _exit(...args) {\n    process.exit(...args)\n  },\n\n  spawn(cmd, opts = {}) {\n    const cleanAndExit = (code = 0) => {\n      servers.rm(opts)\n      this._exit(code)\n    }\n\n    const startServer = port => {\n      const serverAddress = `http://localhost:${port}`\n\n      process.env.PORT = port\n      servers.add(serverAddress, opts)\n\n      signals.forEach(signal => process.on(signal, cleanAndExit))\n\n      const [command, ...args] = getCmd(cmd)\n      const { status, error } = this._spawnSync(command, args, {\n        stdio: 'inherit',\n        cwd: process.cwd()\n      })\n\n      if (error) throw error\n      cleanAndExit(status)\n    }\n\n    if (opts.port) {\n      startServer(opts.port)\n    } else {\n      getPort()\n        .then(startServer)\n        .catch(err => {\n          throw err\n        })\n    }\n  }\n}\n"
  },
  {
    "path": "src/cli/servers.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst chalk = require('chalk')\nconst tildify = require('tildify')\nconst mkdirp = require('mkdirp')\nconst common = require('../common')\n\nconst serversDir = common.serversDir\n\nmodule.exports = {\n  add,\n  rm,\n  ls\n}\n\nfunction isUrl(str) {\n  return /^(http|https):/.test(str)\n}\n\n// Converts '_-Some Project_Name--' to 'some-project-name'\nfunction domainify(str) {\n  return (\n    str\n      .toLowerCase()\n      // Replace all _ and spaces with -\n      .replace(/(_| )/g, '-')\n      // Trim - characters\n      .replace(/(^-*|-*$)/g, '')\n  )\n}\n\nfunction getId(cwd) {\n  return domainify(path.basename(cwd))\n}\n\nfunction getServerFile(id) {\n  return `${serversDir}/${id}.json`\n}\n\nfunction add(param, opts = {}) {\n  mkdirp.sync(serversDir)\n\n  const cwd = opts.dir || process.cwd()\n  const id = opts.name ? domainify(opts.name) : getId(cwd)\n\n  const file = getServerFile(id)\n\n  let conf = {}\n\n  if (opts.xfwd) {\n    conf.xfwd = opts.xfwd\n  }\n\n  if (opts.changeOrigin) {\n    conf.changeOrigin = opts.changeOrigin\n  }\n\n  if (opts.httpProxyEnv) {\n    conf.httpProxyEnv = opts.httpProxyEnv\n  }\n\n  if (isUrl(param)) {\n    conf = {\n      target: param,\n      ...conf\n    }\n  } else {\n    conf = {\n      cwd,\n      cmd: param,\n      ...conf\n    }\n\n    if (opts.o) conf.out = opts.o\n\n    conf.env = {}\n\n    // By default, save PATH env for version managers users\n    conf.env.PATH = process.env.PATH\n\n    // Copy other env option\n    if (opts.env) {\n      opts.env.forEach(key => {\n        const value = process.env[key]\n        if (value) {\n          conf.env[key] = value\n        }\n      })\n    }\n\n    // Copy port option\n    if (opts.port) {\n      conf.env.PORT = opts.port\n    }\n  }\n\n  const data = JSON.stringify(conf, null, 2)\n\n  console.log(`Create ${tildify(file)}`)\n  fs.writeFileSync(file, data)\n\n  // if we're mapping a domain to a URL there's no additional info to output\n  if (conf.target) return\n\n  // if we're mapping a domain to a local server add some info\n  if (conf.out) {\n    const logFile = tildify(path.resolve(conf.out))\n    console.log(`Output ${logFile}`)\n  } else {\n    console.log(\"Output No log file specified (use '-o app.log')\")\n  }\n\n  if (!opts.p) {\n    console.log(\"Port Random port (use '-p 1337' to set a fixed port)\")\n  }\n}\n\nfunction rm(opts = {}) {\n  const cwd = process.cwd()\n  const id = opts.n || getId(cwd)\n  const file = getServerFile(id)\n\n  console.log(`Remove  ${tildify(file)}`)\n  if (fs.existsSync(file)) {\n    fs.unlinkSync(file)\n    console.log('Removed')\n  } else {\n    console.log('No such file')\n  }\n}\n\nfunction ls() {\n  mkdirp.sync(serversDir)\n\n  const list = fs\n    .readdirSync(serversDir)\n    .map(file => {\n      const id = path.basename(file, '.json')\n      const serverFile = getServerFile(id)\n      let server\n\n      try {\n        server = JSON.parse(fs.readFileSync(serverFile))\n      } catch (error) {\n        // Ignore mis-named or malformed files\n        return\n      }\n\n      if (server.cmd) {\n        return `${id}\\n${chalk.gray(tildify(server.cwd))}\\n${chalk.gray(\n          server.cmd\n        )}`\n      } else {\n        return `${id}\\n${chalk.gray(server.target)}`\n      }\n    })\n    .filter(item => item)\n    .join('\\n\\n')\n\n  console.log(list)\n}\n"
  },
  {
    "path": "src/common.js",
    "content": "const path = require('path')\nconst homedir = require('os').homedir()\n\nconst hotelDir = path.join(homedir, '.hotel')\n\nmodule.exports = {\n  hotelDir,\n  confFile: path.join(hotelDir, 'conf.json'),\n  serversDir: path.join(hotelDir, 'servers'),\n  pidFile: path.join(hotelDir, 'daemon.pid'),\n  logFile: path.join(hotelDir, 'daemon.log'),\n  startupFile: path.join(hotelDir, 'startup')\n}\n"
  },
  {
    "path": "src/conf.js",
    "content": "const fs = require('fs')\nconst mkdirp = require('mkdirp')\nconst { hotelDir, confFile } = require('./common')\n\n// Create dir\nmkdirp.sync(hotelDir)\n\n// Defaults\nconst defaults = {\n  port: 2000,\n  host: '127.0.0.1',\n  timeout: 5000,\n  tld: 'localhost',\n  // Replace with your network proxy IP (1.2.3.4:5000) if any\n  // For example, if you're behind a corporate proxy\n  proxy: false\n}\n\n// Create empty conf it it doesn't exist\nif (!fs.existsSync(confFile)) fs.writeFileSync(confFile, '{}')\n\n// Read file\nconst conf = JSON.parse(fs.readFileSync(confFile))\n\n// Assign defaults and export\nmodule.exports = { ...defaults, ...conf }\n"
  },
  {
    "path": "src/daemon/app.js",
    "content": "const path = require('path')\nconst http = require('http')\nconst express = require('express')\nconst vhost = require('vhost')\nconst serverReady = require('server-ready')\nconst conf = require('../conf')\n\n// Require routes\nconst IndexRouter = require('./routers')\nconst APIRouter = require('./routers/api')\nconst TLDHost = require('./vhosts/tld')\n\nmodule.exports = group => {\n  const app = express()\n  const server = http.createServer(app)\n\n  // Initialize routes\n  const indexRouter = IndexRouter(group)\n  const api = APIRouter(group)\n  const tldHost = TLDHost(group)\n\n  // requests timeout\n  serverReady.timeout = conf.timeout\n\n  // Templates\n  app.set('views', path.join(__dirname, 'views'))\n  app.set('view engine', 'pug')\n  app.locals.pretty = true\n\n  // API\n  app.use('/_', api)\n\n  // .tld host\n  app.use(vhost(new RegExp(`.*.${conf.tld}`), tldHost))\n\n  // app.get('/', (req, res) => res.render('index'))\n\n  // Static files\n  // vendors, etc...\n  app.use(express.static(path.join(__dirname, 'public')))\n  // front files\n  app.use(express.static(path.join(__dirname, '../../dist')))\n\n  // localhost router\n  app.use(indexRouter)\n\n  // Handle CONNECT, used by WebSockets and https when accessing .localhost domains\n  server.on('connect', (req, socket, head) => {\n    group.handleConnect(req, socket, head)\n  })\n\n  server.on('upgrade', (req, socket, head) => {\n    group.handleUpgrade(req, socket, head)\n  })\n\n  return server\n}\n"
  },
  {
    "path": "src/daemon/group.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst EventEmitter = require('events')\nconst url = require('url')\nconst once = require('once')\nconst getPort = require('get-port')\nconst matcher = require('matcher')\nconst respawn = require('respawn')\nconst afterAll = require('after-all')\nconst httpProxy = require('http-proxy')\nconst serverReady = require('server-ready')\nconst log = require('./log')\nconst tcpProxy = require('./tcp-proxy')\nconst daemonConf = require('../conf')\nconst getCmd = require('../get-cmd')\n\nmodule.exports = () => new Group()\n\nclass Group extends EventEmitter {\n  constructor() {\n    super()\n\n    this._list = {}\n    this._proxy = httpProxy.createProxyServer({\n      xfwd: true\n    })\n  }\n\n  _output(id, data) {\n    this.emit('output', id, data)\n  }\n\n  _log(mon, logFile, data) {\n    mon.tail = mon.tail\n      .concat(data)\n      .split('\\n')\n      .slice(-100)\n      .join('\\n')\n\n    if (logFile) {\n      fs.appendFile(logFile, data, err => {\n        if (err) log(err.message)\n      })\n    }\n  }\n\n  _change() {\n    this.emit('change', this._list)\n  }\n\n  //\n  // Conf\n  //\n\n  list() {\n    return this._list\n  }\n\n  find(id) {\n    return this._list[id]\n  }\n\n  add(id, conf) {\n    if (conf.target) {\n      log(`Add target ${id}`)\n      this._list[id] = conf\n      this._change()\n      return\n    }\n\n    log(`Add server ${id}`)\n\n    const HTTP_PROXY = `http://127.0.0.1:${daemonConf.port}/proxy.pac`\n\n    conf.env = {\n      ...process.env,\n      ...conf.env\n    }\n\n    if (conf.httpProxyEnv) {\n      conf.env = {\n        HTTP_PROXY,\n        HTTPS_PROXY: HTTP_PROXY,\n        http_proxy: HTTP_PROXY,\n        https_proxy: HTTP_PROXY,\n        ...conf.env\n      }\n    }\n\n    let logFile\n    if (conf.out) {\n      logFile = path.resolve(conf.cwd, conf.out)\n    }\n\n    const command = getCmd(conf.cmd)\n\n    const mon = respawn(command, {\n      ...conf,\n      maxRestarts: 0\n    })\n\n    this._list[id] = mon\n\n    // Add proxy config\n    mon.xfwd = conf.xfwd || false\n    mon.changeOrigin = conf.changeOrigin || false\n\n    // Emit output\n    mon.on('stdout', data => this._output(id, data))\n    mon.on('stderr', data => this._output(id, data))\n    mon.on('warn', data => this._output(id, data))\n\n    // Emit change\n    mon.on('start', () => this._change())\n    mon.on('stop', () => this._change())\n    mon.on('crash', () => this._change())\n    mon.on('sleep', () => this._change())\n    mon.on('exit', () => this._change())\n\n    // Log status\n    mon.on('start', () => log(id, 'has started'))\n    mon.on('stop', () => log(id, 'has stopped'))\n    mon.on('crash', () => log(id, 'has crashed'))\n    mon.on('sleep', () => log(id, 'is sleeping'))\n    mon.on('exit', () => log(id, 'child process has exited'))\n\n    // Handle logs\n    mon.tail = ''\n\n    mon.on('stdout', data => this._log(mon, logFile, data))\n    mon.on('stderr', data => this._log(mon, logFile, data))\n    mon.on('warn', data => this._log(mon, logFile, data))\n\n    mon.on('start', () => {\n      mon.tail = ''\n\n      if (logFile) {\n        fs.unlink(logFile, err => {\n          if (err) log(err.message)\n        })\n      }\n    })\n\n    this._change()\n  }\n\n  remove(id, cb) {\n    const item = this.find(id)\n    if (item) {\n      delete this._list[id]\n      this._change()\n\n      if (item.stop) {\n        item.stop(cb)\n        item.removeAllListeners()\n        return\n      }\n    }\n\n    cb && cb()\n  }\n\n  stopAll(cb) {\n    const next = afterAll(cb)\n\n    Object.keys(this._list).forEach(key => {\n      if (this._list[key].stop) {\n        this._list[key].stop(next())\n      }\n    })\n  }\n\n  update(id, conf) {\n    this.remove(id, () => this.add(id, conf))\n  }\n\n  //\n  // Hostname resolver\n  //\n\n  resolve(str) {\n    log(`Resolve ${str}`)\n    const arr = Object.keys(this._list)\n      .sort()\n      .reverse()\n      .map(h => ({\n        host: h,\n        isStrictMatch: matcher.isMatch(str, h),\n        isWildcardMatch: matcher.isMatch(str, `*.${h}`)\n      }))\n\n    const strictMatch = arr.find(h => h.isStrictMatch)\n    const wildcardMatch = arr.find(h => h.isWildcardMatch)\n\n    if (strictMatch) return strictMatch.host\n    if (wildcardMatch) return wildcardMatch.host\n  }\n\n  //\n  // Middlewares\n  //\n\n  exists(req, res, next) {\n    // Resolve using either hostname `app.tld`\n    // or id param `http://localhost:2000/app`\n    const tld = new RegExp(`.${daemonConf.tld}$`)\n    const id = req.params.id\n      ? this.resolve(req.params.id)\n      : this.resolve(req.hostname.replace(tld, ''))\n\n    // Find item\n    const item = this.find(id)\n\n    // Not found\n    if (!id || !item) {\n      const msg = `Can't find server id: ${id}`\n      log(msg)\n      return res.status(404).send(msg)\n    }\n\n    req.hotel = {\n      id,\n      item\n    }\n\n    next()\n  }\n\n  start(req, res, next) {\n    const { item } = req.hotel\n\n    if (item.start) {\n      if (item.env.PORT) {\n        item.start()\n        next()\n      } else {\n        getPort()\n          .then(port => {\n            item.env.PORT = port\n            item.start()\n            next()\n          })\n          .catch(error => {\n            next(error)\n          })\n      }\n    } else {\n      next()\n    }\n  }\n\n  stop(req, res, next) {\n    const { item } = req.hotel\n\n    if (item.stop) {\n      item.stop()\n    }\n\n    next()\n  }\n\n  proxyWeb(req, res, target) {\n    const { xfwd, changeOrigin } = req.hotel.item\n\n    this._proxy.web(\n      req,\n      res,\n      {\n        target,\n        xfwd,\n        changeOrigin\n      },\n      err => {\n        log('Proxy - Error', err.message)\n        const server = req.hotel.item\n        const view = server.start ? 'server-error' : 'target-error'\n        res.status(502).render(view, {\n          err,\n          serverReady,\n          server\n        })\n      }\n    )\n  }\n\n  proxy(req, res) {\n    const [hostname, port] = req.headers.host && req.headers.host.split(':')\n    const { item } = req.hotel\n\n    // Handle case where port is set\n    // http://app.localhost:5000 should proxy to http://localhost:5000\n    if (port) {\n      const target = `http://127.0.0.1:${port}`\n\n      log(`Proxy - http://${req.headers.host} → ${target}`)\n      return this.proxyWeb(req, res, target)\n    }\n\n    // Make sure to send only one response\n    const send = once(() => {\n      const { target } = item\n\n      log(`Proxy - http://${hostname} → ${target}`)\n      this.proxyWeb(req, res, target)\n    })\n\n    if (item.start) {\n      // Set target\n      item.target = `http://localhost:${item.env.PORT}`\n\n      // If server stops, no need to wait for timeout\n      item.once('stop', send)\n\n      // When PORT is open, proxy\n      serverReady(item.env.PORT, send)\n    } else {\n      // Send immediatly if item is not a server started by a command\n      send()\n    }\n  }\n\n  redirect(req, res) {\n    const { id } = req.params\n    const { item } = req.hotel\n\n    // Make sure to send only one response\n    const send = once(() => {\n      log(`Redirect - ${id} → ${item.target}`)\n      res.redirect(item.target)\n    })\n\n    if (item.start) {\n      // Set target\n      item.target = `http://${req.hostname}:${item.env.PORT}`\n\n      // If server stops, no need to wait for timeout\n      item.once('stop', send)\n\n      // When PORT is open, redirect\n      serverReady(item.env.PORT, send)\n    } else {\n      // Send immediatly if item is not a server started by a command\n      send()\n    }\n  }\n\n  parseHost(host) {\n    const [hostname, port] = host.split(':')\n    const tld = new RegExp(`.${daemonConf.tld}$`)\n    const id = this.resolve(hostname.replace(tld, ''))\n    return { id, hostname, port }\n  }\n\n  // Needed to proxy WebSocket from CONNECT\n  handleUpgrade(req, socket, head) {\n    if (req.headers.host) {\n      const { host } = req.headers\n      const { id, port } = this.parseHost(host)\n      const item = this.find(id)\n\n      if (item) {\n        let target\n        if (port && port !== '80') {\n          target = `ws://127.0.0.1:${port}`\n        } else if (item.start) {\n          target = `ws://127.0.0.1:${item.env.PORT}`\n        } else {\n          const { hostname } = url.parse(item.target)\n          target = `ws://${hostname}`\n        }\n        log(`WebSocket - ${host} → ${target}`)\n        this._proxy.ws(req, socket, head, { target }, err => {\n          log('WebSocket - Error', err.message)\n        })\n      } else {\n        log(`WebSocket - No server matching ${id}`)\n      }\n    } else {\n      log('WebSocket - No host header found')\n    }\n  }\n\n  // Handle CONNECT, used by WebSockets and https when accessing .localhost domains\n  handleConnect(req, socket, head) {\n    if (req.headers.host) {\n      const { host } = req.headers\n      const { id, hostname, port } = this.parseHost(host)\n\n      // If https make socket go through https proxy on 2001\n      // TODO find a way to detect https and wss without relying on port number\n      if (port === '443') {\n        return tcpProxy.proxy(socket, daemonConf.port + 1, hostname)\n      }\n\n      const item = this.find(id)\n\n      if (item) {\n        if (port && port !== '80') {\n          log(`Connect - ${host} → ${port}`)\n          tcpProxy.proxy(socket, port)\n        } else if (item.start) {\n          const { PORT } = item.env\n          log(`Connect - ${host} → ${PORT}`)\n          tcpProxy.proxy(socket, PORT)\n        } else {\n          const { hostname, port } = url.parse(item.target)\n          const targetPort = port || 80\n          log(`Connect - ${host} → ${hostname}:${port}`)\n          tcpProxy.proxy(socket, targetPort, hostname)\n        }\n      } else {\n        log(`Connect - Can't find server for ${id}`)\n        socket.end()\n      }\n    } else {\n      log('Connect - No host header found')\n    }\n  }\n}\n"
  },
  {
    "path": "src/daemon/index.js",
    "content": "const exitHook = require('exit-hook')\nconst httpProxy = require('http-proxy')\nconst conf = require('../conf')\nconst pidFile = require('../pid-file')\nconst pem = require('./pem')\nconst log = require('./log')\nconst Group = require('./group')\nconst Loader = require('./loader')\nconst App = require('./app')\n\nconst group = Group()\nconst app = App(group)\n\n// Load and watch files\nLoader(group)\n\n// Create pid file\npidFile.create()\n\n// Clean exit\nexitHook(() => {\n  console.log('Exiting')\n  console.log('Stop daemon')\n  proxy.close()\n  app.close()\n  group.stopAll()\n\n  console.log('Remove pid file')\n  pidFile.remove()\n})\n\n// HTTPS proxy\nconst proxy = httpProxy.createServer({\n  target: {\n    host: '127.0.0.1',\n    port: conf.port\n  },\n  ssl: pem.generate(),\n  ws: true,\n  xfwd: true\n})\n\n// Start HTTPS proxy and HTTP server\nproxy.listen(conf.port + 1)\n\napp.listen(conf.port, conf.host, function() {\n  log(`Server listening on port ${conf.host}:${conf.port}`)\n})\n"
  },
  {
    "path": "src/daemon/loader.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst mkdirp = require('mkdirp')\nconst chokidar = require('chokidar')\nconst log = require('./log')\nconst common = require('../common')\n\nfunction getId(file) {\n  return path.basename(file, '.json')\n}\n\nfunction handleAdd(group, file) {\n  log(`${file} added`)\n  const id = getId(file)\n\n  try {\n    const conf = JSON.parse(fs.readFileSync(file, 'utf8'))\n    group.add(id, conf)\n  } catch (err) {\n    log(`Error: Failed to parse ${file}`, err)\n  }\n}\n\nfunction handleUnlink(group, file, cb) {\n  log(`${file} unlinked`)\n  const id = getId(file)\n  group.remove(id, cb)\n}\n\nfunction handleChange(group, file) {\n  log(`${file} changed`)\n  handleUnlink(group, file, () => {\n    handleAdd(group, file)\n  })\n}\n\nmodule.exports = (group, opts = { watch: true }) => {\n  const dir = common.serversDir\n\n  // Ensure directory exists\n  mkdirp.sync(dir)\n\n  // Watch ~/.hotel/servers\n  if (opts.watch) {\n    log(`Watching ${dir}`)\n    chokidar\n      .watch(dir)\n      .on('add', file => handleAdd(group, file))\n      .on('change', file => handleChange(group, file))\n      .on('unlink', file => handleUnlink(group, file))\n  }\n\n  // Bootstrap\n  fs.readdirSync(dir).forEach(file => {\n    handleAdd(group, path.resolve(dir, file))\n  })\n}\n"
  },
  {
    "path": "src/daemon/log.js",
    "content": "const tinydate = require('tinydate')\nconst stamp = tinydate('{HH}:{mm}:{ss}')\n\nmodule.exports = function log(...args) {\n  console.log(stamp(), '-', ...args)\n}\n"
  },
  {
    "path": "src/daemon/pem.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst tildify = require('tildify')\nconst selfsigned = require('selfsigned')\nconst log = require('./log')\nconst { hotelDir } = require('../common')\n\nconst KEY_FILE = path.join(hotelDir, 'key.pem')\nconst CERT_FILE = path.join(hotelDir, 'cert.pem')\n\nfunction generate() {\n  if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {\n    log(`Reading self-signed certificate in ${tildify(hotelDir)}`)\n    return {\n      key: fs.readFileSync(KEY_FILE, 'utf-8'),\n      cert: fs.readFileSync(CERT_FILE, 'utf-8')\n    }\n  } else {\n    log(`Generating self-signed certificate in ${tildify(hotelDir)}`)\n    const pems = selfsigned.generate([{ name: 'commonName', value: 'hotel' }], {\n      days: 365\n    })\n    fs.writeFileSync(KEY_FILE, pems.private, 'utf-8')\n    fs.writeFileSync(CERT_FILE, pems.cert, 'utf-8')\n\n    return { key: pems.private, cert: pems.cert }\n  }\n}\n\nmodule.exports = {\n  KEY_FILE,\n  CERT_FILE,\n  generate\n}\n"
  },
  {
    "path": "src/daemon/public/error.css",
    "content": "body {\n  font-family: -apple-system, BlinkMacSystemFont,\n    \"Segoe UI\", \"Roboto\", \"Oxygen\",\n    \"Ubuntu\", \"Cantarell\", \"Fira Sans\",\n    \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  color: #212121;\n  padding: 0;\n  margin: 0 auto;\n  display: flex;\n  max-width: 960px;\n  min-height: 100vh;\n  flex-direction: column;\n}\n\nh1, h2 {\n  margin-top: 60px;\n}\n\nli {\n  list-style: square;\n}\n\npre {\n  background: #F9F9F9;\n  padding: 20px;\n}\n\na {\n  color: inherit;\n}\n\nmain {\n  flex: 1;\n}\n\nfooter {\n  padding: 20px 0;\n}\n"
  },
  {
    "path": "src/daemon/routers/api/events.js",
    "content": "const express = require('express')\nconst connectSSE = require('connect-sse')\nconst sse = connectSSE()\n\nfunction listen(res, group, groupEvent, handler) {\n  function removeListener() {\n    // Remove group handler\n    group.removeListener(groupEvent, handler)\n\n    // Remove self\n    res.removeListener('close', removeListener)\n    res.removeListener('finish', removeListener)\n  }\n\n  group.on(groupEvent, handler)\n\n  res.on('close', removeListener)\n  res.on('finish', removeListener)\n}\n\nmodule.exports = group => {\n  const router = express.Router()\n\n  router.get('/', sse, (req, res) => {\n    // Handler\n    function sendState() {\n      res.json(group.list())\n    }\n\n    // Bootstrap\n    sendState()\n\n    // Listen\n    listen(res, group, 'change', sendState)\n  })\n\n  router.get('/output', sse, (req, res) => {\n    function sendOutput(id, data) {\n      res.json({\n        id,\n        output: data.toString()\n      })\n    }\n\n    // Bootstrap\n    const list = group.list()\n    Object.keys(list).forEach(id => {\n      var mon = list[id]\n      if (mon && mon.tail) {\n        sendOutput(id, mon.tail)\n      }\n    })\n\n    // Listen\n    listen(res, group, 'output', sendOutput)\n  })\n\n  return router\n}\n"
  },
  {
    "path": "src/daemon/routers/api/index.js",
    "content": "const express = require('express')\n\nconst ServerRouter = require('./servers')\nconst EventRouter = require('./events')\n\nmodule.exports = group => {\n  const router = express.Router()\n\n  const servers = ServerRouter(group)\n  const events = EventRouter(group)\n\n  router.use('/servers', servers)\n  router.use('/events', events)\n\n  return router\n}\n"
  },
  {
    "path": "src/daemon/routers/api/servers.js",
    "content": "const express = require('express')\n\nmodule.exports = group => {\n  const router = express.Router()\n\n  router.get('/', (req, res) => {\n    res.json(group.list())\n  })\n\n  router.post(\n    '/:id/start',\n    group.exists.bind(group),\n    group.start.bind(group),\n    (req, res) => res.end()\n  )\n\n  router.post(\n    '/:id/stop',\n    group.exists.bind(group),\n    group.stop.bind(group),\n    (req, res) => res.end()\n  )\n\n  return router\n}\n"
  },
  {
    "path": "src/daemon/routers/index.js",
    "content": "const express = require('express')\nconst conf = require('../../conf')\nconst log = require('../log')\n\nmodule.exports = function(group) {\n  const router = express.Router()\n\n  function pac(req, res) {\n    log('Serve proxy.pac')\n    if (conf.proxy) {\n      res.render('proxy-pac-with-proxy', { conf })\n    } else {\n      res.render('proxy-pac', { conf })\n    }\n  }\n\n  router\n    .get('/proxy.pac', pac)\n    .get(\n      '/:id',\n      group.exists.bind(group),\n      group.start.bind(group),\n      group.redirect.bind(group)\n    )\n\n  return router\n}\n"
  },
  {
    "path": "src/daemon/tcp-proxy.js",
    "content": "const net = require('net')\nconst log = require('./log')\n\nmodule.exports = {\n  proxy\n}\n\nfunction proxy(source, targetPort, targetHost) {\n  const target = net.connect(targetPort)\n  source.pipe(target).pipe(source)\n\n  const handleError = err => {\n    log('TCP Proxy - Error', err)\n    source.destroy()\n    target.destroy()\n  }\n\n  source.on('error', handleError)\n  target.on('error', handleError)\n\n  source.write(\n    'HTTP/1.1 200 Connection Established\\r\\n' +\n      'Proxy-agent: Hotel\\r\\n' +\n      '\\r\\n'\n  )\n\n  return target\n}\n"
  },
  {
    "path": "src/daemon/vhosts/tld.js",
    "content": "const express = require('express')\nconst conf = require('../../conf')\nconst log = require('../log')\n\n// *.tld vhost\nmodule.exports = group => {\n  const app = express.Router()\n  const hotelRegExp = new RegExp(`hotel.${conf.tld}$`)\n\n  app.use((req, res, next) => {\n    const { hostname } = req\n\n    // Skip hotel.tld\n    if (hotelRegExp.test(hostname)) {\n      log(`hotel.${conf.tld}`)\n      return next()\n    }\n\n    // If hostname is resolved proxy request\n    group.exists(req, res, () => {\n      group.start(req, res, () => {\n        group.proxy(req, res)\n      })\n    })\n  })\n\n  return app\n}\n"
  },
  {
    "path": "src/daemon/views/_error.pug",
    "content": "doctype html\nhead\n  title Error\n  meta(charset='utf-8')\n  meta(name='viewport', content='width=device-width, initial-scale=1')\n  link(rel='icon', type='image/png', href='favicon.png')\n  style\n    include ../public/error.css\nbody\n  h1 Error\n  main\n    block content\n  \n  footer\n    a(href='https://github.com/typicode/hotel') readme\n"
  },
  {
    "path": "src/daemon/views/proxy-pac-with-proxy.pug",
    "content": ".\n  // Set conf.proxy in ~/.hotel/conf.json to your proxy address and port.\n  // For example: { \"proxy\": \"1.2.3.4:5000\" }\n  //\n  // See also https://en.wikipedia.org/wiki/Private_network\n  function FindProxyForURL (url, host) {\n    if (dnsDomainIs(host, '.#{conf.tld}')) {\n      return 'PROXY 127.0.0.1:#{conf.port}';\n    }\n\n    var address = dnsResolve(host);\n    if (\n      isPlainHostName(host) ||\n      dnsDomainIs(host, '.local') ||\n      isInNet(address, '10.0.0.0', '255.0.0.0') ||\n      isInNet(address, '172.16.0.0',  '255.240.0.0') ||\n      isInNet(address, '192.168.0.0',  '255.255.0.0') ||\n      isInNet(address, '127.0.0.0', '255.255.255.0')\n    ) {\n      return 'DIRECT';\n    }\n\n    return 'PROXY #{conf.proxy}';\n  }\n"
  },
  {
    "path": "src/daemon/views/proxy-pac.pug",
    "content": ".\n  // Proxy only *.#{conf.tld} requests to hotel\n  // Configuration file can be found in ~/.hotel\n  function FindProxyForURL (url, host) {\n    if (dnsDomainIs(host, '.#{conf.tld}')) {\n      return 'PROXY 127.0.0.1:#{conf.port}';\n    }\n\n    return 'DIRECT';\n  }\n"
  },
  {
    "path": "src/daemon/views/server-error.pug",
    "content": "extends _error.pug\n\nblock content\n  p Can't connect to server on PORT=#{server.env.PORT}\n  p: a(href=`http://localhost:${server.env.PORT}`) http://localhost:#{server.env.PORT}\n\n  h2 Possible causes\n  ul\n    li Server crashed or timeout of #{serverReady.timeout}ms exceeded.\n    li Server is not listening on PORT environment variable.\n\n  p Try to reload or check logs.\n\n  h2 Logs\n  pre: code= server.command.join(' ')\n  pre: code= server.tail\n"
  },
  {
    "path": "src/daemon/views/target-error.pug",
    "content": "extends _error.pug\n\nblock content\n  p Cannot proxy request to \n    a(href=server.target)= server.target\n\n  pre: code=err.message\n"
  },
  {
    "path": "src/get-cmd.js",
    "content": "const os = require('os')\nconst unquote = require('unquote')\n\nmodule.exports = cmd =>\n  os.platform() === 'win32'\n    ? ['cmd', '/c'].concat(cmd.split(' '))\n    : ['sh', '-c'].concat(unquote(cmd))\n"
  },
  {
    "path": "src/pid-file.js",
    "content": "const fs = require('fs')\nconst { pidFile } = require('./common')\n\nmodule.exports = {\n  create,\n  read,\n  remove\n}\n\nfunction create() {\n  console.log('create', pidFile, process.pid)\n  return fs.writeFileSync(pidFile, process.pid.toString())\n}\n\nfunction read() {\n  if (fs.existsSync(pidFile)) {\n    return fs.readFileSync(pidFile, 'utf-8')\n  }\n}\n\nfunction remove() {\n  if (fs.existsSync(pidFile)) {\n    fs.unlinkSync(pidFile)\n  }\n}\n"
  },
  {
    "path": "src/scripts/uninstall.js",
    "content": "const fs = require('fs')\nconst { startupFile, pidFile } = require('../common')\n\nfunction killProcess() {\n  if (!fs.existsSync(pidFile)) return\n\n  const pid = fs.readFileSync(pidFile, 'utf-8')\n  try {\n    process.kill(pid)\n  } catch (err) {}\n\n  fs.unlinkSync(pidFile)\n}\n\nfunction removeStartup() {\n  if (!fs.existsSync(startupFile)) return\n  const startupFilePath = fs.readFileSync(startupFile, 'utf-8')\n  fs.unlinkSync(startupFile)\n\n  if (!fs.existsSync(startupFilePath)) return\n  fs.unlinkSync(startupFilePath)\n}\n\nmodule.exports = () => {\n  killProcess()\n  removeStartup()\n}\n"
  },
  {
    "path": "test/_setup.js",
    "content": "const os = require('os')\nconst sinon = require('sinon')\nconst tempy = require('tempy')\n\n// Required by AVA, see package.json\nsinon.stub(os, 'homedir').returns(tempy.directory())\n"
  },
  {
    "path": "test/cli/daemon.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst untildify = require('untildify')\nconst userStartup = require('user-startup')\nconst common = require('../../src/common')\nconst cli = require('../../src/cli')\n\ntest.before(() => {\n  sinon.stub(userStartup, 'create')\n  sinon.stub(userStartup, 'remove')\n  sinon.stub(process, 'kill')\n})\n\ntest('start should start daemon', t => {\n  const node = process.execPath\n  const daemonFile = path.join(__dirname, '../../src/daemon')\n  const daemonLog = path.resolve(untildify('~/.hotel/daemon.log'))\n\n  cli(['', '', 'start'])\n\n  sinon.assert.calledWithExactly(\n    userStartup.create,\n    'hotel',\n    node,\n    [daemonFile],\n    daemonLog\n  )\n\n  t.is(\n    fs.readFileSync(common.startupFile, 'utf-8'),\n    userStartup.getFile('hotel'),\n    'startupFile should point to startup file path'\n  )\n\n  t.pass()\n})\n\ntest('stop should stop daemon', t => {\n  fs.writeFileSync(common.pidFile, '1234')\n\n  cli(['', '', 'stop'])\n\n  sinon.assert.calledWithExactly(userStartup.remove, 'hotel')\n  sinon.assert.calledWithExactly(process.kill, '1234')\n  t.pass()\n})\n"
  },
  {
    "path": "test/cli/run.js",
    "content": "const path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst cli = require('../../src/cli')\nconst servers = require('../../src/cli/servers')\nconst run = require('../../src/cli/run')\n\nconst appDir = path.join(__dirname, '../fixtures/app')\n\ntest('spawn with port', t => {\n  const status = 1\n\n  sinon.spy(servers, 'add')\n  sinon.spy(servers, 'rm')\n\n  // Stub _exit to avoid messing with process.exit\n  sinon.stub(run, '_exit')\n  // Stub _spawnSync to immediately return avoid messing with child_process\n  sinon.stub(run, '_spawnSync').callsFake(() => ({ status }))\n\n  process.chdir(appDir)\n\n  const opts = { port: 5000 }\n\n  run.spawn('node index.js', opts)\n\n  // test that everything was called correctly\n  t.true(servers.add.called)\n  t.regex(\n    servers.add.firstCall.args[0],\n    /http:\\/\\/localhost:/,\n    'should add a target'\n  )\n\n  t.is(servers.add.firstCall.args[1], opts, 'should pass options to add')\n\n  t.true(servers.rm.called)\n  t.is(servers.rm.firstCall.args[0], opts, 'should use same options to remove')\n\n  t.true(run._exit.called)\n  t.is(run._exit.firstCall.args[0], status, 'should exit')\n})\n\ntest('cli run should call run.spawn', t => {\n  sinon.stub(run, 'spawn')\n  cli(['', '', 'run', 'node index.js'])\n\n  t.is(run.spawn.firstCall.args[0], 'node index.js')\n})\n"
  },
  {
    "path": "test/cli/servers.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst servers = require('../../src/cli/servers')\nconst cli = require('../../src/cli')\nconst { serversDir } = require('../../src/common')\n\nconst appDir = path.join(__dirname, '../fixtures/app')\n\ntest('add should create file', t => {\n  process.chdir(appDir)\n  cli(['', '', 'add', 'node index.js'])\n\n  const file = path.join(serversDir, 'app.json')\n  const conf = {\n    cmd: 'node index.js',\n    cwd: process.cwd(),\n    env: {\n      PATH: process.env.PATH\n    }\n  }\n\n  const actual = JSON.parse(fs.readFileSync(file))\n  t.deepEqual(actual, conf)\n})\n\ntest('add should create file with URL safe characters by defaults', t => {\n  cli(['', '', 'add', 'node index.js', '--dir', '/_-Some Project_Name--'])\n\n  const file = path.join(serversDir, 'some-project-name.json')\n\n  t.true(fs.existsSync(file))\n})\n\ntest('add should create file with URL safe characters by defaults', t => {\n  cli(['', '', 'add', 'node index.js', '--name', '/_-Some Project_Name--'])\n\n  const file = path.join(serversDir, 'some-project-name.json')\n\n  t.true(fs.existsSync(file))\n})\n\ntest('add should support options', t => {\n  process.env.FOO = 'FOO_VALUE'\n  process.env.BAR = 'BAR_VALUE'\n  const cmd = 'node index.js'\n  const name = 'project'\n  const port = 3000\n  const out = '/some/path/out.log'\n  const env = ['FOO', 'BAR']\n\n  cli([\n    '',\n    '',\n    'add',\n    cmd,\n    '-n',\n    name,\n    '-p',\n    port,\n    '-o',\n    out,\n    '-e',\n    env[0],\n    env[1],\n    '-x',\n    '--co',\n    '--http-proxy-env'\n  ])\n\n  const file = path.join(serversDir, 'project.json')\n  const conf = {\n    cmd: 'node index.js',\n    cwd: process.cwd(),\n    out: out,\n    env: {\n      PATH: process.env.PATH,\n      FOO: process.env.FOO,\n      BAR: process.env.BAR,\n      PORT: port\n    },\n    xfwd: true,\n    changeOrigin: true,\n    httpProxyEnv: true\n  }\n\n  const actual = JSON.parse(fs.readFileSync(file))\n  t.deepEqual(actual, conf)\n})\n\ntest('add should support option aliases', t => {\n  process.env.FOO = 'FOO'\n  const cmd = 'node index.js'\n  const name = 'alias-test'\n\n  cli(['', '', 'add', cmd, '-n', name])\n\n  const file = path.join(serversDir, 'alias-test.json')\n  t.true(fs.existsSync(file))\n})\n\ntest('add should support URL', t => {\n  cli(['', '', 'add', 'http://1.2.3.4', '-n', 'proxy'])\n\n  const file = path.join(serversDir, 'proxy.json')\n  const conf = {\n    target: 'http://1.2.3.4'\n  }\n\n  const actual = JSON.parse(fs.readFileSync(file))\n  t.deepEqual(actual, conf)\n})\n\ntest('add should support URL and options', t => {\n  cli(['', '', 'add', 'http://1.2.3.4', '-n', 'proxy', '-x', '--co'])\n\n  const file = path.join(serversDir, 'proxy.json')\n  const conf = {\n    target: 'http://1.2.3.4',\n    xfwd: true,\n    changeOrigin: true\n  }\n\n  const actual = JSON.parse(fs.readFileSync(file))\n  t.deepEqual(actual, conf)\n})\n\n/*\nFIXME fails for an unknown reason only in CI, process.chdir doesn't seem to change dir\ntest('rm should remove file', (t) => {\n  const file = path.join(serversDir, 'other-app.json')\n  fs.writeFileSync(file, '')\n\n  process.chdir(otherAppDir)\n  cli(['', '', 'rm'])\n  t.true(!fs.existsSync(file))\n})\n*/\n\ntest('rm should remove file using name', t => {\n  const name = 'some-other-app'\n  const file = path.join(serversDir, `${name}.json`)\n\n  fs.writeFileSync(file, '')\n  cli(['', '', 'rm', '-n', name])\n  t.true(!fs.existsSync(file))\n})\n\ntest('ls', t => {\n  sinon.spy(servers, 'ls')\n  cli(['', '', 'ls'])\n  sinon.assert.calledOnce(servers.ls)\n  t.pass()\n})\n\ntest('ls should ignore non-json files', t => {\n  const name = '.DS_Store'\n  const file = path.join(serversDir, `${name}`)\n  fs.writeFileSync(file, '')\n\n  t.notThrows(() => {\n    cli(['', '', 'ls'])\n  })\n})\n"
  },
  {
    "path": "test/daemon/app.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst http = require('http')\nconst test = require('ava')\nconst request = require('supertest')\nconst conf = require('../../src/conf')\nconst App = require('../../src/daemon/app')\nconst Group = require('../../src/daemon/group')\nconst Loader = require('../../src/daemon/loader')\nconst servers = require('../../src/cli/servers')\n\nconst { tld } = conf\nlet app\n\nfunction ensureDistExists(t) {\n  const exists = fs.existsSync(path.join(__dirname, '../../dist'))\n  t.true(exists, 'dist directory must exist (try to run `npm run build`)')\n}\n\ntest.before(() => {\n  // Set request timeout to 20 seconds instead of 5 seconds for slower CI servers\n  conf.timeout = 20000\n\n  // Fake server to respond to URL\n  http\n    .createServer((req, res) => {\n      res.statusCode = 200\n      res.end(`ok - host: ${req.headers.host}`)\n    })\n    .listen(4000)\n\n  // Add server\n  servers.add('node index.js', {\n    name: 'node',\n    port: 51234,\n    dir: path.join(__dirname, '../fixtures/app'),\n    out: '/tmp/logs/app.log',\n    xfwd: true\n  })\n\n  // Add server with subdomain\n  servers.add('node index.js', {\n    name: 'subdomain.node',\n    port: 51235,\n    dir: path.join(__dirname, '../fixtures/app'),\n    out: '/tmp/logs/app.log'\n  })\n\n  // Add server with custom env\n  process.env.FOO = 'FOO_VALUE'\n  servers.add('node index.js', {\n    name: 'custom-env',\n    port: 51236,\n    dir: path.join(__dirname, '../fixtures/app'),\n    out: '/tmp/logs/app.log',\n    env: ['FOO'],\n    httpProxyEnv: true\n  })\n\n  // Add failing server\n  servers.add('unknown-cmd', { name: 'failing' })\n\n  // Add server and proxy for testing removal\n  servers.add('unknown-cmd', { name: 'server-to-remove' })\n  servers.add('http://example.com', { name: 'proxy-to-remove' })\n\n  // Add URL\n  servers.add('http://localhost:4000', { name: 'proxy' })\n\n  // Add https URL\n  servers.add('https://jsonplaceholder.typicode.com', {\n    name: 'working-proxy-with-https-target',\n    changeOrigin: true\n  })\n\n  servers.add('https://jsonplaceholder.typicode.com', {\n    name: 'failing-proxy-with-https-target'\n  })\n\n  // Add unavailable URL\n  servers.add('http://localhost:4100', { name: 'unavailable-proxy' })\n\n  const group = Group()\n  app = App(group)\n  app.group = group\n  Loader(group, { watch: false })\n})\n\ntest.cb.after(t => app.group.stopAll(t.end))\n\n//\n// Test daemon/vhosts/tld.js\n//\n\ntest.cb('GET http://hotel.tld should return 200', t => {\n  ensureDistExists(t)\n  request(app)\n    .get('/')\n    .set('Host', `hotel.${tld}`)\n    .expect(200, t.end)\n})\n\ntest.cb(\n  'GET http://node.tld should proxy request and host should be node.tld',\n  t => {\n    request(app)\n      .get('/')\n      .set('Host', `node.${tld}`)\n      .expect(new RegExp(`host: node.${tld}`))\n      .expect(200, /Hello World/, (err, res) => {\n        if (err) return t.end(err)\n        t.notRegex(\n          res.text,\n          /http:\\/\\/127.0.0.1:2000\\/proxy.pac/,\n          `shouldn't be started with HTTP_PROXY env set`\n        )\n        return t.end()\n      })\n  }\n)\n\ntest.cb('GET http://subdomain.node.tld should proxy request', t => {\n  request(app)\n    .get('/')\n    .set('Host', `subdomain.node.${tld}`)\n    .expect(200, /Hello World/, t.end)\n})\n\ntest.cb('GET http://any.node.tld should proxy request', t => {\n  request(app)\n    .get('/')\n    .set('Host', `any.node.${tld}`)\n    .expect(200, /Hello World/, t.end)\n})\n\ntest.cb('GET http://unknown.tld should return 404', t => {\n  request(app)\n    .get('/')\n    .set('Host', `unknown.${tld}`)\n    .expect(404, t.end)\n})\n\ntest.cb('GET http://failing.tld should return 502', t => {\n  request(app)\n    .get('/')\n    .set('Host', `failing.${tld}`)\n    .expect(502, t.end)\n})\n\ntest.cb(\n  'GET http://proxy.tld should return 200 and host should be proxy.localhost',\n  t => {\n    request(app)\n      .get('/')\n      .set('Host', `proxy.${tld}`)\n      .expect(200, new RegExp(`host: proxy.${tld}`), t.end)\n  }\n)\n\ntest.cb('GET http://node.tld:4000 should proxy to localhost:4000', t => {\n  request(app)\n    .get('/')\n    .set('Host', `node.${tld}:4000`)\n    .expect(200, /ok/, t.end)\n})\n\n//\n// Test proxy to URLs\n//\n\ntest.cb(\n  'GET http://working-proxy-with-https-target.tld should return 200',\n  t => {\n    request(app)\n      .get('/')\n      .set('Host', `working-proxy-with-https-target.${tld}`)\n      .expect(200, t.end)\n  }\n)\n\ntest.cb(\n  'GET http://failing-proxy-with-https-target.tld should return 502',\n  t => {\n    request(app)\n      .get('/')\n      .set('Host', `failing-proxy-with-https-target.${tld}`)\n      .expect(502, t.end)\n  }\n)\n\ntest.cb('GET http://unavailable-proxy.tld should return 502', t => {\n  request(app)\n    .get('/')\n    .set('Host', `unavailable-proxy.${tld}`)\n    .expect(502, t.end)\n})\n\n//\n// TEST daemon/routers/api.js\n//\n\ntest.cb('GET /_/servers', t => {\n  request(app)\n    .get('/_/servers')\n    .expect(200, (err, res) => {\n      if (err) return t.end(err)\n      t.is(Object.keys(res.body).length, 10, 'got wrong number of servers')\n      t.end()\n    })\n})\n\ntest.cb('POST /_/servers/:id/start', t => {\n  request(app)\n    .post('/_/servers/node/start')\n    .expect(200, err => {\n      if (err) return t.end(err)\n      t.is(app.group.find('node').status, 'running')\n      t.end()\n    })\n})\n\ntest.cb('POST /_/servers/:id/stop', t => {\n  request(app)\n    .post('/_/servers/node/stop')\n    .expect(200, err => {\n      if (err) return t.end(err)\n      t.not(app.group.find('node').status, 'running')\n      t.end()\n    })\n})\n\n//\n// TEST daemon/routers/index.js\n//\n\ntest.cb('GET /proxy.pac should serve /proxy.pac', t => {\n  request(app)\n    .get('/proxy.pac')\n    .expect(200, t.end)\n})\n\ntest.cb('GET http://localhost:2000/node should redirect to node server', t => {\n  if (process.env.APPVEYOR) return t.end()\n  request(app)\n    .get('/node')\n    .set('Host', 'localhost')\n    .expect('location', /http:\\/\\/localhost:51234/)\n    .expect(302, t.end)\n})\n\ntest.cb(\n  'GET http://127.0.0.1:2000/node should use the same hostname to redirect',\n  t => {\n    // temporary disable this test on AppVeyor\n    // Randomly fails\n    if (process.env.APPVEYOR) return t.end()\n    request(app)\n      .get('/node')\n      .expect('location', /http:\\/\\/127.0.0.1:51234/)\n      .expect(302, t.end)\n  }\n)\n\ntest.cb('GET http://localhost:2000/proxy should redirect to target', t => {\n  if (process.env.APPVEYOR) return t.end()\n  request(app)\n    .get('/proxy')\n    .set('Host', 'localhost')\n    .expect('location', /http:\\/\\/localhost:4000/)\n    .expect(302, t.end)\n})\n\n//\n// Test daemon/app.js\n//\n\ntest.cb('GET / should render index.html', t => {\n  ensureDistExists(t)\n  request(app)\n    .get('/')\n    .expect(200, t.end)\n})\n\n//\n// Test env variables\n//\n\ntest.cb('GET / should contain custom env values', t => {\n  request(app)\n    .get('/')\n    .set('Host', `custom-env.${tld}`)\n    .expect(200, /FOO_VALUE/, t.end)\n})\n\ntest.cb('GET / should contain proxy env values', t => {\n  request(app)\n    .get('/')\n    .set('Host', `custom-env.${tld}`)\n    .expect(200, /http:\\/\\/127.0.0.1:2000\\/proxy.pac/, t.end)\n})\n\n//\n// Test headers\n//\n\ntest.cb('GET node.tld/ should contain X-FORWARD headers', t => {\n  request(app)\n    .get('/')\n    .set('Host', `node.${tld}`)\n    .expect(200, new RegExp(`x-forwarded-host: node.${tld}`), t.end)\n})\n\ntest.cb('GET subdomain.node.tld/ should not contain X-FORWARD headers', t => {\n  request(app)\n    .get('/')\n    .set('Host', `subdomain.node.${tld}`)\n    .expect(200, /x-forwarded-host: undefined/, t.end)\n})\n\n//\n// Test remove\n//\n\ntest.cb('Removing a server should make it unavailable', t => {\n  t.truthy(app.group.find('server-to-remove'))\n  app.group.remove('server-to-remove', () => {\n    request(app)\n      .get('/')\n      .set('Host', `server-to-remove.${tld}`)\n      .expect(404, t.end)\n  })\n})\n\ntest.cb('Removing a proxy should make it unavailable', t => {\n  t.truthy(app.group.find('proxy-to-remove'))\n  app.group.remove('proxy-to-remove', () => {\n    request(app)\n      .get('/')\n      .set('Host', `proxy-to-remove.${tld}`)\n      .expect(404, t.end)\n  })\n})\n"
  },
  {
    "path": "test/daemon/group.js",
    "content": "const test = require('ava')\nconst sinon = require('sinon')\nconst Group = require('../../src/daemon/group')\nconst tcpProxy = require('../../src/daemon/tcp-proxy')\nconst conf = require('../../src/conf')\n\nconst { tld } = conf\nsinon.stub(tcpProxy, 'proxy')\n\ntest('group.resolve should find the correct server or target id', t => {\n  const group = Group()\n  const conf = { target: 'http://example.com' }\n  group.add('app', conf)\n  group.add('foo.app', conf)\n\n  t.is(group.resolve('app'), 'app')\n  t.is(group.resolve('bar.app'), 'app')\n  t.is(group.resolve('foo.app'), 'foo.app')\n  t.is(group.resolve('baz.foo.app'), 'foo.app')\n})\n\ntest('group.handleUpgrade with proxy', t => {\n  const group = Group()\n  const target = 'example.com'\n  const req = {\n    headers: {\n      host: `proxy.${tld}:80`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  sinon.stub(group._proxy, 'ws')\n\n  group.add('proxy', { target: `http://${target}` })\n  group.handleUpgrade(req, head, socket)\n\n  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {\n    target: `ws://${target}`\n  })\n  t.pass()\n})\n\ntest('group.handleUpgrade with app', t => {\n  const group = Group()\n  const PORT = '9000'\n  const req = {\n    headers: {\n      host: `app.${tld}:80`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  sinon.stub(group._proxy, 'ws')\n\n  group.add('app', {\n    cmd: 'cmd',\n    cwd: '/some/path',\n    env: {\n      PORT\n    }\n  })\n  group.handleUpgrade(req, head, socket)\n\n  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {\n    target: `ws://127.0.0.1:${PORT}`\n  })\n  t.pass()\n})\n\ntest('group.handleUpgrade with app and port, port should take precedence', t => {\n  const port = 5000\n  const group = Group()\n  const req = {\n    headers: {\n      host: `app.${tld}:${port}`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  sinon.stub(group._proxy, 'ws')\n\n  group.add('app', {\n    cmd: 'cmd',\n    cwd: '/some/path'\n  })\n  group.handleUpgrade(req, head, socket)\n\n  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {\n    target: `ws://127.0.0.1:${port}`\n  })\n  t.pass()\n})\n\ntest('group.handleConnect with proxy', t => {\n  const group = Group()\n  const target = 'example.com'\n  const req = {\n    headers: {\n      host: `proxy.${tld}:80`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  tcpProxy.proxy.reset()\n\n  group.add('proxy', { target: `http://${target}` })\n  group.handleConnect(req, head, socket)\n\n  sinon.assert.calledWith(tcpProxy.proxy, socket, 80, 'example.com')\n  t.pass()\n})\n\ntest('group.handleConnect with app', t => {\n  const group = Group()\n  const PORT = '9000'\n  const req = {\n    headers: {\n      host: `app.${tld}:80`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  tcpProxy.proxy.reset()\n\n  group.add('app', {\n    cmd: 'cmd',\n    cwd: '/some/path',\n    env: {\n      PORT\n    }\n  })\n  group.handleConnect(req, head, socket)\n\n  sinon.assert.calledWith(tcpProxy.proxy, socket, PORT)\n  t.pass()\n})\n\ntest('group.handleConnect on port 443', t => {\n  const group = Group()\n  const req = {\n    headers: {\n      host: `anything.${tld}:443`\n    }\n  }\n  const head = {}\n  const socket = {}\n\n  tcpProxy.proxy.reset()\n  group.handleConnect(req, head, socket)\n\n  sinon.assert.calledWith(tcpProxy.proxy, socket, conf.port + 1)\n  t.pass()\n})\n"
  },
  {
    "path": "test/daemon/pem.js",
    "content": "const fs = require('fs')\nconst test = require('ava')\n// TODO rename to KEY_NAME\nconst { KEY_FILE, CERT_FILE, generate } = require('../../src/daemon/pem')\nconst { hotelDir } = require('../../src/common')\n\ntest.before(() => {\n  fs.mkdirSync(hotelDir)\n})\n\ntest(\"should create cert files if they don't exist\", t => {\n  const { key, cert } = generate()\n  t.true(fs.existsSync(KEY_FILE))\n  t.true(fs.existsSync(CERT_FILE))\n  t.is(key, fs.readFileSync(KEY_FILE, 'utf-8'))\n  t.is(cert, fs.readFileSync(CERT_FILE, 'utf-8'))\n})\n\ntest('should read cert files if they exist', t => {\n  fs.writeFileSync(KEY_FILE, 'foo')\n  fs.writeFileSync(CERT_FILE, 'bar')\n  const { key, cert } = generate()\n  t.is('foo', key)\n  t.is('bar', cert)\n})\n"
  },
  {
    "path": "test/fixtures/app/index.js",
    "content": "var http = require('http')\n\nhttp\n  .createServer(function(req, res) {\n    console.log(req.headers)\n    res.writeHead(200, { 'Content-Type': 'text/plain' })\n    res.end(\n      [\n        'Hello World',\n        process.env.FOO,\n        process.env.HTTP_PROXY,\n        'x-forwarded-host: ' + req.headers['x-forwarded-host'],\n        'host: ' + req.headers.host\n      ].join(' ')\n    )\n  })\n  .listen(process.env.PORT, '127.0.0.1')\n\nconsole.log('Server listening on port', process.env.PORT)\n"
  },
  {
    "path": "test/fixtures/verbose/index.js",
    "content": "setInterval(\n  () =>\n    console.log('[32m green text with blank line - ' + Math.random() + '\\n'),\n  200\n)\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/\",\n    \"sourceMap\": true,\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n    \"jsx\": \"react\",\n    \"strict\": true,\n    \"experimentalDecorators\": true\n    // \"emitDecoratorMetadata\": true,         /* Enables experimental support for emitting type metadata for decorators. */\n  }\n}"
  },
  {
    "path": "tslint.json",
    "content": "{\n    \"defaultSeverity\": \"error\",\n    \"extends\": [\n      \"tslint:recommended\",\n      \"tslint-config-prettier\"\n    ],\n    \"jsRules\": {},\n    \"rulesDirectory\": [\n      \"tslint-plugin-prettier\"\n    ],\n    \"rules\": {\n      \"prettier\": [\n        true,\n        {\n          \"semi\": false,\n          \"singleQuote\": true\n        }\n      ]\n    }\n  }"
  },
  {
    "path": "webpack.common.js",
    "content": "const path = require('path')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nmodule.exports = {\n  entry: './src/app/index.tsx',\n  module: {\n    rules: [\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader']\n      },\n      {\n        test: /\\.tsx?$/,\n        use: 'ts-loader',\n        exclude: /node_modules/\n      }\n    ]\n  },\n  resolve: {\n    extensions: ['.tsx', '.ts', '.js']\n  },\n  output: {\n    filename: 'bundle.js',\n    path: path.resolve(__dirname, 'dist')\n  },\n  plugins: [\n    new HtmlWebpackPlugin({\n      template: 'src/app/index.html'\n    })\n  ]\n}\n"
  },
  {
    "path": "webpack.dev.js",
    "content": "const merge = require('webpack-merge')\nconst common = require('./webpack.common.js')\n\nmodule.exports = merge(common, {\n  devtool: 'inline-source-map',\n  devServer: {\n    contentBase: './dist',\n    proxy: {\n      '/_': 'http://localhost:2000'\n    }\n  }\n})\n"
  },
  {
    "path": "webpack.prod.js",
    "content": "const webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst UglifyJSPlugin = require('uglifyjs-webpack-plugin')\nconst common = require('./webpack.common.js')\n\nmodule.exports = merge(common, {\n  plugins: [\n    new UglifyJSPlugin({\n      sourceMap: true\n    }),\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify('production')\n    })\n  ]\n})\n"
  }
]