Full Code of typicode/hotel for AI

master bcbdade4a5ce cached
76 files
86.2 KB
26.3k tokens
93 symbols
1 requests
Download .txt
Repository: typicode/hotel
Branch: master
Commit: bcbdade4a5ce
Files: 76
Total size: 86.2 KB

Directory structure:
gitextract_2x2tk0um/

├── .babelrc
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .npmignore
├── .stylelintrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── bin/
│   └── uninstall.js
├── docs/
│   ├── Docker.md
│   └── README.md
├── nodemon.json
├── package.json
├── src/
│   ├── app/
│   │   ├── Store.ts
│   │   ├── api.ts
│   │   ├── components/
│   │   │   ├── App/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Content/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Link/
│   │   │   │   └── index.tsx
│   │   │   ├── Nav/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Splash/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   └── Switch/
│   │   │       ├── index.css
│   │   │       └── index.tsx
│   │   ├── formatter.ts
│   │   ├── global.d.ts
│   │   ├── index.html
│   │   └── index.tsx
│   ├── cli/
│   │   ├── bin.js
│   │   ├── daemon.js
│   │   ├── index.js
│   │   ├── run.js
│   │   └── servers.js
│   ├── common.js
│   ├── conf.js
│   ├── daemon/
│   │   ├── app.js
│   │   ├── group.js
│   │   ├── index.js
│   │   ├── loader.js
│   │   ├── log.js
│   │   ├── pem.js
│   │   ├── public/
│   │   │   └── error.css
│   │   ├── routers/
│   │   │   ├── api/
│   │   │   │   ├── events.js
│   │   │   │   ├── index.js
│   │   │   │   └── servers.js
│   │   │   └── index.js
│   │   ├── tcp-proxy.js
│   │   ├── vhosts/
│   │   │   └── tld.js
│   │   └── views/
│   │       ├── _error.pug
│   │       ├── proxy-pac-with-proxy.pug
│   │       ├── proxy-pac.pug
│   │       ├── server-error.pug
│   │       └── target-error.pug
│   ├── get-cmd.js
│   ├── pid-file.js
│   └── scripts/
│       └── uninstall.js
├── test/
│   ├── _setup.js
│   ├── cli/
│   │   ├── daemon.js
│   │   ├── run.js
│   │   └── servers.js
│   ├── daemon/
│   │   ├── app.js
│   │   ├── group.js
│   │   └── pem.js
│   └── fixtures/
│       ├── app/
│       │   └── index.js
│       └── verbose/
│           └── index.js
├── tsconfig.json
├── tslint.json
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": [
    ["env", {
      "targets": {
        "node": "6"
      }
    }]
  ],
  "plugins": [
    "transform-object-rest-spread"
  ]
}

================================================
FILE: .eslintrc.js
================================================
module.exports = {
  extends: ['standard', 'prettier'],
  plugins: ['prettier'],
  rules: {
    'prettier/prettier': [
      'error',
      {
        singleQuote: true,
        semi: false,
      },
    ]
  },
  env: { mocha: true }
}

================================================
FILE: .gitattributes
================================================
* text=auto


================================================
FILE: .github/FUNDING.yml
================================================
github: typicode


================================================
FILE: .gitignore
================================================
.DS_Store
*.log
node_modules
lib
dist
static


================================================
FILE: .npmignore
================================================
src

================================================
FILE: .stylelintrc
================================================
{
  "extends": [ "stylelint-config-standard", "stylelint-config-recess-order" ]
}

================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - "stable"
  - "6"
script:
  - npm run build
  - npm test


================================================
FILE: CHANGELOG.md
================================================
# Change Log

## 0.8.7

* Fix UI menu overflow

## 0.8.6

* Fix `The listener must be a function` error

## 0.8.5

* Fix colors in output

## 0.8.4

* Fix UI crash

## 0.8.3

* Fix error in Edge
* Improve bundle size

## 0.8.2 

* UI

## 0.8.1

* Fix error page

## 0.8.0

* Create empty `conf.json` if it doesn't exist
* Update UI
  * New 2018 style 🎉
  * Links now open in new tabs (should improve integration with third-party tools)
* Update all dependencies

__Breaking__

* Drop Internet Explorer 11 support for the UI
* Drop Node 4 support
* 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`
* __`.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`)

## 0.7.6

* Fix `package.json` not found error

## 0.7.5

* Add [please-upgrade-node](https://github.com/typicode/please-upgrade-node)
* Chore: update all dependencies

## 0.7.4

* Remove `util.log` which has been deprecated in Node 6

## 0.7.3

* Prevent `hotel ls` from crashing when listing malformed files [#190](https://github.com/typicode/hotel/pull/190)

## 0.7.2

* Update error page UI
* Update Self-Signed SSL Certificate (__you may need to add an exception again__)
* Fix Vue warning message in UI

## 0.7.1

* Fix daemon error

## 0.7.0

* Add `run` command
* Add `http-proxy-env` flag to `hotel add`
* Drop Node `0.12` support

__Breaking__

* 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.

## 0.6.1

* Prevent using unsupported characters with `hotel add --name` [#100](https://github.com/typicode/hotel/issues/100)

## 0.6.0

* Add `--xfwd` and `--change-origin` flags to `hotel add` command
* Log proxy errors

__Breaking__

* If you want hotel to add `X-Forwarded-*` headers to requests, you need now to explicitly pass `-x/--xfwd` flags when adding a server.

## 0.5.13

* Fix `hotel add` CLI bug

## 0.5.12

* Add dark theme
* Update `X-Forwarded-Port` header
* Improve `ember-cli` and `livereload` support

## 0.5.11

* Add more `X-Forwarded-*` headers

## 0.5.10

* Pass `HTTP_PROXY` env to servers started by hotel

## 0.5.9

* UI bug fix

## 0.5.8

* Add `favicon`
* Fix Safari and IE bug

## 0.5.6

* Fix Safari bug

## 0.5.5

* Add `X-Forwarded-Proto` header for ssl proxy
* Support an array of environment variables for the CLI option `--env`
* UI enhancements

## 0.5.4

* Fix Node 0.12 issue

## 0.5.3

* UI tweaks

## 0.5.2

* Fix option alias issue [#109](https://github.com/typicode/hotel/issues/109)

## 0.5.1

* Fix conf issue

## 0.5.0

* Various UI improvements
* Add URL mapping support, for example `hotel add http://192.168.1.10 --name remote-server`
* Change `hotel rm` options

## 0.4.22

* UI tweaks

## 0.4.21

* Fix UI issue with IE

## 0.4.20

* Fix UI issue with Safari 9

## 0.4.19

* Support ANSI colors in the browser

## 0.4.18

* Bug fix

## 0.4.17

* Add `proxy` conf, use it if you're behind a corporate proxy.
* Bug fix

## 0.4.16

* 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.

## 0.4.15

* Fix blank page issue in `v0.4.14`.

## 0.4.14

* Fix UI issues.

## 0.4.13

* Fix issue with Node 0.12.

## 0.4.12

* Add wildcard subdomains `http://*.app.localhost`.

## 0.4.11

* Strip ANSI when viewing logs in the browser.

## 0.4.10

* Fix IE and Safari issue (added fetch polyfill).

## 0.4.9

* Add server logs in the browser.
* Bundle icons to make them available without network access.
* Bug fixes.

## 0.4.8

* Bug fix

## 0.4.7

* Bundle front-end dependencies to make homepage work without network access.
* Support subdomains `http://sub.app.localhost`.
* Support `https` and `wss`.

## 0.4.6

* Bug fixes (0.4.3 to 0.4.5 deprecated).
* Added `~/.hotel/daemon.pid` file.

## 0.4.3

* UI update.
* Added top-level domain configuration option `tld`.
* Added IE support.

## 0.4.2

* Removed `socket.io` dependency.

## 0.4.1

* Added WebSocket support for projects being accessed using local `.localhost` domain.

## 0.4.0

* Added Local `.localhost` domain support for HTTP requests.


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015-present 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: README.md
================================================
# hotel [![](https://badge.fury.io/js/hotel.svg)](https://www.npmjs.com/package/hotel)

> Start apps from your browser and use local domains/https automatically

