- Moved the web-api to the master branch
- Rewrote parts of the web-api to support ES6
- Added local webserver to the web-api
- Added command to start the local webserver ("npm run api")
This commit is contained in:
Cronick 2016-09-27 01:39:46 +02:00
parent 0cdf54b8e9
commit 3b65a69f74
20 changed files with 924 additions and 2 deletions

View File

@ -58,7 +58,7 @@ The required database tables get generated automatically.
You need at minimum [Node.js](https://nodejs.org/en/) version 6.x.
Open up a terminal and enter ``npm run boot`` to start the server.
Open up a terminal and enter ``npm run boot`` to start the server or ``npm run api`` to start the web-api.
## Docker setup

262
api/css/main.css Normal file
View File

@ -0,0 +1,262 @@
html {
-webkit-animation: fadein 1s;
-moz-animation: fadein 1s;
-ms-animation: fadein 1s;
-o-animation: fadein 1s;
animation: fadein 1s;
}
body {
overflow-x: hidden;
overflow-y: scroll !important;
}
#map {
position: absolute;
left: calc(50% - 400px);
width: 800px !important;
height: 400px !important;
}
#map_manager {
padding-bottom: 500px;
margin-top: -100px;
display: none;
}
.view {
letter-spacing: 0px;
padding: 5px 25px !important;
margin-top: -35px;
}
.area {
font-family: "Andale Mono", AndaleMono, monospace;
text-align: center;
font-style: normal;
font-variant: normal;
-webkit-font-smoothing: antialiased;
font-size: 75px;
letter-spacing: -5px;
color: #fff;
position: relative;
margin-top: 5px;
text-transform: uppercase;
}
.Codemirror {
text-align: left;
}
.ping {
position: absolute;
top: 0px;
left: 10px;
font-size: 18px;
letter-spacing: 0px;
}
.version {
font-size: 15px;
letter-spacing: 0px;
margin-top: -75px;
margin-bottom: -25px;
text-transform: none;
}
.btn {
text-transform: uppercase;
}
.centered {
text-align: center;
position: relative;
top: 40%;
transform: translateY(-40%);
margin-top: -175px;
-webkit-transform: translateY(-40%);
-moz-transform: translateY(0%);
}
@-moz-document url-prefix() {
.centered {
margin-top: 0px;
}
}
.body {
background: #2d2d2d;
background-color: #2d2d2d;
}
.star {
position: absolute;
width: 2px;
height: 2px;
background: rgba(255, 255, 255, 0.45);
opacity: 1;
}
.btn {
font-family: "Andale Mono", AndaleMono, monospace;
display: inline-block;
padding: 5px;
border: 1px solid rgba(255, 255, 255, .35);
border-radius: 4px;
color: rgba(255, 255, 255, .75);
text-decoration: none;
transition: border .35s, background .35s;
min-width: 100px;
text-align: center;
font-size: 15px;
font-style: normal;
font-variant: normal;
font-weight: 500;
line-height: 25px;
cursor: pointer;
}
.input {
background: rgba(0,0,0,0);
cursor: auto;
}
input:focus {
outline: none;
}
button:focus {
outline: none;
}
.label {
background: rgba(255, 255, 255, .05);
border: 1px solid rgba(255, 255, 255, .5);
color: #79a2b7;
}
.info {
padding-bottom: 45px;
margin-top: -45px;
font-size: 18px;
letter-spacing: 0px;
}
.cmd_label {
border: none;
background: rgba(255,255,255,0.05);
color: white;
cursor: auto;
text-transform: none;
}
.submit {
background: rgba(165, 165, 165, 0.15);
}
.with_label {
margin-left: 25px;
width: 120px;
}
#connection_status {
text-transform: uppercase;
}
.login_area {
font-size: 25px;
letter-spacing: 0px;
text-transform: none;
margin: -25px;
padding: 0px;
}
.login_input {
text-transform: none;
background: rgba(0,0,0,0);
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.btn:hover {
background: rgba(255, 255, 255, .05);
border: 1px solid rgba(255, 255, 255, .5);
}
.txt {
margin-left: calc(20%);
margin-right: calc(20%);
left: auto;
width: auto;
min-height: 300px;
background: rgba(255, 255, 255, 0.0) !important;
border: 1px solid rgba(255, 255, 255, 0.0) !important;
border-radius: 4px !important;
padding: 10px 15px !important;
color: rgba(255, 255, 255, .75) !important;
resize: none;
transition: color .35s !important;
}
.txt:focus {
outline: none;
color: rgba(255, 255, 255, .85);
}
::-moz-selection {
color: rgba(255, 255, 255, .85);
background: rgba(255, 255, 255, .075);
}
::selection {
color: rgba(255, 255, 255, .85);
background: rgba(255, 255, 255, .075);
}
::-webkit-scrollbar{
width: 10px;
height: 0px;
background: transparent;
}
::-webkit-scrollbar-thumb{
background: rgba(255, 255, 255, .15);
border-radius: 5px;
}
::-webkit-scrollbar-corner{
background: transparent;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@-moz-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulsate {
0% {transform: scale(0.1, 0.1); opacity: 0.0;}
50% {opacity: 1.0;}
100% {transform: scale(1.2, 1.2); opacity: 0.0;}
}
@-webkit-keyframes pulsate {
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
50% {opacity: 1.0;}
100% {-webkit-transform: scale(1.2, 1.2); opacity: 0.0;}
}
@-moz-keyframes pulsate {
0% {transform: scale(0.1, 0.1); opacity: 0.0;}
50% {opacity: 1.0;}
100% {transform: scale(1.2, 1.2); opacity: 0.0;}
}

23
api/css/pure.min.css vendored Normal file

File diff suppressed because one or more lines are too long

208
api/css/vex.css Normal file
View File

@ -0,0 +1,208 @@
@keyframes vex-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-webkit-keyframes vex-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-moz-keyframes vex-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-ms-keyframes vex-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-o-keyframes vex-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes vex-fadeout {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-webkit-keyframes vex-fadeout {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-moz-keyframes vex-fadeout {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-ms-keyframes vex-fadeout {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-o-keyframes vex-fadeout {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@keyframes vex-rotation {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg); }
100% {
transform: rotate(359deg);
-webkit-transform: rotate(359deg);
-moz-transform: rotate(359deg);
-ms-transform: rotate(359deg);
-o-transform: rotate(359deg); } }
@-webkit-keyframes vex-rotation {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg); }
100% {
transform: rotate(359deg);
-webkit-transform: rotate(359deg);
-moz-transform: rotate(359deg);
-ms-transform: rotate(359deg);
-o-transform: rotate(359deg); } }
@-moz-keyframes vex-rotation {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg); }
100% {
transform: rotate(359deg);
-webkit-transform: rotate(359deg);
-moz-transform: rotate(359deg);
-ms-transform: rotate(359deg);
-o-transform: rotate(359deg); } }
@-ms-keyframes vex-rotation {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg); }
100% {
transform: rotate(359deg);
-webkit-transform: rotate(359deg);
-moz-transform: rotate(359deg);
-ms-transform: rotate(359deg);
-o-transform: rotate(359deg); } }
@-o-keyframes vex-rotation {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-o-transform: rotate(0deg); }
100% {
transform: rotate(359deg);
-webkit-transform: rotate(359deg);
-moz-transform: rotate(359deg);
-ms-transform: rotate(359deg);
-o-transform: rotate(359deg); } }
.vex, .vex *, .vex *:before, .vex *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box; }
.vex {
position: fixed;
overflow: auto;
-webkit-overflow-scrolling: touch;
z-index: 1111;
top: 0;
right: 0;
bottom: 0;
left: 0; }
.vex-scrollbar-measure {
position: absolute;
top: -9999px;
width: 50px;
height: 50px;
overflow: scroll; }
.vex-overlay {
background: #000;
filter: alpha(opacity=40);
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; }
.vex-overlay {
animation: vex-fadein 0.5s;
-webkit-animation: vex-fadein 0.5s;
-moz-animation: vex-fadein 0.5s;
-ms-animation: vex-fadein 0.5s;
-o-animation: vex-fadein 0.5s;
-webkit-backface-visibility: hidden;
position: fixed;
background: rgba(0, 0, 0, 0.4);
top: 0;
right: 0;
bottom: 0;
left: 0; }
.vex-close:before {
font-family: Arial, sans-serif;
content: "\00D7"; }
.vex-dialog-form {
margin: 0; }
.vex-dialog-button {
text-rendering: optimizeLegibility;
-moz-appearance: none;
-webkit-appearance: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent; }
.vex-loading-spinner {
animation: vex-rotation 0.7s linear infinite;
-webkit-animation: vex-rotation 0.7s linear infinite;
-moz-animation: vex-rotation 0.7s linear infinite;
-ms-animation: vex-rotation 0.7s linear infinite;
-o-animation: vex-rotation 0.7s linear infinite;
-webkit-backface-visibility: hidden;
-moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.1);
-webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.1);
box-shadow: 0 0 1em rgba(0, 0, 0, 0.1);
position: fixed;
z-index: 1112;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 2em;
width: 2em;
background: #fff; }
body.vex-open {
overflow: hidden; }

BIN
api/img/gym_NEUTRAL.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
api/img/gym_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
api/img/gym_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
api/img/gym_yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

1
api/img/license.txt Normal file
View File

@ -0,0 +1 @@
https://shareicon.net/license/cc-3-0-by

BIN
api/img/pokestop_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
api/img/pokestop_lure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
api/img/pokestop_puple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
api/img/spawn_point.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

51
api/index.html Normal file
View File

@ -0,0 +1,51 @@
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Language" content="en">
<meta name="viewport" content="width=device-width">
<meta content="origin-when-cross-origin" name="referrer" />
<title>POGOserver api</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/api//css/main.css">
<link rel="stylesheet" type="text/css" href="/api/css/pure.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vex-js/2.3.4/js/vex.combined.min.js"></script>
<script>vex.defaultOptions.className = "vex-theme-plain";</script>
<link rel="stylesheet" type="text/css" href="/api/css/vex.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/vex-js/2.3.4/css/vex-theme-plain.css">
</head>
<body class="body">
<p class="ping area" id="server_ping" style="position: absolute;top: 0px;left: 10px; display:none;">Ping: 0ms</p>
<div class="area">
<p>POGOserver</p>
<p id="version" class="version"> </p>
<p><button class="btn color-1 view login_input" id="connection_status" href="#" style="color:yellow;">Connecting..</button></p>
</div>
<div id="login_area" class="area">
<p class="login_area">Login</p>
<p><input class="btn color-1 view input login_input" id="login_username" style="margin:-20px;" autocomplete="new-password" value="root"></input></p>
<p><input class="btn color-1 view input login_input" id="login_password" type="password" autocomplete="new-password"></input></p>
<p><button class="btn color-1 view login_input" id="login_attempt">Login</button></p>
</div>
<div id="world_manager" class="area" style="display:none;">
<p class="info" id="server_version">Server version: v0.1.0</p>
<p class="info" id="connected_players">Connected players: 0</p>
<p class="login_area" style="padding-bottom: 15px;">World Manager</p>
<p><button class="btn color-1 view cmd_label label">Spawn</button><input class="btn color-1 view input login_input with_label" id="spawn_user" placeholder="Username" autocomplete="new-password"></input><input class="btn color-1 view input login_input with_label" autocomplete="new-password" id="spawn_pkmn" placeholder="Pokemon"></input><button id="submit_spawn" class="btn color-1 view login_input with_label submit">Submit</button></p>
<br/>
</div>
<div class="area" id="map_manager">
<div id="map"></div>
</div>
<script src="/api/cfg.js"></script>
<script src="/api/js/init.js"></script>
</body>
</html>