![](https://i.imgur.com/eDLgWMj.png)

_Tip: if you don't enable local domains, hotel can still be used as a **catalog of local servers**._

Hotel works great on any OS (macOS, Linux, Windows) and with __all servers :heart:__
* Node (Express, Webpack)
* PHP (Laravel, Symfony)
* Ruby (Rails, Sinatra, Jekyll)
* Python (Django)
* Docker
* Go
* Apache, Nginx
* ...

_To all the amazing people who have answered the Hotel survey, thanks so much <3 !_

## v0.8.0 upgrade

`.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.

If you're upgrading, please be sure to:
1. Remove `"tld": "dev"` from your `~/.hotel/conf.json` file
2. Run `hotel stop && hotel start`
3. Refresh your network settings

## Support

If you are benefiting from hotel, you can support its development on [Patreon](https://patreon.com/typicode).

You can view the list of Supporters here https://thanks.typicode.com.

## Video

* [Starting apps with Hotel - Spacedojo Code Kata by Josh Owens](https://www.youtube.com/watch?v=BHW4tzctQ0k)

## Features

* __Local domains__ - `http://project.localhost`
* __HTTPS via local self-signed SSL certificate__ - `https://project.localhost`
* __Wildcard subdomains__ - `http://*.project.localhost`
* __Works everywhere__ - macOS, Linux and Windows
* __Works with any server__ - Node, Ruby, PHP, ...
* __Proxy__ - Map local domains to remote servers
* __System-friendly__ - No messing with `port 80`, `/etc/hosts`, `sudo` or additional software
* Fallback URL - `http://localhost:2000/project`
* Servers are only started when you access them
* Plays nice with other servers (Apache, Nginx, ...)
* Random or fixed ports

## Install

```sh
npm install -g hotel && hotel start
```

Hotel requires Node to be installed, if you don't have it, you can simply install it using one of the following method:

* https://github.com/creationix/nvm `nvm install stable`
* https://brew.sh `brew install node`

You can also visit https://nodejs.org.

## Quick start

### Local domains (optional)

To 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

[__See instructions here__](https://github.com/typicode/hotel/blob/master/docs/README.md).

### Add your servers

```sh
# Add your server to hotel
~/projects/one$ hotel add 'npm start'
# Or start your server in the terminal as usual and get a temporary local domain
~/projects/two$ hotel run 'npm start' 
```

Visit [localhost:2000](http://localhost:2000) or [http(s)://hotel.localhost](http://hotel.localhost).

Alternatively you can directly go to

```
http://localhost:2000/one
http://localhost:2000/two
```

```
http(s)://one.localhost
http(s)://two.localhost 
```

#### Popular servers examples

Using other servers? Here are some examples to get you started :)

```sh
hotel add 'ember server'                               # Ember
hotel add 'jekyll serve --port $PORT'                  # Jekyll
hotel add 'rails server -p $PORT -b 127.0.0.1'         # Rails
hotel add 'python -m SimpleHTTPServer $PORT'           # static file server (Python)
hotel add 'php -S 127.0.0.1:$PORT'                     # PHP
hotel add 'docker-compose up'                          # docker-compose
hotel add 'python manage.py runserver 127.0.0.1:$PORT' # Django
# ...
```

On __Windows__ use `"%PORT%"` instead of `'$PORT'`

[__See a Docker example here.__](https://github.com/typicode/hotel/blob/master/docs/Docker.md).

### Proxy requests to remote servers

Add your remote servers

```sh
~$ hotel add http://192.168.1.12:1337 --name aliased-address
~$ hotel add http://google.com --name aliased-domain 
```

You can now access them using

```sh
http://aliased-address.localhost # will proxy requests to http://192.168.1.12:1337
http://aliased-domain.localhost # will proxy requests to http://google.com
```

## CLI usage and options

```sh
hotel add <cmd|url> [opts]
hotel run <cmd> [opts]

# Examples

hotel add 'nodemon app.js' --out dev.log  # Set output file (default: none)
hotel add 'nodemon app.js' --name name    # Set custom name (default: current dir name)
hotel add 'nodemon app.js' --port 3000    # Set a fixed port (default: random port)
hotel add 'nodemon app.js' --env PATH     # Store PATH environment variable in server config
hotel add http://192.168.1.10 --name app  # map local domain to URL

hotel run 'nodemon app.js'                # Run server and get a temporary local domain

# Other commands

hotel ls     # List servers
hotel rm     # Remove server
hotel start  # Start hotel daemon
hotel stop   # Stop hotel daemon
```

To get help

```sh
hotel --help
hotel --help <cmd>
```

## Port

For `hotel` to work, your servers need to listen on the PORT environment variable.
Here are some examples showing how you can do it from your code or the command-line:

```js
var port = process.env.PORT || 3000
server.listen(port)
```

```sh
hotel add 'cmd -p $PORT'  # OS X, Linux
hotel add "cmd -p %PORT%" # Windows
```

## Fallback URL

If 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).

## Configurations, logs and self-signed SSL certificate

You can find hotel related files in `~/.hotel` :

```sh
~/.hotel/conf.json
~/.hotel/daemon.log
~/.hotel/daemon.pid
~/.hotel/key.pem
~/.hotel/cert.pem
~/.hotel/servers/<app-name>.json
```

By default, `hotel` uses the following configuration values:

```js
{
  "port": 2000,
  "host": '127.0.0.1',
  
  // Timeout when proxying requests to local domains
  "timeout": 5000,
  
  // Change this if you want to use another tld than .localhost
  "tld": 'localhost', 
  
  // If you're behind a corporate proxy, replace this with your network proxy IP (example: "1.2.3.4:5000")
  "proxy": false
}
```

To override a value, simply add it to `~/.hotel/conf.json` and run `hotel stop && hotel start`

## Third-party tools

* [Hotelier](https://github.com/macav/hotelier) Hotelier (Mac & Windows Tray App)
* [Hotel Clerk](https://github.com/therealklanni/hotel-clerk) OS X menubar
* [HotelX](https://github.com/djyde/HotelX) Another OS X menubar (only 1.6MB)
* [alfred-hotel](https://github.com/exah/alfred-hotel) Alfred 3 workflow
* [Hotel Manager](https://github.com/hardpixel/hotel-manager) Gnome Shell extension

## FAQ

#### Setting a fixed port

```sh
hotel add --port 3000 'server-cmd $PORT' 
```

#### Adding `X-Forwarded-*` headers to requests

```sh
hotel add --xfwd 'server-cmd'
```

#### Setting `HTTP_PROXY` env

Use `--http-proxy-env` flag when adding your server or edit your server configuration in `~/.hotel/servers`

```sh
hotel add --http-proxy-env 'server-cmd'
```

#### Proxying requests to a remote `https` server

```sh
hotel add --change-origin 'https://jsonplaceholder.typicode.com'
```

_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._

#### `ENOSPC` and `EACCES` errors

If 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`.

```sh
# to fix permissions
sudo chown -R $USER: $HOME/.hotel
```

See also, https://docs.npmjs.com/getting-started/fixing-npm-permissions

#### Configuring a network proxy IP

If you're behind a corporate proxy, replace `"proxy"` with your network proxy IP in `~/.hotel/conf.json`. For example:

```json
{
  "proxy": "1.2.3.4:5000"
}
```

## License

MIT

[Patreon](https://www.patreon.com/typicode) - [Supporters](https://thanks.typicode.com) ✨


================================================
FILE: appveyor.yml
================================================
environment:
  nodejs_version: '6'

install:
  - ps: Install-Product node $env:nodejs_version
  - npm install --ignore-scripts

test_script:
  - node --version
  - npm --version
  - npm test

build_script: npm run build


================================================
FILE: bin/uninstall.js
================================================
require('../lib/scripts/uninstall')()


================================================
FILE: docs/Docker.md
================================================
# Docker

[Docker](https://www.docker.com/) is a software container platform that integrates easily into Hotel.

### Dockerfile

A [Dockerfile](https://docs.docker.com/engine/reference/builder/) is used to create a single container. Here is an example:

```
FROM httpd:2.4
COPY ./public-html/ /usr/local/apache2/htdocs/
```

To build this image, run the following in the application directory:

```
docker build -t my-apache2 .
```

To use Hotel for this example, run the following in the application directory:

```
hotel add 'docker run -dit --name my-running-app my-apache2'
```

### Docker Compose

[Compose](https://docs.docker.com/compose/) is a tool for defining and running multi-container Docker applications. Here is a simple example:

```
version: '3'
services:
  web:
    build: .
    ports:
     - "5000:5000"
```

This binds the internal port 5000 on the container to port 5000 on the host machine. To build this image, run the following:

`docker-compose build`

To use Hotel for this, run the following in the application directory:

```
hotel add 'docker-compose up' -p 5000
```


================================================
FILE: docs/README.md
================================================
# Configuring local .localhost domains

_This step is totally optional and you can use hotel without it._

To 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)].

__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.

## Configuring another .tld

You can edit `~/.hotel/conf.json` to use another Top-level Domain than `.localhost`.

```json
{
  "tld": "test"
}
```

__Important__ Don't forget to restart hotel and reload network or browser configuration.

## System configuration (recommended)

##### macOS

`Network Preferences > Advanced > Proxies > Automatic Proxy Configuration`

##### Windows

`Settings > Network and Internet > Proxy > Use setup script`

##### Linux

On Ubuntu

`System Settings > Network > Network Proxy > Automatic`

For other distributions, check your network manager and look for proxy configuration. Use browser configuration as an alternative.

## Browser configuration

Browsers can be configured to use a specific proxy. Use this method as an alternative to system-wide configuration.

##### Chrome

Exit Chrome and start it using the following option:

```sh
# Linux
$ google-chrome --proxy-pac-url=http://localhost:2000/proxy.pac

# macOS
$ open -a "Google Chrome" --args --proxy-pac-url=http://localhost:2000/proxy.pac
```

##### Firefox

`Preferences > Advanced > Network > Connection > Settings > Automatic proxy URL configuration`

##### Internet Explorer

Uses system network configuration.


================================================
FILE: nodemon.json
================================================
{
  "exec": "babel-node",
  "ignore": [
    "src/app/**/*",
    "src/daemon/public/**/*"
  ]
}


================================================
FILE: package.json
================================================
{
  "name": "hotel",
  "version": "1.0.0",
  "description": "Local domains for everyone and more! ",
  "main": "lib",
  "bin": "lib/cli/bin.js",
  "engines": {
    "node": ">=6"
  },
  "scripts": {
    "test": "ava && npm run lint",
    "lint": "eslint . --ignore-path .gitignore && stylelint './src/app/**/*.css'",
    "fix": "eslint . --ignore-path .gitignore --fix && stylelint './src/app/**/*.css' --fix",
    "start": "run-p start:*",
    "start:webpack": "webpack-dev-server --config webpack.dev.js --open",
    "start:nodemon": "nodemon -- src/daemon",
    "prepublishOnly": "npm run build && pkg-ok",
    "uninstall": "node bin/uninstall.js",
    "build": "run-s build:*",
    "build:webpack": "rimraf dist && webpack --config webpack.prod.js",
    "build:babel": "rimraf lib && babel src -d lib --copy-files --ignore src/app",
    "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 "
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/typicode/hotel.git"
  },
  "keywords": [
    "dev",
    "devtool",
    "domain",
    "host",
    "https",
    "local",
    "localhost",
    "manager",
    "process",
    "proxy",
    "server"
  ],
  "author": "Typicode <typicode@gmail.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/typicode/hotel/issues"
  },
  "homepage": "https://github.com/typicode/hotel",
  "dependencies": {
    "after-all": "^2.0.2",
    "ansi2html": "0.0.1",
    "chalk": "^2.3.1",
    "chokidar": "^2.0.2",
    "connect-sse": "^1.2.0",
    "exit-hook": "^1.1.1",
    "express": "^4.16.2",
    "get-port": "^3.2.0",
    "http-proxy": "^1.17.0",
    "matcher": "^1.1.0",
    "mkdirp": "^0.5.1",
    "once": "^1.3.2",
    "please-upgrade-node": "^3.0.2",
    "pug": "^2.0.0-rc.4",
    "respawn": "^2.4.1",
    "selfsigned": "^1.10.2",
    "server-ready": "^0.3.1",
    "sudo-block": "^2.0.0",
    "tildify": "^1.1.2",
    "tinydate": "^1.0.0",
    "unquote": "^1.1.1",
    "untildify": "^3.0.2",
    "update-notifier": "^2.3.0",
    "user-startup": "^0.2.1",
    "vhost": "^3.0.2",
    "yargs": "^10.1.2"
  },
  "devDependencies": {
    "@types/classnames": "^2.2.3",
    "@types/escape-html": "0.0.20",
    "@types/react": "^16.0.38",
    "@types/react-dom": "^16.0.4",
    "@types/react-icons": "^2.2.5",
    "ava": "^0.25.0",
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "classnames": "^2.2.5",
    "css-loader": "^0.28.10",
    "escape-html": "^1.0.3",
    "eslint": "^4.18.1",
    "eslint-config-prettier": "^2.9.0",
    "eslint-config-standard": "^11.0.0",
    "eslint-plugin-import": "^2.9.0",
    "eslint-plugin-node": "^5.2.1",
    "eslint-plugin-prettier": "^2.6.0",
    "eslint-plugin-promise": "^3.6.0",
    "eslint-plugin-standard": "^3.0.1",
    "html-webpack-plugin": "^2.30.1",
    "husky": "^0.15.0-rc.8",
    "lodash.uniqueid": "^4.0.1",
    "mobx": "^3.5.1",
    "mobx-react": "^4.4.2",
    "nodemon": "^1.15.1",
    "npm-run-all": "^4.1.2",
    "pkg-ok": "^2.1.0",
    "prettier": "^1.10.2",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-icons": "^2.2.7",
    "rimraf": "^2.6.2",
    "sinon": "^4.4.2",
    "style-loader": "^0.19.1",
    "stylelint": "^8.4.0",
    "stylelint-config-recess-order": "^1.2.3",
    "stylelint-config-standard": "^18.1.0",
    "supertest": "^3.0.0",
    "tempy": "^0.2.0",
    "ts-loader": "^3.5.0",
    "tslint": "^5.9.1",
    "tslint-config-prettier": "^1.9.0",
    "tslint-plugin-prettier": "^1.3.0",
    "typescript": "^2.7.2",
    "uglifyjs-webpack-plugin": "^1.2.2",
    "webpack": "^3.11.0",
    "webpack-dev-server": "^2.11.1",
    "webpack-merge": "^4.1.2"
  },
  "ava": {
    "serial": true,
    "verbose": true,
    "require": [
      "babel-register",
      "./test/_setup"
    ],
    "babel": "inherit"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm test"
    }
  }
}


================================================
FILE: src/app/Store.ts
================================================
import * as uniqueId from 'lodash.uniqueid'
import { action, computed, observable } from 'mobx'
import * as api from './api'
import { formatLines } from './formatter'

export interface IProxy {
  target: string
}

export interface ILine {
  id: string
  html: string
}

export interface IMonitor {
  cwd: string
  command: string[]
  status: string
  output: ILine[]
  started: Date
  pid: number
}

export const RUNNING = 'running'
export const STOPPED = 'stopped'
const MAX_OUTPUT_LENGTH = 1000

function clear(servers: Map<string, IMonitor | IProxy>, data: any) {
  servers.forEach((server, id) => {
    if (!data.hasOwnProperty(id)) {
      servers.delete(id)
    }
  })
}

export default class Store {
  @observable public isLoading: boolean = true
  @observable public selectedMonitorId: string = ''
  @observable public monitors: Map<string, IMonitor> = new Map()
  @observable public proxies: Map<string, IProxy> = new Map()

  constructor() {
    this.watchServers()
    this.watchOutput()
  }

  @action
  public watchServers() {
    api.watchServers(data => {
      // Delete servers that do not exist anymore in Hotel
      clear(this.monitors, data)
      clear(this.proxies, data)

      // Create or update servers
      Object.keys(data).forEach(id => {
        const server = data[id]
        if (this.monitors.has(id) || this.proxies.has(id)) {
          // Update server state
          if (server.hasOwnProperty('status')) {
            Object.assign(this.monitors.get(id), server)
          } else {
            Object.assign(this.proxies.get(id), server)
          }
        } else {
          // Create new server
          if (server.hasOwnProperty('status')) {
            server.output = []
            this.monitors.set(id, server)
          } else {
            this.proxies.set(id, server)
          }
        }
      })

      // Initial data has been loaded
      this.isLoading = false
    })
  }

  @action
  public watchOutput() {
    api.watchOutput(data => {
      const { id, output } = data
      const lines = formatLines(output).map(html => ({
        html,
        id: uniqueId()
      }))

      lines.forEach(line => {
        const monitor = this.monitors.get(id)
        if (monitor) {
          monitor.output.push(line)

          if (monitor.output.length > MAX_OUTPUT_LENGTH) {
            monitor.output.shift()
          }
        }
      })
    })
  }

  @action
  public selectMonitor(monitorId: string) {
    this.selectedMonitorId =
      this.selectedMonitorId === monitorId ? '' : monitorId
  }

  @action
  public toggleMonitor(monitorId: string) {
    const monitor = this.monitors.get(monitorId)

    if (monitor) {
      if (monitor.status === RUNNING) {
        api.stopMonitor(monitorId)
        monitor.status = STOPPED // optimistic update
      } else {
        api.startMonitor(monitorId)
        monitor.status = RUNNING
      }
    }
  }

  @action
  public clearOutput(monitorId: string) {
    const monitor = this.monitors.get(monitorId)
    if (monitor) {
      monitor.output = []
    }
  }
}


================================================
FILE: src/app/api.ts
================================================
interface IEvent {
  data: string
}

export function fetchServers() {
  return window.fetch('/_/servers').then(response => response.json())
}

export function watchServers(cb: (data: any) => void) {
  if (window.EventSource) {
    new window.EventSource('/_/events').onmessage = (event: IEvent) => {
      const data = JSON.parse(event.data)
      cb(data)
    }
  } else {
    setInterval(() => {
      window
        .fetch('/_/servers')
        .then(response => response.json())
        .then(data => cb(data))
    }, 1000)
  }
}

export function watchOutput(cb: (data: any) => void) {
  if (window.EventSource) {
    new window.EventSource('/_/events/output').onmessage = (event: IEvent) => {
      const data = JSON.parse(event.data)
      cb(data)
    }
  } else {
    window.alert("Sorry, server logs aren't supported on this browser :(")
  }
}

export function startMonitor(id: string) {
  return window.fetch(`/_/servers/${id}/start`, { method: 'POST' })
}

export function stopMonitor(id: string) {
  return window.fetch(`/_/servers/${id}/stop`, { method: 'POST' })
}


================================================
FILE: src/app/components/App/index.css
================================================
* {
  box-sizing: border-box;
}

:root {
  --primary-light: #484848;
  --primary: #212121;
  --primary: #282c2f;
  --primary-dark: #000;
  --accent: #4c9e97;
}

body {
  padding: 0;
  margin: 0;
  font-family: 'Roboto', sans-serif;
  color: white;
  background-color: var(--primary-dark);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  color: #6c6c6f;
  text-decoration: none;
}

a:hover {
  color: white;
  text-decoration: underline;
}

.nav {
  height: 100vh;
}

/* 640px */
@media (min-width: 40rem) {
  .container {
    display: grid;
    grid-template-columns: 1fr 3fr;
  }
}


================================================
FILE: src/app/components/App/index.tsx
================================================
import { observer } from 'mobx-react'
import * as React from 'react'
import Store from '../../Store'
import Content from '../Content'
import Nav from '../Nav'
import Splash from '../Splash'
import './index.css'

export interface IProps {
  store: Store
}

function App({ store }: IProps) {
  return (
    <div className="container">
      <Nav store={store} />
      {store.selectedMonitorId ? <Content store={store} /> : <Splash />}
    </div>
  )
}

export default observer(App)


================================================
FILE: src/app/components/Content/index.css
================================================
.content {
  height: 100vh;
  overflow-y: scroll;
  background-color: var(--primary);
}

.content a {
  color: white;
}

.content-bar {
  position: sticky;
  top: 0;
  left: 0;
  display: flex;
  justify-content: space-between;
  padding: 0 0 0 1rem;
  background-color: var(--primary-dark);
}

.content-bar > span {
  align-self: center;
}

pre {
  padding: 1rem;
  margin: 0;
  word-break: break-all;
}

button {
  padding: 1rem;
  color: white;
  cursor: pointer;
  background: none;
  border: none;
  outline: none;
}

button:hover {
  background: var(--primary);
}


================================================
FILE: src/app/components/Content/index.tsx
================================================
import { observer } from 'mobx-react'
import * as React from 'react'
import * as MdArrowDownward from 'react-icons/lib/md/arrow-downward'
import * as MdClearAll from 'react-icons/lib/md/clear-all'
import Link from '../Link'

import Store from '../../Store'
import './index.css'

export interface IProps {
  store: Store
}

@observer
class Content extends React.Component<IProps, {}> {
  private el: HTMLDivElement | null = null
  private atBottom: boolean = true

  public componentWillUpdate() {
    if (this.el) {
      this.atBottom = this.isAtBottom()
    }
  }

  public componentDidUpdate() {
    if (this.atBottom) {
      this.scrollToBottom()
    }
  }

  public isAtBottom() {
    if (this.el) {
      const { scrollHeight, scrollTop, clientHeight } = this.el
      return scrollHeight - scrollTop === clientHeight
    } else {
      return true
    }
  }

  public scrollToBottom() {
    if (this.el) {
      this.el.scrollTop = this.el.scrollHeight
    }
  }

  public onScroll() {
    this.atBottom = this.isAtBottom()
  }

  public render() {
    const { store } = this.props
    const monitor = store.monitors.get(store.selectedMonitorId)
    return (
      <div
        className="content"
        onScroll={() => this.onScroll()}
        ref={el => {
          this.el = el
        }}
      >
        <div className="content-bar">
          <span>
            <Link id={store.selectedMonitorId} />
          </span>
          <span>
            <button
              title="Clear output"
              onClick={() => store.clearOutput(store.selectedMonitorId)}
            >
              <MdClearAll />
            </button>
            <button
              title="Scroll to bottom"
              onClick={() => this.scrollToBottom()}
            >
              <MdArrowDownward />
            </button>
          </span>
        </div>
        <pre>
          {monitor &&
            monitor.output.map(line => (
              <div
                key={line.id}
                dangerouslySetInnerHTML={{ __html: line.html }}
              />
            ))}
        </pre>
      </div>
    )
  }
}

export default Content


================================================
FILE: src/app/components/Link/index.tsx
================================================
import * as React from 'react'
import { IMonitor, IProxy } from '../../Store'

function href(id: string) {
  const { protocol, hostname } = window.location
  if (/hotel\./.test(hostname)) {
    // Accessed using hotel.tld
    const tld = hostname.split('.').slice(-1)[0]
    return `${protocol}//${id}.${tld}`
  } else {
    // Accessed using localhost
    return `/${id}`
  }
}

interface IProps {
  id: string
}

function Link({ id }: IProps) {
  return (
    <a href={href(id)} target="_blank" onClick={e => e.stopPropagation()}>
      {id}
    </a>
  )
}

export default Link


================================================
FILE: src/app/components/Nav/index.css
================================================
.nav {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  border-right: 1px solid var(--primary);
}

header {
  padding: 1rem;
  font-size: 1rem;
  line-height: 1rem;
  text-transform: capitalize;
  border-bottom: 1px solid var(--primary);
}

.menu {
  flex: 1;
  overflow-y: scroll;
}

.menu.hidden {
  visibility: hidden;
}

footer {
  padding: 1rem;
}

h2 {
  padding: 1rem;
  margin: 0;
  font-size: 1rem;
  font-weight: normal;
  text-transform: uppercase;
}

ul {
  padding: 0;
  margin: 0 0 2rem 0;
  list-style: none;
}

li {
  display: flex;
  justify-content: space-between;
  height: 3rem;
  color: var(--primary-light);
  transition: border-color 0.2s;
}

li:focus {
  outline: none;
}

li.running * {
  color: white;
}

li.monitor:hover {
  cursor: pointer;
  background: var(--primary);
}

li.selected {
  background: var(--primary);
}

li > span {
  align-self: center;
  padding: 0 1rem;
}

/* Align Switch vertically */
li > span:nth-last-child() {
  line-height: 0;
}

p {
  padding: 1rem;
}


================================================
FILE: src/app/components/Nav/index.tsx
================================================
import * as classNames from 'classnames'
import { observer } from 'mobx-react'
import * as React from 'react'
import Store, { RUNNING } from '../../Store'
import Link from '../Link'
import Switch from '../Switch'
import './index.css'

const examples = `~/app$ hotel add 'cmd'
~/app$ hotel add 'cmd -p $PORT'
~/app$ hotel add http://192.16.1.2:3000`

export interface IProps {
  store: Store
}

function Nav({ store }: IProps) {
  const { isLoading, selectedMonitorId, monitors, proxies } = store
  return (
    <div className="nav">
      <header>hotel</header>
      <div className={classNames('menu', { hidden: isLoading })}>
        {monitors.size === 0 &&
          proxies.size === 0 && (
            <div>
              <p>To add a server, use hotel add</p>
              <pre>
                <code>{examples}</code>
              </pre>
            </div>
          )}

        {monitors.size > 0 && (
          <div>
            <h2>monitors</h2>
            <ul>
              {Array.from(monitors).map(([id, monitor]) => {
                return (
                  <li
                    key={id}
                    className={classNames('monitor', {
                      running: monitor.status === RUNNING,
                      selected: id === selectedMonitorId
                    })}
                    onClick={() => store.selectMonitor(id)}
                  >
                    <span>
                      <Link id={id} />
                    </span>
                    <span>
                      <Switch
                        onClick={() => store.toggleMonitor(id)}
                        checked={monitor.status === RUNNING}
                      />
                    </span>
                  </li>
                )
              })}
            </ul>
          </div>
        )}

        {proxies.size > 0 && (
          <div>
            <h2>proxies</h2>
            <ul>
              {Array.from(proxies).map(([id, proxy]) => {
                return (
                  <li key={id}>
                    <span>
                      <Link id={id} />
                    </span>
                  </li>
                )
              })}
            </ul>
          </div>
        )}
      </div>
      <footer>
        <a href="https://github.com/typicode/hotel" target="_blank">
          README
        </a>
      </footer>
    </div>
  )
}

export default observer(Nav)


================================================
FILE: src/app/components/Splash/index.css
================================================
.splash {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}


================================================
FILE: src/app/components/Splash/index.tsx
================================================
import * as React from 'react'
import './index.css'

function Splash() {
  return <div className="splash">{/* Hotel ホテル */}</div>
}

export default Splash


================================================
FILE: src/app/components/Switch/index.css
================================================
.switch {
  position: relative;
  display: inline-block;
  width: 30px;
  height: 17px;
}

.switch input {
  display: none;
}

.slider {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  cursor: pointer;
  background-color: #ccc;
  transition: 0.4s;
}

.slider::before {
  position: absolute;
  bottom: 2px;
  left: 2px;
  width: 13px;
  height: 13px;
  content: "";
  background-color: white;
  transition: 0.4s;
}

input:checked + .slider {
  background-color: var(--accent);
}

input:focus + .slider {
  box-shadow: 0 0 1px var(--primary-dark); /* works ? */
}

input:checked + .slider::before {
  transform: translateX(13px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 17px;
}

.slider.round::before {
  border-radius: 50%;
}


================================================
FILE: src/app/components/Switch/index.tsx
================================================
import * as React from 'react'
import './index.css'

export interface IProps {
  onClick?: () => void
  checked?: boolean
}

function Switch({ onClick = () => null, checked }: IProps) {
  return (
    <label
      className="switch"
      onClick={e => {
        e.stopPropagation()
        e.preventDefault()
        onClick()
      }}
    >
      <input type="checkbox" checked={checked} />
      <span className="slider" />
    </label>
  )
}

export default Switch


================================================
FILE: src/app/formatter.ts
================================================
import * as ansi2HTML from 'ansi2html'
import * as escapeHTML from 'escape-html'
import { IMonitor } from './Store'

function blankLine(val: string) {
  return val.trim() === '' ? '&nbsp;' : val
}

export function formatLines(str: string): string[] {
  return str
    .replace(/\n$/, '')
    .split('\n')
    .map(escapeHTML)
    .map(blankLine)
    .map(ansi2HTML)
}

export function statusTitle(monitor: IMonitor) {
  return monitor.pid
    ? `PID ${monitor.pid}\nStarted since ${new Date(
        monitor.started
      ).toLocaleString()}`
    : ''
}


================================================
FILE: src/app/global.d.ts
================================================
declare module 'ansi2html'
declare module 'lodash.uniqueid'
declare module 'react-icons/lib/md/arrow-downward'
declare module 'react-icons/lib/md/clear-all'
interface Window { EventSource: any }


================================================
FILE: src/app/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <title>Hotel</title>
    <link rel="icon" type="image/png" href="favicon.png">
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
    <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

================================================
FILE: src/app/index.tsx
================================================
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './components/App'
import Store from './Store'

const store = new Store()
ReactDOM.render(<App store={store} />, document.getElementById('root'))


================================================
FILE: src/cli/bin.js
================================================
#!/usr/bin/env node
const pkg = require('../../package.json')
require('please-upgrade-node')(pkg)

const updateNotifier = require('update-notifier')
const sudoBlock = require('sudo-block')

sudoBlock('\nShould not be run as root, please retry without sudo.\n')
updateNotifier({ pkg }).notify()
require('./')(process.argv)


================================================
FILE: src/cli/daemon.js
================================================
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
const startup = require('user-startup')
const common = require('../common')
const conf = require('../conf')
const uninstall = require('../scripts/uninstall')

module.exports = {
  start,
  stop
}

// Start daemon in background
function start() {
  const node = process.execPath
  const daemonFile = path.join(__dirname, '../daemon')
  const startupFile = startup.getFile('hotel')

  startup.create('hotel', node, [daemonFile], common.logFile)

  // Save startup file path in ~/.hotel
  // Will be used later by uninstall script
  mkdirp.sync(common.hotelDir)
  fs.writeFileSync(common.startupFile, startupFile)

  console.log(`Started http://localhost:${conf.port}`)
}

// Stop daemon
function stop() {
  startup.remove('hotel')
  // kills process and clean stuff in ~/.hotel
  uninstall()
  console.log('Stopped')
}


================================================
FILE: src/cli/index.js
================================================
const yargs = require('yargs')
const servers = require('./servers')
const run = require('./run')
const daemon = require('./daemon')
const pkg = require('../../package.json')

const addOptions = {
  name: {
    alias: 'n',
    describe: 'Server name'
  },
  port: {
    alias: 'p',
    describe: 'Set PORT environment variable',
    number: true
  },
  out: {
    alias: 'o',
    describe: 'Output file'
  },
  env: {
    alias: 'e',
    describe: 'Additional environment variables',
    array: true
  },
  xfwd: {
    alias: 'x',
    describe: 'Adds x-forward headers',
    default: false,
    boolean: true
  },
  'change-origin': {
    alias: 'co',
    describe: 'Changes the origin of the host header to the target URL',
    default: false,
    boolean: true
  },
  'http-proxy-env': {
    describe: 'Adds HTTP_PROXY environment variable',
    default: false,
    boolean: true
  },
  dir: {
    describe: 'Server directory',
    string: true
  }
}

module.exports = processArgv =>
  yargs(processArgv.slice(2))
    .version(pkg.version)
    .alias('v', 'version')
    .help('h')
    .alias('h', 'help')
    .command(
      'add <cmd_or_url> [options]',
      'Add server or proxy',
      yargs => yargs.options(addOptions),
      // .demand(1),
      argv => servers.add(argv['cmd_or_url'], argv)
    )
    .command(
      'run <cmd> [options]',
      'Run server and get a temporary local domain',
      yargs => {
        const runOptions = { ...addOptions }
        delete runOptions['out']
        return yargs.options(runOptions)
        // TODO demand(1) ?
      },
      argv => run.spawn(argv['cmd'], argv)
    )
    .command(
      'rm [options]',
      'Remove server or proxy',
      yargs => {
        yargs.option('name', {
          alias: 'n',
          describe: 'Name'
        })
      },
      argv => servers.rm(argv)
    )
    .command('ls', 'List servers', {}, argv => servers.ls(argv))
    .command('start', 'Start daemon', {}, () => daemon.start())
    .command('stop', 'Stop daemon', {}, () => daemon.stop())
    .example('$0 add --help')
    .example('$0 add nodemon')
    .example('$0 add npm start')
    .example("$0 add 'cmd -p $PORT'")
    .example("$0 add 'cmd -p $PORT' --port 4000")
    .example("$0 add 'cmd -p $PORT' --out app.log")
    .example("$0 add 'cmd -p $PORT' --name app")
    .example("$0 add 'cmd -p $PORT' --env PATH")
    .example('$0 add http://192.168.1.10 -n app ')
    .example('$0 rm')
    .example('$0 rm -n app')
    .epilog('https://github.com/typicode/hotel')
    .demand(1)
    .strict()
    .help().argv


================================================
FILE: src/cli/run.js
================================================
const cp = require('child_process')
const getPort = require('get-port')
const servers = require('./servers')
const getCmd = require('../get-cmd')

const signals = ['SIGINT', 'SIGTERM', 'SIGHUP']

module.exports = {
  // For testing purpose, allows stubbing cp.spawnSync
  _spawnSync(...args) {
    cp.spawnSync(...args)
  },

  // For testing purpose, allows stubbing process.exit
  _exit(...args) {
    process.exit(...args)
  },

  spawn(cmd, opts = {}) {
    const cleanAndExit = (code = 0) => {
      servers.rm(opts)
      this._exit(code)
    }

    const startServer = port => {
      const serverAddress = `http://localhost:${port}`

      process.env.PORT = port
      servers.add(serverAddress, opts)

      signals.forEach(signal => process.on(signal, cleanAndExit))

      const [command, ...args] = getCmd(cmd)
      const { status, error } = this._spawnSync(command, args, {
        stdio: 'inherit',
        cwd: process.cwd()
      })

      if (error) throw error
      cleanAndExit(status)
    }

    if (opts.port) {
      startServer(opts.port)
    } else {
      getPort()
        .then(startServer)
        .catch(err => {
          throw err
        })
    }
  }
}


================================================
FILE: src/cli/servers.js
================================================
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const tildify = require('tildify')
const mkdirp = require('mkdirp')
const common = require('../common')

const serversDir = common.serversDir

module.exports = {
  add,
  rm,
  ls
}

function isUrl(str) {
  return /^(http|https):/.test(str)
}

// Converts '_-Some Project_Name--' to 'some-project-name'
function domainify(str) {
  return (
    str
      .toLowerCase()
      // Replace all _ and spaces with -
      .replace(/(_| )/g, '-')
      // Trim - characters
      .replace(/(^-*|-*$)/g, '')
  )
}

function getId(cwd) {
  return domainify(path.basename(cwd))
}

function getServerFile(id) {
  return `${serversDir}/${id}.json`
}

function add(param, opts = {}) {
  mkdirp.sync(serversDir)

  const cwd = opts.dir || process.cwd()
  const id = opts.name ? domainify(opts.name) : getId(cwd)

  const file = getServerFile(id)

  let conf = {}

  if (opts.xfwd) {
    conf.xfwd = opts.xfwd
  }

  if (opts.changeOrigin) {
    conf.changeOrigin = opts.changeOrigin
  }

  if (opts.httpProxyEnv) {
    conf.httpProxyEnv = opts.httpProxyEnv
  }

  if (isUrl(param)) {
    conf = {
      target: param,
      ...conf
    }
  } else {
    conf = {
      cwd,
      cmd: param,
      ...conf
    }

    if (opts.o) conf.out = opts.o

    conf.env = {}

    // By default, save PATH env for version managers users
    conf.env.PATH = process.env.PATH

    // Copy other env option
    if (opts.env) {
      opts.env.forEach(key => {
        const value = process.env[key]
        if (value) {
          conf.env[key] = value
        }
      })
    }

    // Copy port option
    if (opts.port) {
      conf.env.PORT = opts.port
    }
  }

  const data = JSON.stringify(conf, null, 2)

  console.log(`Create ${tildify(file)}`)
  fs.writeFileSync(file, data)

  // if we're mapping a domain to a URL there's no additional info to output
  if (conf.target) return

  // if we're mapping a domain to a local server add some info
  if (conf.out) {
    const logFile = tildify(path.resolve(conf.out))
    console.log(`Output ${logFile}`)
  } else {
    console.log("Output No log file specified (use '-o app.log')")
  }

  if (!opts.p) {
    console.log("Port Random port (use '-p 1337' to set a fixed port)")
  }
}

function rm(opts = {}) {
  const cwd = process.cwd()
  const id = opts.n || getId(cwd)
  const file = getServerFile(id)

  console.log(`Remove  ${tildify(file)}`)
  if (fs.existsSync(file)) {
    fs.unlinkSync(file)
    console.log('Removed')
  } else {
    console.log('No such file')
  }
}

function ls() {
  mkdirp.sync(serversDir)

  const list = fs
    .readdirSync(serversDir)
    .map(file => {
      const id = path.basename(file, '.json')
      const serverFile = getServerFile(id)
      let server

      try {
        server = JSON.parse(fs.readFileSync(serverFile))
      } catch (error) {
        // Ignore mis-named or malformed files
        return
      }

      if (server.cmd) {
        return `${id}\n${chalk.gray(tildify(server.cwd))}\n${chalk.gray(
          server.cmd
        )}`
      } else {
        return `${id}\n${chalk.gray(server.target)}`
      }
    })
    .filter(item => item)
    .join('\n\n')

  console.log(list)
}


================================================
FILE: src/common.js
================================================
const path = require('path')
const homedir = require('os').homedir()

const hotelDir = path.join(homedir, '.hotel')

module.exports = {
  hotelDir,
  confFile: path.join(hotelDir, 'conf.json'),
  serversDir: path.join(hotelDir, 'servers'),
  pidFile: path.join(hotelDir, 'daemon.pid'),
  logFile: path.join(hotelDir, 'daemon.log'),
  startupFile: path.join(hotelDir, 'startup')
}


================================================
FILE: src/conf.js
================================================
const fs = require('fs')
const mkdirp = require('mkdirp')
const { hotelDir, confFile } = require('./common')

// Create dir
mkdirp.sync(hotelDir)

// Defaults
const defaults = {
  port: 2000,
  host: '127.0.0.1',
  timeout: 5000,
  tld: 'localhost',
  // Replace with your network proxy IP (1.2.3.4:5000) if any
  // For example, if you're behind a corporate proxy
  proxy: false
}

// Create empty conf it it doesn't exist
if (!fs.existsSync(confFile)) fs.writeFileSync(confFile, '{}')

// Read file
const conf = JSON.parse(fs.readFileSync(confFile))

// Assign defaults and export
module.exports = { ...defaults, ...conf }


================================================
FILE: src/daemon/app.js
================================================
const path = require('path')
const http = require('http')
const express = require('express')
const vhost = require('vhost')
const serverReady = require('server-ready')
const conf = require('../conf')

// Require routes
const IndexRouter = require('./routers')
const APIRouter = require('./routers/api')
const TLDHost = require('./vhosts/tld')

module.exports = group => {
  const app = express()
  const server = http.createServer(app)

  // Initialize routes
  const indexRouter = IndexRouter(group)
  const api = APIRouter(group)
  const tldHost = TLDHost(group)

  // requests timeout
  serverReady.timeout = conf.timeout

  // Templates
  app.set('views', path.join(__dirname, 'views'))
  app.set('view engine', 'pug')
  app.locals.pretty = true

  // API
  app.use('/_', api)

  // .tld host
  app.use(vhost(new RegExp(`.*.${conf.tld}`), tldHost))

  // app.get('/', (req, res) => res.render('index'))

  // Static files
  // vendors, etc...
  app.use(express.static(path.join(__dirname, 'public')))
  // front files
  app.use(express.static(path.join(__dirname, '../../dist')))

  // localhost router
  app.use(indexRouter)

  // Handle CONNECT, used by WebSockets and https when accessing .localhost domains
  server.on('connect', (req, socket, head) => {
    group.handleConnect(req, socket, head)
  })

  server.on('upgrade', (req, socket, head) => {
    group.handleUpgrade(req, socket, head)
  })

  return server
}


================================================
FILE: src/daemon/group.js
================================================
const fs = require('fs')
const path = require('path')
const EventEmitter = require('events')
const url = require('url')
const once = require('once')
const getPort = require('get-port')
const matcher = require('matcher')
const respawn = require('respawn')
const afterAll = require('after-all')
const httpProxy = require('http-proxy')
const serverReady = require('server-ready')
const log = require('./log')
const tcpProxy = require('./tcp-proxy')
const daemonConf = require('../conf')
const getCmd = require('../get-cmd')

module.exports = () => new Group()

class Group extends EventEmitter {
  constructor() {
    super()

    this._list = {}
    this._proxy = httpProxy.createProxyServer({
      xfwd: true
    })
  }

  _output(id, data) {
    this.emit('output', id, data)
  }

  _log(mon, logFile, data) {
    mon.tail = mon.tail
      .concat(data)
      .split('\n')
      .slice(-100)
      .join('\n')

    if (logFile) {
      fs.appendFile(logFile, data, err => {
        if (err) log(err.message)
      })
    }
  }

  _change() {
    this.emit('change', this._list)
  }

  //
  // Conf
  //

  list() {
    return this._list
  }

  find(id) {
    return this._list[id]
  }

  add(id, conf) {
    if (conf.target) {
      log(`Add target ${id}`)
      this._list[id] = conf
      this._change()
      return
    }

    log(`Add server ${id}`)

    const HTTP_PROXY = `http://127.0.0.1:${daemonConf.port}/proxy.pac`

    conf.env = {
      ...process.env,
      ...conf.env
    }

    if (conf.httpProxyEnv) {
      conf.env = {
        HTTP_PROXY,
        HTTPS_PROXY: HTTP_PROXY,
        http_proxy: HTTP_PROXY,
        https_proxy: HTTP_PROXY,
        ...conf.env
      }
    }

    let logFile
    if (conf.out) {
      logFile = path.resolve(conf.cwd, conf.out)
    }

    const command = getCmd(conf.cmd)

    const mon = respawn(command, {
      ...conf,
      maxRestarts: 0
    })

    this._list[id] = mon

    // Add proxy config
    mon.xfwd = conf.xfwd || false
    mon.changeOrigin = conf.changeOrigin || false

    // Emit output
    mon.on('stdout', data => this._output(id, data))
    mon.on('stderr', data => this._output(id, data))
    mon.on('warn', data => this._output(id, data))

    // Emit change
    mon.on('start', () => this._change())
    mon.on('stop', () => this._change())
    mon.on('crash', () => this._change())
    mon.on('sleep', () => this._change())
    mon.on('exit', () => this._change())

    // Log status
    mon.on('start', () => log(id, 'has started'))
    mon.on('stop', () => log(id, 'has stopped'))
    mon.on('crash', () => log(id, 'has crashed'))
    mon.on('sleep', () => log(id, 'is sleeping'))
    mon.on('exit', () => log(id, 'child process has exited'))

    // Handle logs
    mon.tail = ''

    mon.on('stdout', data => this._log(mon, logFile, data))
    mon.on('stderr', data => this._log(mon, logFile, data))
    mon.on('warn', data => this._log(mon, logFile, data))

    mon.on('start', () => {
      mon.tail = ''

      if (logFile) {
        fs.unlink(logFile, err => {
          if (err) log(err.message)
        })
      }
    })

    this._change()
  }

  remove(id, cb) {
    const item = this.find(id)
    if (item) {
      delete this._list[id]
      this._change()

      if (item.stop) {
        item.stop(cb)
        item.removeAllListeners()
        return
      }
    }

    cb && cb()
  }

  stopAll(cb) {
    const next = afterAll(cb)

    Object.keys(this._list).forEach(key => {
      if (this._list[key].stop) {
        this._list[key].stop(next())
      }
    })
  }

  update(id, conf) {
    this.remove(id, () => this.add(id, conf))
  }

  //
  // Hostname resolver
  //

  resolve(str) {
    log(`Resolve ${str}`)
    const arr = Object.keys(this._list)
      .sort()
      .reverse()
      .map(h => ({
        host: h,
        isStrictMatch: matcher.isMatch(str, h),
        isWildcardMatch: matcher.isMatch(str, `*.${h}`)
      }))

    const strictMatch = arr.find(h => h.isStrictMatch)
    const wildcardMatch = arr.find(h => h.isWildcardMatch)

    if (strictMatch) return strictMatch.host
    if (wildcardMatch) return wildcardMatch.host
  }

  //
  // Middlewares
  //

  exists(req, res, next) {
    // Resolve using either hostname `app.tld`
    // or id param `http://localhost:2000/app`
    const tld = new RegExp(`.${daemonConf.tld}$`)
    const id = req.params.id
      ? this.resolve(req.params.id)
      : this.resolve(req.hostname.replace(tld, ''))

    // Find item
    const item = this.find(id)

    // Not found
    if (!id || !item) {
      const msg = `Can't find server id: ${id}`
      log(msg)
      return res.status(404).send(msg)
    }

    req.hotel = {
      id,
      item
    }

    next()
  }

  start(req, res, next) {
    const { item } = req.hotel

    if (item.start) {
      if (item.env.PORT) {
        item.start()
        next()
      } else {
        getPort()
          .then(port => {
            item.env.PORT = port
            item.start()
            next()
          })
          .catch(error => {
            next(error)
          })
      }
    } else {
      next()
    }
  }

  stop(req, res, next) {
    const { item } = req.hotel

    if (item.stop) {
      item.stop()
    }

    next()
  }

  proxyWeb(req, res, target) {
    const { xfwd, changeOrigin } = req.hotel.item

    this._proxy.web(
      req,
      res,
      {
        target,
        xfwd,
        changeOrigin
      },
      err => {
        log('Proxy - Error', err.message)
        const server = req.hotel.item
        const view = server.start ? 'server-error' : 'target-error'
        res.status(502).render(view, {
          err,
          serverReady,
          server
        })
      }
    )
  }

  proxy(req, res) {
    const [hostname, port] = req.headers.host && req.headers.host.split(':')
    const { item } = req.hotel

    // Handle case where port is set
    // http://app.localhost:5000 should proxy to http://localhost:5000
    if (port) {
      const target = `http://127.0.0.1:${port}`

      log(`Proxy - http://${req.headers.host} → ${target}`)
      return this.proxyWeb(req, res, target)
    }

    // Make sure to send only one response
    const send = once(() => {
      const { target } = item

      log(`Proxy - http://${hostname} → ${target}`)
      this.proxyWeb(req, res, target)
    })

    if (item.start) {
      // Set target
      item.target = `http://localhost:${item.env.PORT}`

      // If server stops, no need to wait for timeout
      item.once('stop', send)

      // When PORT is open, proxy
      serverReady(item.env.PORT, send)
    } else {
      // Send immediatly if item is not a server started by a command
      send()
    }
  }

  redirect(req, res) {
    const { id } = req.params
    const { item } = req.hotel

    // Make sure to send only one response
    const send = once(() => {
      log(`Redirect - ${id} → ${item.target}`)
      res.redirect(item.target)
    })

    if (item.start) {
      // Set target
      item.target = `http://${req.hostname}:${item.env.PORT}`

      // If server stops, no need to wait for timeout
      item.once('stop', send)

      // When PORT is open, redirect
      serverReady(item.env.PORT, send)
    } else {
      // Send immediatly if item is not a server started by a command
      send()
    }
  }

  parseHost(host) {
    const [hostname, port] = host.split(':')
    const tld = new RegExp(`.${daemonConf.tld}$`)
    const id = this.resolve(hostname.replace(tld, ''))
    return { id, hostname, port }
  }

  // Needed to proxy WebSocket from CONNECT
  handleUpgrade(req, socket, head) {
    if (req.headers.host) {
      const { host } = req.headers
      const { id, port } = this.parseHost(host)
      const item = this.find(id)

      if (item) {
        let target
        if (port && port !== '80') {
          target = `ws://127.0.0.1:${port}`
        } else if (item.start) {
          target = `ws://127.0.0.1:${item.env.PORT}`
        } else {
          const { hostname } = url.parse(item.target)
          target = `ws://${hostname}`
        }
        log(`WebSocket - ${host} → ${target}`)
        this._proxy.ws(req, socket, head, { target }, err => {
          log('WebSocket - Error', err.message)
        })
      } else {
        log(`WebSocket - No server matching ${id}`)
      }
    } else {
      log('WebSocket - No host header found')
    }
  }

  // Handle CONNECT, used by WebSockets and https when accessing .localhost domains
  handleConnect(req, socket, head) {
    if (req.headers.host) {
      const { host } = req.headers
      const { id, hostname, port } = this.parseHost(host)

      // If https make socket go through https proxy on 2001
      // TODO find a way to detect https and wss without relying on port number
      if (port === '443') {
        return tcpProxy.proxy(socket, daemonConf.port + 1, hostname)
      }

      const item = this.find(id)

      if (item) {
        if (port && port !== '80') {
          log(`Connect - ${host} → ${port}`)
          tcpProxy.proxy(socket, port)
        } else if (item.start) {
          const { PORT } = item.env
          log(`Connect - ${host} → ${PORT}`)
          tcpProxy.proxy(socket, PORT)
        } else {
          const { hostname, port } = url.parse(item.target)
          const targetPort = port || 80
          log(`Connect - ${host} → ${hostname}:${port}`)
          tcpProxy.proxy(socket, targetPort, hostname)
        }
      } else {
        log(`Connect - Can't find server for ${id}`)
        socket.end()
      }
    } else {
      log('Connect - No host header found')
    }
  }
}


================================================
FILE: src/daemon/index.js
================================================
const exitHook = require('exit-hook')
const httpProxy = require('http-proxy')
const conf = require('../conf')
const pidFile = require('../pid-file')
const pem = require('./pem')
const log = require('./log')
const Group = require('./group')
const Loader = require('./loader')
const App = require('./app')

const group = Group()
const app = App(group)

// Load and watch files
Loader(group)

// Create pid file
pidFile.create()

// Clean exit
exitHook(() => {
  console.log('Exiting')
  console.log('Stop daemon')
  proxy.close()
  app.close()
  group.stopAll()

  console.log('Remove pid file')
  pidFile.remove()
})

// HTTPS proxy
const proxy = httpProxy.createServer({
  target: {
    host: '127.0.0.1',
    port: conf.port
  },
  ssl: pem.generate(),
  ws: true,
  xfwd: true
})

// Start HTTPS proxy and HTTP server
proxy.listen(conf.port + 1)

app.listen(conf.port, conf.host, function() {
  log(`Server listening on port ${conf.host}:${conf.port}`)
})


================================================
FILE: src/daemon/loader.js
================================================
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
const chokidar = require('chokidar')
const log = require('./log')
const common = require('../common')

function getId(file) {
  return path.basename(file, '.json')
}

function handleAdd(group, file) {
  log(`${file} added`)
  const id = getId(file)

  try {
    const conf = JSON.parse(fs.readFileSync(file, 'utf8'))
    group.add(id, conf)
  } catch (err) {
    log(`Error: Failed to parse ${file}`, err)
  }
}

function handleUnlink(group, file, cb) {
  log(`${file} unlinked`)
  const id = getId(file)
  group.remove(id, cb)
}

function handleChange(group, file) {
  log(`${file} changed`)
  handleUnlink(group, file, () => {
    handleAdd(group, file)
  })
}

module.exports = (group, opts = { watch: true }) => {
  const dir = common.serversDir

  // Ensure directory exists
  mkdirp.sync(dir)

  // Watch ~/.hotel/servers
  if (opts.watch) {
    log(`Watching ${dir}`)
    chokidar
      .watch(dir)
      .on('add', file => handleAdd(group, file))
      .on('change', file => handleChange(group, file))
      .on('unlink', file => handleUnlink(group, file))
  }

  // Bootstrap
  fs.readdirSync(dir).forEach(file => {
    handleAdd(group, path.resolve(dir, file))
  })
}


================================================
FILE: src/daemon/log.js
================================================
const tinydate = require('tinydate')
const stamp = tinydate('{HH}:{mm}:{ss}')

module.exports = function log(...args) {
  console.log(stamp(), '-', ...args)
}


================================================
FILE: src/daemon/pem.js
================================================
const fs = require('fs')
const path = require('path')
const tildify = require('tildify')
const selfsigned = require('selfsigned')
const log = require('./log')
const { hotelDir } = require('../common')

const KEY_FILE = path.join(hotelDir, 'key.pem')
const CERT_FILE = path.join(hotelDir, 'cert.pem')

function generate() {
  if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
    log(`Reading self-signed certificate in ${tildify(hotelDir)}`)
    return {
      key: fs.readFileSync(KEY_FILE, 'utf-8'),
      cert: fs.readFileSync(CERT_FILE, 'utf-8')
    }
  } else {
    log(`Generating self-signed certificate in ${tildify(hotelDir)}`)
    const pems = selfsigned.generate([{ name: 'commonName', value: 'hotel' }], {
      days: 365
    })
    fs.writeFileSync(KEY_FILE, pems.private, 'utf-8')
    fs.writeFileSync(CERT_FILE, pems.cert, 'utf-8')

    return { key: pems.private, cert: pems.cert }
  }
}

module.exports = {
  KEY_FILE,
  CERT_FILE,
  generate
}


================================================
FILE: src/daemon/public/error.css
================================================
body {
  font-family: -apple-system, BlinkMacSystemFont,
    "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
  color: #212121;
  padding: 0;
  margin: 0 auto;
  display: flex;
  max-width: 960px;
  min-height: 100vh;
  flex-direction: column;
}

h1, h2 {
  margin-top: 60px;
}

li {
  list-style: square;
}

pre {
  background: #F9F9F9;
  padding: 20px;
}

a {
  color: inherit;
}

main {
  flex: 1;
}

footer {
  padding: 20px 0;
}


================================================
FILE: src/daemon/routers/api/events.js
================================================
const express = require('express')
const connectSSE = require('connect-sse')
const sse = connectSSE()

function listen(res, group, groupEvent, handler) {
  function removeListener() {
    // Remove group handler
    group.removeListener(groupEvent, handler)

    // Remove self
    res.removeListener('close', removeListener)
    res.removeListener('finish', removeListener)
  }

  group.on(groupEvent, handler)

  res.on('close', removeListener)
  res.on('finish', removeListener)
}

module.exports = group => {
  const router = express.Router()

  router.get('/', sse, (req, res) => {
    // Handler
    function sendState() {
      res.json(group.list())
    }

    // Bootstrap
    sendState()

    // Listen
    listen(res, group, 'change', sendState)
  })

  router.get('/output', sse, (req, res) => {
    function sendOutput(id, data) {
      res.json({
        id,
        output: data.toString()
      })
    }

    // Bootstrap
    const list = group.list()
    Object.keys(list).forEach(id => {
      var mon = list[id]
      if (mon && mon.tail) {
        sendOutput(id, mon.tail)
      }
    })

    // Listen
    listen(res, group, 'output', sendOutput)
  })

  return router
}


================================================
FILE: src/daemon/routers/api/index.js
================================================
const express = require('express')

const ServerRouter = require('./servers')
const EventRouter = require('./events')

module.exports = group => {
  const router = express.Router()

  const servers = ServerRouter(group)
  const events = EventRouter(group)

  router.use('/servers', servers)
  router.use('/events', events)

  return router
}


================================================
FILE: src/daemon/routers/api/servers.js
================================================
const express = require('express')

module.exports = group => {
  const router = express.Router()

  router.get('/', (req, res) => {
    res.json(group.list())
  })

  router.post(
    '/:id/start',
    group.exists.bind(group),
    group.start.bind(group),
    (req, res) => res.end()
  )

  router.post(
    '/:id/stop',
    group.exists.bind(group),
    group.stop.bind(group),
    (req, res) => res.end()
  )

  return router
}


================================================
FILE: src/daemon/routers/index.js
================================================
const express = require('express')
const conf = require('../../conf')
const log = require('../log')

module.exports = function(group) {
  const router = express.Router()

  function pac(req, res) {
    log('Serve proxy.pac')
    if (conf.proxy) {
      res.render('proxy-pac-with-proxy', { conf })
    } else {
      res.render('proxy-pac', { conf })
    }
  }

  router
    .get('/proxy.pac', pac)
    .get(
      '/:id',
      group.exists.bind(group),
      group.start.bind(group),
      group.redirect.bind(group)
    )

  return router
}


================================================
FILE: src/daemon/tcp-proxy.js
================================================
const net = require('net')
const log = require('./log')

module.exports = {
  proxy
}

function proxy(source, targetPort, targetHost) {
  const target = net.connect(targetPort)
  source.pipe(target).pipe(source)

  const handleError = err => {
    log('TCP Proxy - Error', err)
    source.destroy()
    target.destroy()
  }

  source.on('error', handleError)
  target.on('error', handleError)

  source.write(
    'HTTP/1.1 200 Connection Established\r\n' +
      'Proxy-agent: Hotel\r\n' +
      '\r\n'
  )

  return target
}


================================================
FILE: src/daemon/vhosts/tld.js
================================================
const express = require('express')
const conf = require('../../conf')
const log = require('../log')

// *.tld vhost
module.exports = group => {
  const app = express.Router()
  const hotelRegExp = new RegExp(`hotel.${conf.tld}$`)

  app.use((req, res, next) => {
    const { hostname } = req

    // Skip hotel.tld
    if (hotelRegExp.test(hostname)) {
      log(`hotel.${conf.tld}`)
      return next()
    }

    // If hostname is resolved proxy request
    group.exists(req, res, () => {
      group.start(req, res, () => {
        group.proxy(req, res)
      })
    })
  })

  return app
}


================================================
FILE: src/daemon/views/_error.pug
================================================
doctype html
head
  title Error
  meta(charset='utf-8')
  meta(name='viewport', content='width=device-width, initial-scale=1')
  link(rel='icon', type='image/png', href='favicon.png')
  style
    include ../public/error.css
body
  h1 Error
  main
    block content
  
  footer
    a(href='https://github.com/typicode/hotel') readme


================================================
FILE: src/daemon/views/proxy-pac-with-proxy.pug
================================================
.
  // Set conf.proxy in ~/.hotel/conf.json to your proxy address and port.
  // For example: { "proxy": "1.2.3.4:5000" }
  //
  // See also https://en.wikipedia.org/wiki/Private_network
  function FindProxyForURL (url, host) {
    if (dnsDomainIs(host, '.#{conf.tld}')) {
      return 'PROXY 127.0.0.1:#{conf.port}';
    }

    var address = dnsResolve(host);
    if (
      isPlainHostName(host) ||
      dnsDomainIs(host, '.local') ||
      isInNet(address, '10.0.0.0', '255.0.0.0') ||
      isInNet(address, '172.16.0.0',  '255.240.0.0') ||
      isInNet(address, '192.168.0.0',  '255.255.0.0') ||
      isInNet(address, '127.0.0.0', '255.255.255.0')
    ) {
      return 'DIRECT';
    }

    return 'PROXY #{conf.proxy}';
  }


================================================
FILE: src/daemon/views/proxy-pac.pug
================================================
.
  // Proxy only *.#{conf.tld} requests to hotel
  // Configuration file can be found in ~/.hotel
  function FindProxyForURL (url, host) {
    if (dnsDomainIs(host, '.#{conf.tld}')) {
      return 'PROXY 127.0.0.1:#{conf.port}';
    }

    return 'DIRECT';
  }


================================================
FILE: src/daemon/views/server-error.pug
================================================
extends _error.pug

block content
  p Can't connect to server on PORT=#{server.env.PORT}
  p: a(href=`http://localhost:${server.env.PORT}`) http://localhost:#{server.env.PORT}

  h2 Possible causes
  ul
    li Server crashed or timeout of #{serverReady.timeout}ms exceeded.
    li Server is not listening on PORT environment variable.

  p Try to reload or check logs.

  h2 Logs
  pre: code= server.command.join(' ')
  pre: code= server.tail


================================================
FILE: src/daemon/views/target-error.pug
================================================
extends _error.pug

block content
  p Cannot proxy request to 
    a(href=server.target)= server.target

  pre: code=err.message


================================================
FILE: src/get-cmd.js
================================================
const os = require('os')
const unquote = require('unquote')

module.exports = cmd =>
  os.platform() === 'win32'
    ? ['cmd', '/c'].concat(cmd.split(' '))
    : ['sh', '-c'].concat(unquote(cmd))


================================================
FILE: src/pid-file.js
================================================
const fs = require('fs')
const { pidFile } = require('./common')

module.exports = {
  create,
  read,
  remove
}

function create() {
  console.log('create', pidFile, process.pid)
  return fs.writeFileSync(pidFile, process.pid.toString())
}

function read() {
  if (fs.existsSync(pidFile)) {
    return fs.readFileSync(pidFile, 'utf-8')
  }
}

function remove() {
  if (fs.existsSync(pidFile)) {
    fs.unlinkSync(pidFile)
  }
}


================================================
FILE: src/scripts/uninstall.js
================================================
const fs = require('fs')
const { startupFile, pidFile } = require('../common')

function killProcess() {
  if (!fs.existsSync(pidFile)) return

  const pid = fs.readFileSync(pidFile, 'utf-8')
  try {
    process.kill(pid)
  } catch (err) {}

  fs.unlinkSync(pidFile)
}

function removeStartup() {
  if (!fs.existsSync(startupFile)) return
  const startupFilePath = fs.readFileSync(startupFile, 'utf-8')
  fs.unlinkSync(startupFile)

  if (!fs.existsSync(startupFilePath)) return
  fs.unlinkSync(startupFilePath)
}

module.exports = () => {
  killProcess()
  removeStartup()
}


================================================
FILE: test/_setup.js
================================================
const os = require('os')
const sinon = require('sinon')
const tempy = require('tempy')

// Required by AVA, see package.json
sinon.stub(os, 'homedir').returns(tempy.directory())


================================================
FILE: test/cli/daemon.js
================================================
const fs = require('fs')
const path = require('path')
const test = require('ava')
const sinon = require('sinon')
const untildify = require('untildify')
const userStartup = require('user-startup')
const common = require('../../src/common')
const cli = require('../../src/cli')

test.before(() => {
  sinon.stub(userStartup, 'create')
  sinon.stub(userStartup, 'remove')
  sinon.stub(process, 'kill')
})

test('start should start daemon', t => {
  const node = process.execPath
  const daemonFile = path.join(__dirname, '../../src/daemon')
  const daemonLog = path.resolve(untildify('~/.hotel/daemon.log'))

  cli(['', '', 'start'])

  sinon.assert.calledWithExactly(
    userStartup.create,
    'hotel',
    node,
    [daemonFile],
    daemonLog
  )

  t.is(
    fs.readFileSync(common.startupFile, 'utf-8'),
    userStartup.getFile('hotel'),
    'startupFile should point to startup file path'
  )

  t.pass()
})

test('stop should stop daemon', t => {
  fs.writeFileSync(common.pidFile, '1234')

  cli(['', '', 'stop'])

  sinon.assert.calledWithExactly(userStartup.remove, 'hotel')
  sinon.assert.calledWithExactly(process.kill, '1234')
  t.pass()
})


================================================
FILE: test/cli/run.js
================================================
const path = require('path')
const test = require('ava')
const sinon = require('sinon')
const cli = require('../../src/cli')
const servers = require('../../src/cli/servers')
const run = require('../../src/cli/run')

const appDir = path.join(__dirname, '../fixtures/app')

test('spawn with port', t => {
  const status = 1

  sinon.spy(servers, 'add')
  sinon.spy(servers, 'rm')

  // Stub _exit to avoid messing with process.exit
  sinon.stub(run, '_exit')
  // Stub _spawnSync to immediately return avoid messing with child_process
  sinon.stub(run, '_spawnSync').callsFake(() => ({ status }))

  process.chdir(appDir)

  const opts = { port: 5000 }

  run.spawn('node index.js', opts)

  // test that everything was called correctly
  t.true(servers.add.called)
  t.regex(
    servers.add.firstCall.args[0],
    /http:\/\/localhost:/,
    'should add a target'
  )

  t.is(servers.add.firstCall.args[1], opts, 'should pass options to add')

  t.true(servers.rm.called)
  t.is(servers.rm.firstCall.args[0], opts, 'should use same options to remove')

  t.true(run._exit.called)
  t.is(run._exit.firstCall.args[0], status, 'should exit')
})

test('cli run should call run.spawn', t => {
  sinon.stub(run, 'spawn')
  cli(['', '', 'run', 'node index.js'])

  t.is(run.spawn.firstCall.args[0], 'node index.js')
})


================================================
FILE: test/cli/servers.js
================================================
const fs = require('fs')
const path = require('path')
const test = require('ava')
const sinon = require('sinon')
const servers = require('../../src/cli/servers')
const cli = require('../../src/cli')
const { serversDir } = require('../../src/common')

const appDir = path.join(__dirname, '../fixtures/app')

test('add should create file', t => {
  process.chdir(appDir)
  cli(['', '', 'add', 'node index.js'])

  const file = path.join(serversDir, 'app.json')
  const conf = {
    cmd: 'node index.js',
    cwd: process.cwd(),
    env: {
      PATH: process.env.PATH
    }
  }

  const actual = JSON.parse(fs.readFileSync(file))
  t.deepEqual(actual, conf)
})

test('add should create file with URL safe characters by defaults', t => {
  cli(['', '', 'add', 'node index.js', '--dir', '/_-Some Project_Name--'])

  const file = path.join(serversDir, 'some-project-name.json')

  t.true(fs.existsSync(file))
})

test('add should create file with URL safe characters by defaults', t => {
  cli(['', '', 'add', 'node index.js', '--name', '/_-Some Project_Name--'])

  const file = path.join(serversDir, 'some-project-name.json')

  t.true(fs.existsSync(file))
})

test('add should support options', t => {
  process.env.FOO = 'FOO_VALUE'
  process.env.BAR = 'BAR_VALUE'
  const cmd = 'node index.js'
  const name = 'project'
  const port = 3000
  const out = '/some/path/out.log'
  const env = ['FOO', 'BAR']

  cli([
    '',
    '',
    'add',
    cmd,
    '-n',
    name,
    '-p',
    port,
    '-o',
    out,
    '-e',
    env[0],
    env[1],
    '-x',
    '--co',
    '--http-proxy-env'
  ])

  const file = path.join(serversDir, 'project.json')
  const conf = {
    cmd: 'node index.js',
    cwd: process.cwd(),
    out: out,
    env: {
      PATH: process.env.PATH,
      FOO: process.env.FOO,
      BAR: process.env.BAR,
      PORT: port
    },
    xfwd: true,
    changeOrigin: true,
    httpProxyEnv: true
  }

  const actual = JSON.parse(fs.readFileSync(file))
  t.deepEqual(actual, conf)
})

test('add should support option aliases', t => {
  process.env.FOO = 'FOO'
  const cmd = 'node index.js'
  const name = 'alias-test'

  cli(['', '', 'add', cmd, '-n', name])

  const file = path.join(serversDir, 'alias-test.json')
  t.true(fs.existsSync(file))
})

test('add should support URL', t => {
  cli(['', '', 'add', 'http://1.2.3.4', '-n', 'proxy'])

  const file = path.join(serversDir, 'proxy.json')
  const conf = {
    target: 'http://1.2.3.4'
  }

  const actual = JSON.parse(fs.readFileSync(file))
  t.deepEqual(actual, conf)
})

test('add should support URL and options', t => {
  cli(['', '', 'add', 'http://1.2.3.4', '-n', 'proxy', '-x', '--co'])

  const file = path.join(serversDir, 'proxy.json')
  const conf = {
    target: 'http://1.2.3.4',
    xfwd: true,
    changeOrigin: true
  }

  const actual = JSON.parse(fs.readFileSync(file))
  t.deepEqual(actual, conf)
})

/*
FIXME fails for an unknown reason only in CI, process.chdir doesn't seem to change dir
test('rm should remove file', (t) => {
  const file = path.join(serversDir, 'other-app.json')
  fs.writeFileSync(file, '')

  process.chdir(otherAppDir)
  cli(['', '', 'rm'])
  t.true(!fs.existsSync(file))
})
*/

test('rm should remove file using name', t => {
  const name = 'some-other-app'
  const file = path.join(serversDir, `${name}.json`)

  fs.writeFileSync(file, '')
  cli(['', '', 'rm', '-n', name])
  t.true(!fs.existsSync(file))
})

test('ls', t => {
  sinon.spy(servers, 'ls')
  cli(['', '', 'ls'])
  sinon.assert.calledOnce(servers.ls)
  t.pass()
})

test('ls should ignore non-json files', t => {
  const name = '.DS_Store'
  const file = path.join(serversDir, `${name}`)
  fs.writeFileSync(file, '')

  t.notThrows(() => {
    cli(['', '', 'ls'])
  })
})


================================================
FILE: test/daemon/app.js
================================================
const fs = require('fs')
const path = require('path')
const http = require('http')
const test = require('ava')
const request = require('supertest')
const conf = require('../../src/conf')
const App = require('../../src/daemon/app')
const Group = require('../../src/daemon/group')
const Loader = require('../../src/daemon/loader')
const servers = require('../../src/cli/servers')

const { tld } = conf
let app

function ensureDistExists(t) {
  const exists = fs.existsSync(path.join(__dirname, '../../dist'))
  t.true(exists, 'dist directory must exist (try to run `npm run build`)')
}

test.before(() => {
  // Set request timeout to 20 seconds instead of 5 seconds for slower CI servers
  conf.timeout = 20000

  // Fake server to respond to URL
  http
    .createServer((req, res) => {
      res.statusCode = 200
      res.end(`ok - host: ${req.headers.host}`)
    })
    .listen(4000)

  // Add server
  servers.add('node index.js', {
    name: 'node',
    port: 51234,
    dir: path.join(__dirname, '../fixtures/app'),
    out: '/tmp/logs/app.log',
    xfwd: true
  })

  // Add server with subdomain
  servers.add('node index.js', {
    name: 'subdomain.node',
    port: 51235,
    dir: path.join(__dirname, '../fixtures/app'),
    out: '/tmp/logs/app.log'
  })

  // Add server with custom env
  process.env.FOO = 'FOO_VALUE'
  servers.add('node index.js', {
    name: 'custom-env',
    port: 51236,
    dir: path.join(__dirname, '../fixtures/app'),
    out: '/tmp/logs/app.log',
    env: ['FOO'],
    httpProxyEnv: true
  })

  // Add failing server
  servers.add('unknown-cmd', { name: 'failing' })

  // Add server and proxy for testing removal
  servers.add('unknown-cmd', { name: 'server-to-remove' })
  servers.add('http://example.com', { name: 'proxy-to-remove' })

  // Add URL
  servers.add('http://localhost:4000', { name: 'proxy' })

  // Add https URL
  servers.add('https://jsonplaceholder.typicode.com', {
    name: 'working-proxy-with-https-target',
    changeOrigin: true
  })

  servers.add('https://jsonplaceholder.typicode.com', {
    name: 'failing-proxy-with-https-target'
  })

  // Add unavailable URL
  servers.add('http://localhost:4100', { name: 'unavailable-proxy' })

  const group = Group()
  app = App(group)
  app.group = group
  Loader(group, { watch: false })
})

test.cb.after(t => app.group.stopAll(t.end))

//
// Test daemon/vhosts/tld.js
//

test.cb('GET http://hotel.tld should return 200', t => {
  ensureDistExists(t)
  request(app)
    .get('/')
    .set('Host', `hotel.${tld}`)
    .expect(200, t.end)
})

test.cb(
  'GET http://node.tld should proxy request and host should be node.tld',
  t => {
    request(app)
      .get('/')
      .set('Host', `node.${tld}`)
      .expect(new RegExp(`host: node.${tld}`))
      .expect(200, /Hello World/, (err, res) => {
        if (err) return t.end(err)
        t.notRegex(
          res.text,
          /http:\/\/127.0.0.1:2000\/proxy.pac/,
          `shouldn't be started with HTTP_PROXY env set`
        )
        return t.end()
      })
  }
)

test.cb('GET http://subdomain.node.tld should proxy request', t => {
  request(app)
    .get('/')
    .set('Host', `subdomain.node.${tld}`)
    .expect(200, /Hello World/, t.end)
})

test.cb('GET http://any.node.tld should proxy request', t => {
  request(app)
    .get('/')
    .set('Host', `any.node.${tld}`)
    .expect(200, /Hello World/, t.end)
})

test.cb('GET http://unknown.tld should return 404', t => {
  request(app)
    .get('/')
    .set('Host', `unknown.${tld}`)
    .expect(404, t.end)
})

test.cb('GET http://failing.tld should return 502', t => {
  request(app)
    .get('/')
    .set('Host', `failing.${tld}`)
    .expect(502, t.end)
})

test.cb(
  'GET http://proxy.tld should return 200 and host should be proxy.localhost',
  t => {
    request(app)
      .get('/')
      .set('Host', `proxy.${tld}`)
      .expect(200, new RegExp(`host: proxy.${tld}`), t.end)
  }
)

test.cb('GET http://node.tld:4000 should proxy to localhost:4000', t => {
  request(app)
    .get('/')
    .set('Host', `node.${tld}:4000`)
    .expect(200, /ok/, t.end)
})

//
// Test proxy to URLs
//

test.cb(
  'GET http://working-proxy-with-https-target.tld should return 200',
  t => {
    request(app)
      .get('/')
      .set('Host', `working-proxy-with-https-target.${tld}`)
      .expect(200, t.end)
  }
)

test.cb(
  'GET http://failing-proxy-with-https-target.tld should return 502',
  t => {
    request(app)
      .get('/')
      .set('Host', `failing-proxy-with-https-target.${tld}`)
      .expect(502, t.end)
  }
)

test.cb('GET http://unavailable-proxy.tld should return 502', t => {
  request(app)
    .get('/')
    .set('Host', `unavailable-proxy.${tld}`)
    .expect(502, t.end)
})

//
// TEST daemon/routers/api.js
//

test.cb('GET /_/servers', t => {
  request(app)
    .get('/_/servers')
    .expect(200, (err, res) => {
      if (err) return t.end(err)
      t.is(Object.keys(res.body).length, 10, 'got wrong number of servers')
      t.end()
    })
})

test.cb('POST /_/servers/:id/start', t => {
  request(app)
    .post('/_/servers/node/start')
    .expect(200, err => {
      if (err) return t.end(err)
      t.is(app.group.find('node').status, 'running')
      t.end()
    })
})

test.cb('POST /_/servers/:id/stop', t => {
  request(app)
    .post('/_/servers/node/stop')
    .expect(200, err => {
      if (err) return t.end(err)
      t.not(app.group.find('node').status, 'running')
      t.end()
    })
})

//
// TEST daemon/routers/index.js
//

test.cb('GET /proxy.pac should serve /proxy.pac', t => {
  request(app)
    .get('/proxy.pac')
    .expect(200, t.end)
})

test.cb('GET http://localhost:2000/node should redirect to node server', t => {
  if (process.env.APPVEYOR) return t.end()
  request(app)
    .get('/node')
    .set('Host', 'localhost')
    .expect('location', /http:\/\/localhost:51234/)
    .expect(302, t.end)
})

test.cb(
  'GET http://127.0.0.1:2000/node should use the same hostname to redirect',
  t => {
    // temporary disable this test on AppVeyor
    // Randomly fails
    if (process.env.APPVEYOR) return t.end()
    request(app)
      .get('/node')
      .expect('location', /http:\/\/127.0.0.1:51234/)
      .expect(302, t.end)
  }
)

test.cb('GET http://localhost:2000/proxy should redirect to target', t => {
  if (process.env.APPVEYOR) return t.end()
  request(app)
    .get('/proxy')
    .set('Host', 'localhost')
    .expect('location', /http:\/\/localhost:4000/)
    .expect(302, t.end)
})

//
// Test daemon/app.js
//

test.cb('GET / should render index.html', t => {
  ensureDistExists(t)
  request(app)
    .get('/')
    .expect(200, t.end)
})

//
// Test env variables
//

test.cb('GET / should contain custom env values', t => {
  request(app)
    .get('/')
    .set('Host', `custom-env.${tld}`)
    .expect(200, /FOO_VALUE/, t.end)
})

test.cb('GET / should contain proxy env values', t => {
  request(app)
    .get('/')
    .set('Host', `custom-env.${tld}`)
    .expect(200, /http:\/\/127.0.0.1:2000\/proxy.pac/, t.end)
})

//
// Test headers
//

test.cb('GET node.tld/ should contain X-FORWARD headers', t => {
  request(app)
    .get('/')
    .set('Host', `node.${tld}`)
    .expect(200, new RegExp(`x-forwarded-host: node.${tld}`), t.end)
})

test.cb('GET subdomain.node.tld/ should not contain X-FORWARD headers', t => {
  request(app)
    .get('/')
    .set('Host', `subdomain.node.${tld}`)
    .expect(200, /x-forwarded-host: undefined/, t.end)
})

//
// Test remove
//

test.cb('Removing a server should make it unavailable', t => {
  t.truthy(app.group.find('server-to-remove'))
  app.group.remove('server-to-remove', () => {
    request(app)
      .get('/')
      .set('Host', `server-to-remove.${tld}`)
      .expect(404, t.end)
  })
})

test.cb('Removing a proxy should make it unavailable', t => {
  t.truthy(app.group.find('proxy-to-remove'))
  app.group.remove('proxy-to-remove', () => {
    request(app)
      .get('/')
      .set('Host', `proxy-to-remove.${tld}`)
      .expect(404, t.end)
  })
})


================================================
FILE: test/daemon/group.js
================================================
const test = require('ava')
const sinon = require('sinon')
const Group = require('../../src/daemon/group')
const tcpProxy = require('../../src/daemon/tcp-proxy')
const conf = require('../../src/conf')

const { tld } = conf
sinon.stub(tcpProxy, 'proxy')

test('group.resolve should find the correct server or target id', t => {
  const group = Group()
  const conf = { target: 'http://example.com' }
  group.add('app', conf)
  group.add('foo.app', conf)

  t.is(group.resolve('app'), 'app')
  t.is(group.resolve('bar.app'), 'app')
  t.is(group.resolve('foo.app'), 'foo.app')
  t.is(group.resolve('baz.foo.app'), 'foo.app')
})

test('group.handleUpgrade with proxy', t => {
  const group = Group()
  const target = 'example.com'
  const req = {
    headers: {
      host: `proxy.${tld}:80`
    }
  }
  const head = {}
  const socket = {}

  sinon.stub(group._proxy, 'ws')

  group.add('proxy', { target: `http://${target}` })
  group.handleUpgrade(req, head, socket)

  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {
    target: `ws://${target}`
  })
  t.pass()
})

test('group.handleUpgrade with app', t => {
  const group = Group()
  const PORT = '9000'
  const req = {
    headers: {
      host: `app.${tld}:80`
    }
  }
  const head = {}
  const socket = {}

  sinon.stub(group._proxy, 'ws')

  group.add('app', {
    cmd: 'cmd',
    cwd: '/some/path',
    env: {
      PORT
    }
  })
  group.handleUpgrade(req, head, socket)

  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {
    target: `ws://127.0.0.1:${PORT}`
  })
  t.pass()
})

test('group.handleUpgrade with app and port, port should take precedence', t => {
  const port = 5000
  const group = Group()
  const req = {
    headers: {
      host: `app.${tld}:${port}`
    }
  }
  const head = {}
  const socket = {}

  sinon.stub(group._proxy, 'ws')

  group.add('app', {
    cmd: 'cmd',
    cwd: '/some/path'
  })
  group.handleUpgrade(req, head, socket)

  sinon.assert.calledWith(group._proxy.ws, req, head, socket, {
    target: `ws://127.0.0.1:${port}`
  })
  t.pass()
})

test('group.handleConnect with proxy', t => {
  const group = Group()
  const target = 'example.com'
  const req = {
    headers: {
      host: `proxy.${tld}:80`
    }
  }
  const head = {}
  const socket = {}

  tcpProxy.proxy.reset()

  group.add('proxy', { target: `http://${target}` })
  group.handleConnect(req, head, socket)

  sinon.assert.calledWith(tcpProxy.proxy, socket, 80, 'example.com')
  t.pass()
})

test('group.handleConnect with app', t => {
  const group = Group()
  const PORT = '9000'
  const req = {
    headers: {
      host: `app.${tld}:80`
    }
  }
  const head = {}
  const socket = {}

  tcpProxy.proxy.reset()

  group.add('app', {
    cmd: 'cmd',
    cwd: '/some/path',
    env: {
      PORT
    }
  })
  group.handleConnect(req, head, socket)

  sinon.assert.calledWith(tcpProxy.proxy, socket, PORT)
  t.pass()
})

test('group.handleConnect on port 443', t => {
  const group = Group()
  const req = {
    headers: {
      host: `anything.${tld}:443`
    }
  }
  const head = {}
  const socket = {}

  tcpProxy.proxy.reset()
  group.handleConnect(req, head, socket)

  sinon.assert.calledWith(tcpProxy.proxy, socket, conf.port + 1)
  t.pass()
})


================================================
FILE: test/daemon/pem.js
================================================
const fs = require('fs')
const test = require('ava')
// TODO rename to KEY_NAME
const { KEY_FILE, CERT_FILE, generate } = require('../../src/daemon/pem')
const { hotelDir } = require('../../src/common')

test.before(() => {
  fs.mkdirSync(hotelDir)
})

test("should create cert files if they don't exist", t => {
  const { key, cert } = generate()
  t.true(fs.existsSync(KEY_FILE))
  t.true(fs.existsSync(CERT_FILE))
  t.is(key, fs.readFileSync(KEY_FILE, 'utf-8'))
  t.is(cert, fs.readFileSync(CERT_FILE, 'utf-8'))
})

test('should read cert files if they exist', t => {
  fs.writeFileSync(KEY_FILE, 'foo')
  fs.writeFileSync(CERT_FILE, 'bar')
  const { key, cert } = generate()
  t.is('foo', key)
  t.is('bar', cert)
})


================================================
FILE: test/fixtures/app/index.js
================================================
var http = require('http')

http
  .createServer(function(req, res) {
    console.log(req.headers)
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    res.end(
      [
        'Hello World',
        process.env.FOO,
        process.env.HTTP_PROXY,
        'x-forwarded-host: ' + req.headers['x-forwarded-host'],
        'host: ' + req.headers.host
      ].join(' ')
    )
  })
  .listen(process.env.PORT, '127.0.0.1')

console.log('Server listening on port', process.env.PORT)


================================================
FILE: test/fixtures/verbose/index.js
================================================
setInterval(
  () =>
    console.log('[32m green text with blank line - ' + Math.random() + '\n'),
  200
)


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "target": "es6",
    "module": "commonjs",
    "jsx": "react",
    "strict": true,
    "experimentalDecorators": true
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

================================================
FILE: tslint.json
================================================
{
    "defaultSeverity": "error",
    "extends": [
      "tslint:recommended",
      "tslint-config-prettier"
    ],
    "jsRules": {},
    "rulesDirectory": [
      "tslint-plugin-prettier"
    ],
    "rules": {
      "prettier": [
        true,
        {
          "semi": false,
          "singleQuote": true
        }
      ]
    }
  }

================================================
FILE: webpack.common.js
================================================
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/app/index.tsx',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/app/index.html'
    })
  ]
}