36
api/index.js Normal file
View File

@ -0,0 +1,36 @@
import http from "http";
import url from "url";
import path from "path";
import fs from "fs";
const port = process.argv[2] || 9000;
http.createServer((request, response) => {
const uri = url.parse(request.url).pathname;
let filename = path.join(process.cwd(), uri);
fs.exists(filename, exists => {
if(!exists) {
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not Found\n");
response.end();
return;
}
if (fs.statSync(filename).isDirectory()) filename += '/api/index.html';
fs.readFile(filename, "binary", (err, file) => {
if(err) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(`${err}\n`);
response.end();
return;
}
response.writeHead(200);
response.write(file, "binary");
response.end();
});
});
}).listen(parseInt(port, 10));
console.log(`Web-API for POGOserver running at => http://localhost:${port}/\nCTRL + C to shutdown`);

22
api/js/ajax.js Normal file
View File

@ -0,0 +1,22 @@
function send(data, resolve) {
const xhr = new XMLHttpRequest();
const protocol = window.location.protocol;
xhr.open("POST", `${protocol}//${CFG.API.HOST}:${CFG.API.PORT}${CFG.API.ROUTE}`, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
if (typeof resolve === "function") {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(void 0);
}
}
} else {
resolve(xhr.statusText);
}
}
};
xhr.send(JSON.stringify(data));
}

2
api/js/gmaps.js Normal file

File diff suppressed because one or more lines are too long

17
api/js/init.js Normal file
View File

@ -0,0 +1,17 @@
((() => {
function loadScriptDefered(src) {
let js = null;
js = document.createElement("script");
js.type = "text/javascript";
js.src = src;
js.async = false;
document.body.appendChild(js);
};
loadScriptDefered(`http://maps.google.com/maps/api/js?key=${CFG.GMAPS.API_KEY}`);
loadScriptDefered("/api/js/gmaps.js");
loadScriptDefered("/api/js/ajax.js");
loadScriptDefered("/api/js/main.js");
}))();

299
api/js/main.js Normal file
View File

@ -0,0 +1,299 @@
((() => {
let loggedIn = false;
let loginTimeout = null;
let heartInterval = null;
let heartTimeout = null;
let heartTimedOut = true;
const header = `
<div class="pure-form pure-g">
<div class="pure-u-1-3">
<center>
<img src='/api/img/spawn_point.png'/><br/>
<input id="option_one" type="radio" name="type" style="margin: 18px;" value="SPAWN">
</center>
</div>
<div class="pure-u-1-3">
<center>
<img src='/api/img/pokestop_blue.png'/><br/>
<input id="option_two" type="radio" name="type" style="margin: 18px;" value="CHECKPOINT" >
</center>
</div>
<div class="pure-u-1-3">
<center>
<img src='/api/img/gym_NEUTRAL.png'/><br/>
<input id="option_three" type="radio" name="type" style="margin: 18px;" value="GYM">
</center>
</div>
</div>
<div id="form_checkpoint" style="display:none;">
<input name="name" placeholder="Name" type="text" />
<input name="description" placeholder="Description" type="text" />
<input name="image_url" placeholder="Image" type="text" />
<input name="experience" placeholder="Experience" type="text" />
</div>
<div id="form_spawn" style="display:none;">
<input name="interval" placeholder="Interval" type="text" />
<input name="encounters" placeholder="Encounters" type="text" />
</div>
<div id="form_gym" style="display:none;">
<input name="team" placeholder="Team" type="text" />
</div>
<script>
function hideAllForms() {
form_checkpoint.style.display = "none";
form_spawn.style.display = "none";
form_gym.style.display = "none";
}
function showForm(e) {
var target = e.target || e;
hideAllForms();
var key = "#form_" + target.value.toLowerCase();
var el = document.querySelector(key);
el.style.display = "block";
}
option_one.onclick = showForm;
option_two.onclick = showForm;
option_three.onclick = showForm;
option_one.checked = true;
showForm(option_one);
</script>
`;
const gmap = new GMaps({
el: "#map",
disableDoubleClickZoom: true,
lat: 0,
lng: 0,
disableDefaultUI: true,
dblclick(e) {
vex.dialog.open({
message: "",
input: header,
buttons: [
$.extend({}, vex.dialog.buttons.YES, {
text: "Submit"
}),
$.extend({}, vex.dialog.buttons.NO, {
text: "Abort"
})
],
callback(data) {
if (data !== false && Object.keys(data).length) {
const ed = e.data = {};
if (data.type === "SPAWN") {
ed.interval = data.interval;
ed.encounters = data.encounters;
}
else if (data.type === "CHECKPOINT") {
ed.name = data.name;
ed.description = data.description;
ed.imageUrl = data.image_url;
ed.experience = data.experience;
}
else if (data.type === "GYM") {
ed.team = data.team;
}
e.type = data.type;
addFort(e, ed);
}
}
})
}
});
function addFort(e, data) {
let lat = e.latLng.lat();
let lng = e.latLng.lng();
let obj = {
action: "addFortToPosition",
latitude: lat,
longitude: lng,
zoom: gmap.zoom,
type: e.type
};
Object.assign(obj, data);
send(obj, res => {
console.log(res);
refreshMapForts();
});
}
function setStatus(txt, color) {
connection_status.innerHTML = txt;
connection_status.style.color = color;
}
setStatus("Connecting", "yellow");
send({
action: "init"
}, res => {
if (res.success) {
setStatus("Connected!", "green");
}
else {
if (res.reason !== void 0) {
setStatus(res.reason);
} else {
setStatus("Connection failed!", "red");
}
return void 0;
}
});
login_attempt.addEventListener("click", login);
submit_spawn.addEventListener("click", () => {
send({
action: "spawnPkmnToPlayer",
player: spawn_user.value,
pkmn: spawn_pkmn.value
}, res => {
console.log(res);
});
});
function login() {
const username = login_username.value;
const password = login_password.value;
send({
action: "login",
username,
password
}, res => {
if (res.success) {
afterLogin();
}
else {
setStatus("Login failed!", "red");
clearTimeout(loginTimeout);
loginTimeout = setTimeout(() => {
if (loggedIn) {
setStatus("Connected!", "green");
}
}, 3e3);
}
});
}
function afterLogin() {
loggedIn = true;
login_area.style.display = "none";
setStatus("Logged in!", "green");
world_manager.style.display = "block";
server_ping.style.display = "block";
map_manager.style.display = "block";
gmap.refresh();
gmap.setCenter({
lat: CFG.GMAPS.BASE_LAT,
lng: CFG.GMAPS.BASE_LNG
});
gmap.setZoom(CFG.GMAPS.BASE_ZOOM);
initHeartBeat();
refreshMapForts();
refreshConnectedPlayers();
getServerVersion();
}
function refreshConnectedPlayers() {
send({
action: "getConnectedPlayers"
}, res => {
connected_players.innerHTML = `Connected players: ${res.connected_players}`;
});
}
function getServerVersion() {
send({
action: "getServerVersion"
}, res => {
server_version.innerHTML = `Server version: v${res.version}`;
});
}
function getFortIcon(fort) {
if (fort.type === "CHECKPOINT") return ("/api/img/pokestop_blue.png");
else if (fort.uid[fort.uid.length - 1] === "S") return ("/api/img/spawn_point.png");
else return (`/api/img/gym_${fort.owned_by_team}.png`);
}
function refreshMapForts() {
let center = gmap.getCenter();
let lat = center.lat();
let lng = center.lng();
send({
action: "getFortsByPosition",
latitude: lat,
longitude: lng,
zoom: gmap.zoom
}, result => {
gmap.removeMarkers();
result.forts.map((fort) => {
let icon = getFortIcon(fort);
gmap.addMarker({
lat: fort.latitude,
lng: fort.longitude,
title: fort.name,
icon,
rightclick: function(e) {
vex.dialog.confirm({
message: `<center><img src='${getFortIcon(this)}' /><br/>Delete this fort?</center>`,
callback: function(value) {
if (value) removeFort(this);
}.bind(fort)
})
}.bind(fort)
});
});
});
}
function removeFort(fort) {
send({
action: "deleteFortById",
uid: fort.uid,
latitude: fort.latitude,
longitude: fort.longitude,
zoom: gmap.zoom
}, res => {
console.log(res);
refreshMapForts();
});
}
function initHeartBeat() {
clearInterval(heartInterval);
heartInterval = setInterval(() => {
heartTimedOut = true;
const now = +new Date();
heartTimeout = setTimeout(() => {
if (heartTimedOut) {
console.error("Heartbeat timeout!");
loggedIn = false;
setStatus("Reconnecting..", "yellow");
login();
}
}, 5e3);
send({
action: "heartBeat",
timestamp: now
}, res => {
if (res.timestamp) {
heartTimedOut = false;
clearTimeout(heartTimeout);
const ping = res.timestamp - now;
server_ping.innerHTML = `Ping: ${ping}ms`;
refreshConnectedPlayers();
refreshMapForts();
}
});
}, 3e3);
}
}))();

View File

@ -9,7 +9,8 @@
"scripts": {
"test": "echo \"Error: no test specified\"",
"babel-node": "babel-node --presets=es2015",
"boot": "npm run babel-node -- ./src/index.js"
"boot": "npm run babel-node -- ./src/index.js",
"api": "npm run babel-node -- ./api/index.js"
},
"engines": {
"node": ">= 6.x",