================================================
FILE: webpack.dev.js
================================================
const merge = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist',
    proxy: {
      '/_': 'http://localhost:2000'
    }
  }
})


================================================
FILE: webpack.prod.js
================================================
const webpack = require('webpack')
const merge = require('webpack-merge')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  plugins: [
    new UglifyJSPlugin({
      sourceMap: true
    }),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
})
Download .txt
gitextract_2x2tk0um/

├── .babelrc
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .npmignore
├── .stylelintrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── bin/
│   └── uninstall.js
├── docs/
│   ├── Docker.md
│   └── README.md
├── nodemon.json
├── package.json
├── src/
│   ├── app/
│   │   ├── Store.ts
│   │   ├── api.ts
│   │   ├── components/
│   │   │   ├── App/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Content/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Link/
│   │   │   │   └── index.tsx
│   │   │   ├── Nav/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   ├── Splash/
│   │   │   │   ├── index.css
│   │   │   │   └── index.tsx
│   │   │   └── Switch/
│   │   │       ├── index.css
│   │   │       └── index.tsx
│   │   ├── formatter.ts
│   │   ├── global.d.ts
│   │   ├── index.html
│   │   └── index.tsx
│   ├── cli/
│   │   ├── bin.js
│   │   ├── daemon.js
│   │   ├── index.js
│   │   ├── run.js
│   │   └── servers.js
│   ├── common.js
│   ├── conf.js
│   ├── daemon/
│   │   ├── app.js
│   │   ├── group.js
│   │   ├── index.js
│   │   ├── loader.js
│   │   ├── log.js
│   │   ├── pem.js
│   │   ├── public/
│   │   │   └── error.css
│   │   ├── routers/
│   │   │   ├── api/
│   │   │   │   ├── events.js
│   │   │   │   ├── index.js
│   │   │   │   └── servers.js
│   │   │   └── index.js
│   │   ├── tcp-proxy.js
│   │   ├── vhosts/
│   │   │   └── tld.js
│   │   └── views/
│   │       ├── _error.pug
│   │       ├── proxy-pac-with-proxy.pug
│   │       ├── proxy-pac.pug
│   │       ├── server-error.pug
│   │       └── target-error.pug
│   ├── get-cmd.js
│   ├── pid-file.js
│   └── scripts/
│       └── uninstall.js
├── test/
│   ├── _setup.js
│   ├── cli/
│   │   ├── daemon.js
│   │   ├── run.js
│   │   └── servers.js
│   ├── daemon/
│   │   ├── app.js
│   │   ├── group.js
│   │   └── pem.js
│   └── fixtures/
│       ├── app/
│       │   └── index.js
│       └── verbose/
│           └── index.js
├── tsconfig.json
├── tslint.json
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
Download .txt
SYMBOL INDEX (93 symbols across 22 files)

FILE: src/app/Store.ts
  type IProxy (line 6) | interface IProxy {
  type ILine (line 10) | interface ILine {
  type IMonitor (line 15) | interface IMonitor {
  constant RUNNING (line 24) | const RUNNING = 'running'
  constant STOPPED (line 25) | const STOPPED = 'stopped'
  constant MAX_OUTPUT_LENGTH (line 26) | const MAX_OUTPUT_LENGTH = 1000
  function clear (line 28) | function clear(servers: Map<string, IMonitor | IProxy>, data: any) {
  class Store (line 36) | class Store {
    method constructor (line 42) | constructor() {
    method watchServers (line 48) | public watchServers() {
    method watchOutput (line 81) | public watchOutput() {
    method selectMonitor (line 103) | public selectMonitor(monitorId: string) {
    method toggleMonitor (line 109) | public toggleMonitor(monitorId: string) {
    method clearOutput (line 124) | public clearOutput(monitorId: string) {

FILE: src/app/api.ts
  type IEvent (line 1) | interface IEvent {
  function fetchServers (line 5) | function fetchServers() {
  function watchServers (line 9) | function watchServers(cb: (data: any) => void) {
  function watchOutput (line 25) | function watchOutput(cb: (data: any) => void) {
  function startMonitor (line 36) | function startMonitor(id: string) {
  function stopMonitor (line 40) | function stopMonitor(id: string) {

FILE: src/app/components/App/index.tsx
  type IProps (line 9) | interface IProps {
  function App (line 13) | function App({ store }: IProps) {

FILE: src/app/components/Content/index.tsx
  type IProps (line 10) | interface IProps {
  class Content (line 14) | @observer
    method componentWillUpdate (line 19) | public componentWillUpdate() {
    method componentDidUpdate (line 25) | public componentDidUpdate() {
    method isAtBottom (line 31) | public isAtBottom() {
    method scrollToBottom (line 40) | public scrollToBottom() {
    method onScroll (line 46) | public onScroll() {
    method render (line 50) | public render() {

FILE: src/app/components/Link/index.tsx
  function href (line 4) | function href(id: string) {
  type IProps (line 16) | interface IProps {
  function Link (line 20) | function Link({ id }: IProps) {

FILE: src/app/components/Nav/index.tsx
  type IProps (line 13) | interface IProps {
  function Nav (line 17) | function Nav({ store }: IProps) {

FILE: src/app/components/Splash/index.tsx
  function Splash (line 4) | function Splash() {

FILE: src/app/components/Switch/index.tsx
  type IProps (line 4) | interface IProps {
  function Switch (line 9) | function Switch({ onClick = () => null, checked }: IProps) {

FILE: src/app/formatter.ts
  function blankLine (line 5) | function blankLine(val: string) {
  function formatLines (line 9) | function formatLines(str: string): string[] {
  function statusTitle (line 18) | function statusTitle(monitor: IMonitor) {

FILE: src/app/global.d.ts
  type Window (line 5) | interface Window { EventSource: any }

FILE: src/cli/daemon.js
  function start (line 15) | function start() {
  function stop (line 31) | function stop() {

FILE: src/cli/run.js
  method _spawnSync (line 10) | _spawnSync(...args) {
  method _exit (line 15) | _exit(...args) {
  method spawn (line 19) | spawn(cmd, opts = {}) {

FILE: src/cli/servers.js
  function isUrl (line 16) | function isUrl(str) {
  function domainify (line 21) | function domainify(str) {
  function getId (line 32) | function getId(cwd) {
  function getServerFile (line 36) | function getServerFile(id) {
  function add (line 40) | function add(param, opts = {}) {
  function rm (line 118) | function rm(opts = {}) {
  function ls (line 132) | function ls() {

FILE: src/daemon/group.js
  class Group (line 19) | class Group extends EventEmitter {
    method constructor (line 20) | constructor() {
    method _output (line 29) | _output(id, data) {
    method _log (line 33) | _log(mon, logFile, data) {
    method _change (line 47) | _change() {
    method list (line 55) | list() {
    method find (line 59) | find(id) {
    method add (line 63) | add(id, conf) {
    method remove (line 147) | remove(id, cb) {
    method stopAll (line 163) | stopAll(cb) {
    method update (line 173) | update(id, conf) {
    method resolve (line 181) | resolve(str) {
    method exists (line 203) | exists(req, res, next) {
    method start (line 229) | start(req, res, next) {
    method stop (line 252) | stop(req, res, next) {
    method proxyWeb (line 262) | proxyWeb(req, res, target) {
    method proxy (line 286) | proxy(req, res) {
    method redirect (line 322) | redirect(req, res) {
    method parseHost (line 347) | parseHost(host) {
    method handleUpgrade (line 355) | handleUpgrade(req, socket, head) {
    method handleConnect (line 384) | handleConnect(req, socket, head) {

FILE: src/daemon/loader.js
  function getId (line 8) | function getId(file) {
  function handleAdd (line 12) | function handleAdd(group, file) {
  function handleUnlink (line 24) | function handleUnlink(group, file, cb) {
  function handleChange (line 30) | function handleChange(group, file) {

FILE: src/daemon/pem.js
  constant KEY_FILE (line 8) | const KEY_FILE = path.join(hotelDir, 'key.pem')
  constant CERT_FILE (line 9) | const CERT_FILE = path.join(hotelDir, 'cert.pem')
  function generate (line 11) | function generate() {

FILE: src/daemon/routers/api/events.js
  function listen (line 5) | function listen(res, group, groupEvent, handler) {
  function sendState (line 26) | function sendState() {
  function sendOutput (line 38) | function sendOutput(id, data) {

FILE: src/daemon/routers/index.js
  function pac (line 8) | function pac(req, res) {

FILE: src/daemon/tcp-proxy.js
  function proxy (line 8) | function proxy(source, targetPort, targetHost) {

FILE: src/pid-file.js
  function create (line 10) | function create() {
  function read (line 15) | function read() {
  function remove (line 21) | function remove() {

FILE: src/scripts/uninstall.js
  function killProcess (line 4) | function killProcess() {
  function removeStartup (line 15) | function removeStartup() {

FILE: test/daemon/app.js
  function ensureDistExists (line 15) | function ensureDistExists(t) {
Condensed preview — 76 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
  {
    "path": ".babelrc",
    "chars": 145,
    "preview": "{\n  \"presets\": [\n    [\"env\", {\n      \"targets\": {\n        \"node\": \"6\"\n      }\n    }]\n  ],\n  \"plugins\": [\n    \"transform-"
  },
  {
    "path": ".eslintrc.js",
    "chars": 234,
    "preview": "module.exports = {\n  extends: ['standard', 'prettier'],\n  plugins: ['prettier'],\n  rules: {\n    'prettier/prettier': [\n "
  },
  {
    "path": ".gitattributes",
    "chars": 12,
    "preview": "* text=auto\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: typicode\n"
  },
  {
    "path": ".gitignore",
    "chars": 45,
    "preview": ".DS_Store\n*.log\nnode_modules\nlib\ndist\nstatic\n"
  },
  {
    "path": ".npmignore",
    "chars": 3,
    "preview": "src"
  },
  {
    "path": ".stylelintrc",
    "chars": 81,
    "preview": "{\n  \"extends\": [ \"stylelint-config-standard\", \"stylelint-config-recess-order\" ]\n}"
  },
  {
    "path": ".travis.yml",
    "chars": 87,
    "preview": "language: node_js\nnode_js:\n  - \"stable\"\n  - \"6\"\nscript:\n  - npm run build\n  - npm test\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4525,
    "preview": "# 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"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015-present \n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 8039,
    "preview": "# hotel [![](https://badge.fury.io/js/hotel.svg)](https://www.npmjs.com/package/hotel)\n\n> Start apps from your browser a"
  },
  {
    "path": "appveyor.yml",
    "chars": 220,
    "preview": "environment:\n  nodejs_version: '6'\n\ninstall:\n  - ps: Install-Product node $env:nodejs_version\n  - npm install --ignore-s"
  },
  {
    "path": "bin/uninstall.js",
    "chars": 38,
    "preview": "require('../lib/scripts/uninstall')()\n"
  },
  {
    "path": "docs/Docker.md",
    "chars": 1096,
    "preview": "# Docker\n\n[Docker](https://www.docker.com/) is a software container platform that integrates easily into Hotel.\n\n### Doc"
  },
  {
    "path": "docs/README.md",
    "chars": 1806,
    "preview": "# Configuring local .localhost domains\n\n_This step is totally optional and you can use hotel without it._\n\nTo use local "
  },
  {
    "path": "nodemon.json",
    "chars": 95,
    "preview": "{\n  \"exec\": \"babel-node\",\n  \"ignore\": [\n    \"src/app/**/*\",\n    \"src/daemon/public/**/*\"\n  ]\n}\n"
  },
  {
    "path": "package.json",
    "chars": 4064,
    "preview": "{\n  \"name\": \"hotel\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Local domains for everyone and more! \",\n  \"main\": \"lib\",\n  "
  },
  {
    "path": "src/app/Store.ts",
    "chars": 3066,
    "preview": "import * as uniqueId from 'lodash.uniqueid'\nimport { action, computed, observable } from 'mobx'\nimport * as api from './"
  },
  {
    "path": "src/app/api.ts",
    "chars": 1079,
    "preview": "interface IEvent {\n  data: string\n}\n\nexport function fetchServers() {\n  return window.fetch('/_/servers').then(response "
  },
  {
    "path": "src/app/components/App/index.css",
    "chars": 659,
    "preview": "* {\n  box-sizing: border-box;\n}\n\n:root {\n  --primary-light: #484848;\n  --primary: #212121;\n  --primary: #282c2f;\n  --pri"
  },
  {
    "path": "src/app/components/App/index.tsx",
    "chars": 481,
    "preview": "import { observer } from 'mobx-react'\nimport * as React from 'react'\nimport Store from '../../Store'\nimport Content from"
  },
  {
    "path": "src/app/components/Content/index.css",
    "chars": 570,
    "preview": ".content {\n  height: 100vh;\n  overflow-y: scroll;\n  background-color: var(--primary);\n}\n\n.content a {\n  color: white;\n}\n"
  },
  {
    "path": "src/app/components/Content/index.tsx",
    "chars": 2144,
    "preview": "import { observer } from 'mobx-react'\nimport * as React from 'react'\nimport * as MdArrowDownward from 'react-icons/lib/m"
  },
  {
    "path": "src/app/components/Link/index.tsx",
    "chars": 580,
    "preview": "import * as React from 'react'\nimport { IMonitor, IProxy } from '../../Store'\n\nfunction href(id: string) {\n  const { pro"
  },
  {
    "path": "src/app/components/Nav/index.css",
    "chars": 1025,
    "preview": ".nav {\n  display: flex;\n  flex-direction: column;\n  min-height: 100vh;\n  border-right: 1px solid var(--primary);\n}\n\nhead"
  },
  {
    "path": "src/app/components/Nav/index.tsx",
    "chars": 2419,
    "preview": "import * as classNames from 'classnames'\nimport { observer } from 'mobx-react'\nimport * as React from 'react'\nimport Sto"
  },
  {
    "path": "src/app/components/Splash/index.css",
    "chars": 96,
    "preview": ".splash {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 100vh;\n}\n"
  },
  {
    "path": "src/app/components/Splash/index.tsx",
    "chars": 155,
    "preview": "import * as React from 'react'\nimport './index.css'\n\nfunction Splash() {\n  return <div className=\"splash\">{/* Hotel ホテル "
  },
  {
    "path": "src/app/components/Switch/index.css",
    "chars": 765,
    "preview": ".switch {\n  position: relative;\n  display: inline-block;\n  width: 30px;\n  height: 17px;\n}\n\n.switch input {\n  display: no"
  },
  {
    "path": "src/app/components/Switch/index.tsx",
    "chars": 469,
    "preview": "import * as React from 'react'\nimport './index.css'\n\nexport interface IProps {\n  onClick?: () => void\n  checked?: boolea"
  },
  {
    "path": "src/app/formatter.ts",
    "chars": 554,
    "preview": "import * as ansi2HTML from 'ansi2html'\nimport * as escapeHTML from 'escape-html'\nimport { IMonitor } from './Store'\n\nfun"
  },
  {
    "path": "src/app/global.d.ts",
    "chars": 195,
    "preview": "declare module 'ansi2html'\ndeclare module 'lodash.uniqueid'\ndeclare module 'react-icons/lib/md/arrow-downward'\ndeclare m"
  },
  {
    "path": "src/app/index.html",
    "chars": 472,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"/>\n    <title>Hote"
  },
  {
    "path": "src/app/index.tsx",
    "chars": 231,
    "preview": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport App from './components/App'\nimport Store fro"
  },
  {
    "path": "src/cli/bin.js",
    "chars": 322,
    "preview": "#!/usr/bin/env node\nconst pkg = require('../../package.json')\nrequire('please-upgrade-node')(pkg)\n\nconst updateNotifier "
  },
  {
    "path": "src/cli/daemon.js",
    "chars": 904,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst mkdirp = require('mkdirp')\nconst startup = require('user-sta"
  },
  {
    "path": "src/cli/index.js",
    "chars": 2566,
    "preview": "const yargs = require('yargs')\nconst servers = require('./servers')\nconst run = require('./run')\nconst daemon = require("
  },
  {
    "path": "src/cli/run.js",
    "chars": 1188,
    "preview": "const cp = require('child_process')\nconst getPort = require('get-port')\nconst servers = require('./servers')\nconst getCm"
  },
  {
    "path": "src/cli/servers.js",
    "chars": 3247,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst chalk = require('chalk')\nconst tildify = require('tildify')\n"
  },
  {
    "path": "src/common.js",
    "chars": 380,
    "preview": "const path = require('path')\nconst homedir = require('os').homedir()\n\nconst hotelDir = path.join(homedir, '.hotel')\n\nmod"
  },
  {
    "path": "src/conf.js",
    "chars": 625,
    "preview": "const fs = require('fs')\nconst mkdirp = require('mkdirp')\nconst { hotelDir, confFile } = require('./common')\n\n// Create "
  },
  {
    "path": "src/daemon/app.js",
    "chars": 1427,
    "preview": "const path = require('path')\nconst http = require('http')\nconst express = require('express')\nconst vhost = require('vhos"
  },
  {
    "path": "src/daemon/group.js",
    "chars": 9615,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst EventEmitter = require('events')\nconst url = require('url')\n"
  },
  {
    "path": "src/daemon/index.js",
    "chars": 958,
    "preview": "const exitHook = require('exit-hook')\nconst httpProxy = require('http-proxy')\nconst conf = require('../conf')\nconst pidF"
  },
  {
    "path": "src/daemon/loader.js",
    "chars": 1265,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst mkdirp = require('mkdirp')\nconst chokidar = require('chokida"
  },
  {
    "path": "src/daemon/log.js",
    "chars": 159,
    "preview": "const tinydate = require('tinydate')\nconst stamp = tinydate('{HH}:{mm}:{ss}')\n\nmodule.exports = function log(...args) {\n"
  },
  {
    "path": "src/daemon/pem.js",
    "chars": 971,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst tildify = require('tildify')\nconst selfsigned = require('sel"
  },
  {
    "path": "src/daemon/public/error.css",
    "chars": 505,
    "preview": "body {\n  font-family: -apple-system, BlinkMacSystemFont,\n    \"Segoe UI\", \"Roboto\", \"Oxygen\",\n    \"Ubuntu\", \"Cantarell\", "
  },
  {
    "path": "src/daemon/routers/api/events.js",
    "chars": 1192,
    "preview": "const express = require('express')\nconst connectSSE = require('connect-sse')\nconst sse = connectSSE()\n\nfunction listen(r"
  },
  {
    "path": "src/daemon/routers/api/index.js",
    "chars": 342,
    "preview": "const express = require('express')\n\nconst ServerRouter = require('./servers')\nconst EventRouter = require('./events')\n\nm"
  },
  {
    "path": "src/daemon/routers/api/servers.js",
    "chars": 432,
    "preview": "const express = require('express')\n\nmodule.exports = group => {\n  const router = express.Router()\n\n  router.get('/', (re"
  },
  {
    "path": "src/daemon/routers/index.js",
    "chars": 544,
    "preview": "const express = require('express')\nconst conf = require('../../conf')\nconst log = require('../log')\n\nmodule.exports = fu"
  },
  {
    "path": "src/daemon/tcp-proxy.js",
    "chars": 527,
    "preview": "const net = require('net')\nconst log = require('./log')\n\nmodule.exports = {\n  proxy\n}\n\nfunction proxy(source, targetPort"
  },
  {
    "path": "src/daemon/vhosts/tld.js",
    "chars": 594,
    "preview": "const express = require('express')\nconst conf = require('../../conf')\nconst log = require('../log')\n\n// *.tld vhost\nmodu"
  },
  {
    "path": "src/daemon/views/_error.pug",
    "chars": 332,
    "preview": "doctype html\nhead\n  title Error\n  meta(charset='utf-8')\n  meta(name='viewport', content='width=device-width, initial-sca"
  },
  {
    "path": "src/daemon/views/proxy-pac-with-proxy.pug",
    "chars": 731,
    "preview": ".\n  // Set conf.proxy in ~/.hotel/conf.json to your proxy address and port.\n  // For example: { \"proxy\": \"1.2.3.4:5000\" "
  },
  {
    "path": "src/daemon/views/proxy-pac.pug",
    "chars": 262,
    "preview": ".\n  // Proxy only *.#{conf.tld} requests to hotel\n  // Configuration file can be found in ~/.hotel\n  function FindProxyF"
  },
  {
    "path": "src/daemon/views/server-error.pug",
    "chars": 443,
    "preview": "extends _error.pug\n\nblock content\n  p Can't connect to server on PORT=#{server.env.PORT}\n  p: a(href=`http://localhost:$"
  },
  {
    "path": "src/daemon/views/target-error.pug",
    "chars": 129,
    "preview": "extends _error.pug\n\nblock content\n  p Cannot proxy request to \n    a(href=server.target)= server.target\n\n  pre: code=err"
  },
  {
    "path": "src/get-cmd.js",
    "chars": 196,
    "preview": "const os = require('os')\nconst unquote = require('unquote')\n\nmodule.exports = cmd =>\n  os.platform() === 'win32'\n    ? ["
  },
  {
    "path": "src/pid-file.js",
    "chars": 430,
    "preview": "const fs = require('fs')\nconst { pidFile } = require('./common')\n\nmodule.exports = {\n  create,\n  read,\n  remove\n}\n\nfunct"
  },
  {
    "path": "src/scripts/uninstall.js",
    "chars": 576,
    "preview": "const fs = require('fs')\nconst { startupFile, pidFile } = require('../common')\n\nfunction killProcess() {\n  if (!fs.exist"
  },
  {
    "path": "test/_setup.js",
    "chars": 178,
    "preview": "const os = require('os')\nconst sinon = require('sinon')\nconst tempy = require('tempy')\n\n// Required by AVA, see package."
  },
  {
    "path": "test/cli/daemon.js",
    "chars": 1153,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst u"
  },
  {
    "path": "test/cli/run.js",
    "chars": 1311,
    "preview": "const path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst cli = require('../../src/c"
  },
  {
    "path": "test/cli/servers.js",
    "chars": 3751,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst test = require('ava')\nconst sinon = require('sinon')\nconst s"
  },
  {
    "path": "test/daemon/app.js",
    "chars": 8019,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst http = require('http')\nconst test = require('ava')\nconst req"
  },
  {
    "path": "test/daemon/group.js",
    "chars": 3244,
    "preview": "const test = require('ava')\nconst sinon = require('sinon')\nconst Group = require('../../src/daemon/group')\nconst tcpProx"
  },
  {
    "path": "test/daemon/pem.js",
    "chars": 721,
    "preview": "const fs = require('fs')\nconst test = require('ava')\n// TODO rename to KEY_NAME\nconst { KEY_FILE, CERT_FILE, generate } "
  },
  {
    "path": "test/fixtures/app/index.js",
    "chars": 486,
    "preview": "var http = require('http')\n\nhttp\n  .createServer(function(req, res) {\n    console.log(req.headers)\n    res.writeHead(200"
  },
  {
    "path": "test/fixtures/verbose/index.js",
    "chars": 107,
    "preview": "setInterval(\n  () =>\n    console.log('[32m green text with blank line - ' + Math.random() + '\\n'),\n  200\n)\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 324,
    "preview": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/\",\n    \"sourceMap\": true,\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n"
  },
  {
    "path": "tslint.json",
    "chars": 339,
    "preview": "{\n    \"defaultSeverity\": \"error\",\n    \"extends\": [\n      \"tslint:recommended\",\n      \"tslint-config-prettier\"\n    ],\n   "
  },
  {
    "path": "webpack.common.js",
    "chars": 592,
    "preview": "const path = require('path')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nmodule.exports = {\n  entry: './sr"
  },
  {
    "path": "webpack.dev.js",
    "chars": 255,
    "preview": "const merge = require('webpack-merge')\nconst common = require('./webpack.common.js')\n\nmodule.exports = merge(common, {\n "
  },
  {
    "path": "webpack.prod.js",
    "chars": 384,
    "preview": "const webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst UglifyJSPlugin = require('uglifyjs-webpa"
  }
]

About this extraction

This page contains the full source code of the typicode/hotel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 76 files (86.2 KB), approximately 26.3k tokens, and a symbol index with 93 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!