diff --git a/assets/pledge-test.png b/assets/pledge-test.png new file mode 100644 index 0000000..455b06d Binary files /dev/null and b/assets/pledge-test.png differ diff --git a/assets/pledge.png b/assets/pledge.png new file mode 100644 index 0000000..bee6b07 Binary files /dev/null and b/assets/pledge.png differ diff --git a/config-ci.json b/config-ci.json index 133d97c..bb86ea8 100644 --- a/config-ci.json +++ b/config-ci.json @@ -1,9 +1,10 @@ { "db": "db-test", - "domain": "pledge.our.buildo.io", + "domain": "ci.pledge.com.fake", "interval": 1, "slack": { - "incomingWebhookURL": "https://hooks.slack.com/services/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" + "clientId": "123456789.123456789", + "clientSecret": "abcdef123456789" } } diff --git a/config-prod.json b/config-prod.json index 5613bc4..7b2504d 100644 --- a/config-prod.json +++ b/config-prod.json @@ -1,9 +1,10 @@ { "db": "/var/lib/pledge/db", - "domain": "pledge.our.buildo.io", + "domain": "https://pledge.our.buildo.io", "interval": 1, "slack": { - "incomingWebhookURL": "https://hooks.slack.com/services/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" + "clientId": "2194261799.137771655351", + "clientSecret": "37ca4f24926ff3b36f39f6f2f77faaff" } } diff --git a/config.json.example b/config.json.example index 379e9eb..d00e2ea 100644 --- a/config.json.example +++ b/config.json.example @@ -3,7 +3,8 @@ "domain": "my.deploy.url", "interval": 1, "slack": { - "incomingWebhookURL": "https://url/of/incoming/webhook" + "clientId": "123456789.123456789", + "clientSecret": "abcdef123456789" } } diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..592252b --- /dev/null +++ b/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["tests/*"] +} diff --git a/package.json b/package.json index 03adb36..2a0b611 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build": "npm run clean && babel src --out-dir lib && cp config.json lib || true", "watch": "npm run clean && cp config.json lib && babel src --out-dir lib --watch", "start": "DEBUG=pledge,http,express:* node lib/index.js", + "restart": "DEBUG=pledge,http,express:* nodemon --config nodemon.json lib/index.js", "test": "jest" }, "repository": { @@ -27,6 +28,7 @@ }, "homepage": "https://github.com/buildo/pledge#readme", "dependencies": { + "@slack/client": "^3.8.1", "babel-core": "^6.4.5", "babel-polyfill": "^6.4.5", "babel-runtime": "^6.9.2", @@ -39,6 +41,7 @@ "mockdate": "^2.0.1", "request": "^2.75.0", "request-promise": "^2.0.1", + "simple-oauth2": "^1.0.3", "sqlite": "^2.0.2", "supertest": "^2.0.1" }, @@ -54,6 +57,7 @@ "babel-preset-stage-0": "^6.5.0", "eslint": "^2.4.0", "eslint-config-buildo": "github:buildo/eslint-config-buildo", - "jest": "^18.1.0" + "jest": "^18.1.0", + "nodemon": "^1.11.0" } } diff --git a/src/db.js b/src/db.js index a6d874f..10c8fa1 100644 --- a/src/db.js +++ b/src/db.js @@ -2,14 +2,14 @@ import db from 'sqlite'; import config from '../config.json'; import { formatDate } from './utils'; -const schemaVersion = 3; - const createTables = async () => { await db.run(` CREATE TABLE pledges ( id INTEGER PRIMARY KEY AUTOINCREMENT, + teamId TEXT NOT NULL, requester TEXT NOT NULL, performer TEXT NOT NULL, + performerId TEXT NOT NULL, content TEXT NOT NULL, deadline INTEGER NOT NULL, completed BOOLEAN NOT NULL DEFAULT 0, @@ -18,95 +18,88 @@ const createTables = async () => { ); `); await db.run(` - CREATE TABLE schemaVersions ( - version INTEGER NOT NULL, - migrationDate INTEGER NOT NULL + CREATE TABLE teams ( + teamName TEXT NOT NULL, + teamId TEXT NOT NULL, + botUserId TEXT NOT NULL, + botAccessToken TEXT NOT NULL, + createdAt INTEGER NOT NULL ); `); - await db.run('INSERT INTO schemaVersions values (?, ?)', schemaVersion, Date.now()); }; -const migrateIfNeeded = async () => { - const hasSchemaVersionsTable = !!(await db.get(` - SELECT 1 FROM sqlite_master WHERE name ='schemaVersions' and type='table'; - `)); - const currentVersion = hasSchemaVersionsTable ? - (await db.get('SELECT max(version) as v FROM schemaVersions')).v : 1; - console.log(`starting pledge, current DB version is ${currentVersion}`); // eslint-disable-line no-console - if (currentVersion < 2) { - // version 1 did not have a schemaVersions table, let's create it - await db.run(` - CREATE TABLE if not exists schemaVersions ( - version INTEGER NOT NULL, - migrationDate INTEGER NOT NULL - );` - ); - // add first version line with migrationDate = now - await db.run(` - INSERT INTO schemaVersions values (2, ?) - `, Date.now() - ); - // perform migration v1 => v2 - await db.run(` - ALTER TABLE pledges - ADD COLUMN expiredNotificationSent BOOLEAN NOT NULL DEFAULT 0` - ); - console.log('migrated DB to version 2'); // eslint-disable-line no-console - } +export const getTeam = (teamId) => { + return db.get(` + SELECT teamId, teamName, botUserId, botAccessToken, createdAt + FROM teams + WHERE teamId = ? + `, teamId + ); +}; - if (currentVersion < 3) { - // add version line with migrationDate = now - await db.run(` - INSERT INTO schemaVersions values (3, ?) - `, Date.now() - ); - // perform migration v2 => v3 - await db.run(` - ALTER TABLE pledges - ADD COLUMN completed BOOLEAN NOT NULL DEFAULT 0` - ); - console.log('migrated DB to version 3'); // eslint-disable-line no-console - } +export const deleteTeam = (teamId) => { + return db.run(` + DELETE FROM teams + WHERE teamId = ? + `, teamId + ); +}; + +export const insertTeam = (teamId, teamName, botUserId, botAccessToken) => { + return db.run(` + INSERT INTO teams (teamId, teamName, botUserId, botAccessToken, createdAt) + VALUES (?, ?, ?, ?, ?) + `, teamId, teamName, botUserId, botAccessToken, Date.now() + ); }; -export const getList = async requester => { +export const getList = async (requester, teamId) => { const requests = (await db.all(` - SELECT id, requester, performer, content, deadline + SELECT id, teamId, requester, performer, content, deadline FROM pledges - WHERE requester = ? AND completed = 0 - `, requester)).map(x => ({ ...x, deadline: formatDate(new Date(x.deadline)) })); + WHERE requester = ? AND teamId = ? AND completed = 0 + `, requester, teamId)).map(x => ({ ...x, deadline: formatDate(new Date(x.deadline)) })); const pledges = (await db.all(` - SELECT id, requester, performer, content, deadline + SELECT id, teamId, requester, performer, content, deadline FROM pledges - WHERE performer = ? AND completed = 0 - `, requester)).map(x => ({ ...x, deadline: formatDate(new Date(x.deadline)) })); + WHERE performer = ? AND teamId = ? AND completed = 0 + `, requester, teamId)).map(x => ({ ...x, deadline: formatDate(new Date(x.deadline)) })); return { requests, pledges }; }; export const getPledge = (pledgeId) => { return db.get(` - SELECT id, requester, performer, content, deadline + SELECT id, teamId, requester, performer, performerId, content, deadline FROM pledges WHERE id = ? `, pledgeId ); }; -export const insertPledge = ({ requester, performer, content, deadline }) => { +export const insertPledge = ({ teamId, requester, performer, performerId, content, deadline }) => { return db.run(` - INSERT INTO pledges (requester, performer, content, deadline, created_at) - VALUES (?, ?, ?, ?, ?) - `, requester, performer, content, deadline, Date.now() + INSERT INTO pledges (teamId, requester, performer, performerId, content, deadline, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, teamId, requester, performer, performerId, content, deadline, Date.now() + ); +}; + +export const getTeamByBotAccessToken = (botAccessToken) => { + return db.get(` + SELECT teamId, teamName, botUserId, botAccessToken, createdAt + FROM teams + WHERE botAccessToken = ? + `, botAccessToken ); }; export const findAllPledgesExpiredToNotify = () => { return db.all(` - SELECT id, requester, performer, content, deadline + SELECT id, teamId, requester, performer, performerId, content, deadline FROM pledges - WHERE deadline < ? AND expiredNotificationSent = 0 + WHERE deadline < ? AND expiredNotificationSent = 0 AND completed = 0 `, Date.now() ); }; @@ -146,11 +139,8 @@ export const init = async (dbFilename = config.db) => { const hasPledgesTable = !!(await db.get(` SELECT 1 FROM sqlite_master WHERE name ='pledges' and type='table'; `)); - if (hasPledgesTable) { - await migrateIfNeeded(); - } else { - console.log('creating tables'); // eslint-disable-line no-console - createTables(); + if (!hasPledgesTable) { + await createTables(); } }; diff --git a/src/routes.js b/src/routes.js index f2a5631..158f39f 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,4 +1,5 @@ import express from 'express'; +import config from '../config.json'; import bodyParser from 'body-parser'; import _debug from 'debug'; import human2date from 'date.js'; @@ -8,9 +9,23 @@ import * as slack from './slack'; const debug = _debug('pledge'); -async function newPledge({ text, requester }) { - const [, performer, content, humanReadableDeadline] = /(@[a-zA-Z0-9]+) (.+) by (.+)/.exec(text.trim()) || []; +const credentials = { + client: { + id: config.slack.clientId, + secret: config.slack.clientSecret + }, + auth: { + tokenHost: 'https://slack.com', + tokenPath: '/api/oauth.access', + authorizePath: '/oauth/authorize' + } +}; + +const oauth2 = require('simple-oauth2').create(credentials); +async function newPledge({ text, requester }, botAccessToken) { + const [, , performerId, _performer, content, humanReadableDeadline] = /(\<@([a-zA-Z0-9]+)\|([a-zA-Z0-9]+)\>) (.+) by (.+)/.exec(text.trim()) || []; + const performer = `@${_performer}`; if (!performer) { throw new Error('"Username" is missing. (@username [what] by [when])'); } else if (!content) { @@ -24,21 +39,23 @@ async function newPledge({ text, requester }) { if (deadline.getTime() < Date.now()) { throw new Error('"When" should be in the future'); } - - await db.insertPledge({ requester, performer, content, deadline }).then(debug); + const teamId = (await db.getTeamByBotAccessToken(botAccessToken)).teamId; + await db.insertPledge({ teamId, requester, performer, performerId, content, deadline }).then(debug); await slack.postOnSlack({ text: `${requester} asked you to "${content}" by ${humanReadableDeadline} (${formatDate(deadline)})`, - channel: performer - }); + channel: performerId + + }, botAccessToken); return `You asked ${performer} to "${content}" by ${humanReadableDeadline} (${formatDate(deadline)})`; } -async function getPledgesList(requester) { - const { requests, pledges } = await db.getList(requester); +async function getPledgesList(requester, botAccessToken) { + const teamId = (await db.getTeamByBotAccessToken(botAccessToken)).teamId; + const { requests, pledges } = await db.getList(requester, teamId); - const baseURL = 'https://pledge.our.buildo.io'; + const baseURL = config.domain; const myPledges = `*My pledges:*\n${pledges.map(p => `\n • ${p.content} _for ${p.requester}_ *by ${p.deadline}* <${baseURL}/deletePledge/${p.id}|delete> <${baseURL}/completePledge/${p.id}|complete>`)}`; const myRequests = `*My requests:*\n${requests.map(p => `\n • _${p.performer}_ pledged to ${p.content} *by ${p.deadline}* <${baseURL}/deletePledge/${p.id}|delete> <${baseURL}/completePledge/${p.id}|complete>`)}`; @@ -49,12 +66,13 @@ async function getPledgesList(requester) { export async function findNewNotifications() { // notify for pledges that have expired const expiredPledges = await db.findAllPledgesExpiredToNotify(); - expiredPledges.map(async p => { + await Promise.all(expiredPledges.map(async p => { + const botAccessToken = (await db.getTeam(p.teamId)).botAccessToken; await slack.postOnSlackMultipleChannels({ text: `pledge ${p.content} expired just now` - }, [p.requester, p.performer]); + }, [p.requester, p.performer], botAccessToken); await db.setExpiredNotificationAsSentOnPledge(p.id); - }); + })); } setInterval(findNewNotifications, 60 * 1000); @@ -65,24 +83,43 @@ const router = express.Router(); router.use(bodyParser.urlencoded({ extended: false })); -router.post('/slackCommand', async ({ body: { text, user_name } }, res) => { +router.get('/callback', async (req, res) => { + const code = req.query.code; + const options = { code }; + + oauth2.authorizationCode.getToken(options, async (error, result) => { + if (error) { + console.error('Access Token Error', error.message); // eslint-disable-line no-console + return res.json('Authentication failed'); + } + await db.deleteTeam(result.team_id); + await db.insertTeam(result.team_id, result.team_name, result.bot.bot_user_id, + result.bot.bot_access_token); + return res + .status(200) + .send('You have successfully installed pledge ;-)'); + }); +}); + +router.post('/slackCommand', async ({ body: { text, user_name, team_id } }, res) => { debug({ text, user_name }); const requester = `@${user_name}`; if (typeof text === 'undefined' || typeof user_name === 'undefined') { - res.status(422).send('command does not respect Slack POST format'); + return res.status(422).send('command does not respect Slack POST format'); } + const team = await db.getTeam(team_id); try { switch (text.trim()) { case 'list': - return res.send(await getPledgesList(requester)); + return res.send(await getPledgesList(requester, team.botAccessToken)); default: - return res.send(await newPledge({ text, requester })); + return res.send(await newPledge({ text, requester }, team.botAccessToken)); } } catch (err) { debug(err); - res.send(`Error: ${err.message}`); + return res.send(`Error: ${err.message}`); } }); @@ -92,17 +129,12 @@ router.get('/deletePledge/:pledgeId', async ({ params: { pledgeId } }, res) => { await db.deletePledge(pledgeId); // notify on slack const notificationMessage = `pledge "${content}" has been deleted`; - await slack.postOnSlack({ - text: notificationMessage, - channel: performer - }); - await slack.postOnSlack({ - text: notificationMessage, - channel: requester - }); - res.send(`Successfully deleted pledge #${pledgeId}`); + await slack.postOnSlackMultipleChannels({ + text: notificationMessage + }, [requester, performer]); + return res.send(`Successfully deleted pledge #${pledgeId}`); } catch (e) { - res.send(`Error: ${e.message}`); + return res.send(`Error: ${e.message}`); } }); @@ -112,18 +144,31 @@ router.get('/completePledge/:pledgeId', async ({ params: { pledgeId } }, res) => await db.completePledge(pledgeId); // notify on slack const notificationMessage = `pledge "${content}" has been completed !!!`; - await slack.postOnSlack({ - text: notificationMessage, - channel: performer - }); - await slack.postOnSlack({ - text: notificationMessage, - channel: requester - }); - res.send(`Successfully completed pledge #${pledgeId}`); + await slack.postOnSlackMultipleChannels({ + text: notificationMessage + }, [requester, performer]); + return res.send(`Successfully completed pledge #${pledgeId}`); } catch (e) { - res.send(`Error: ${e.message}`); + return res.send(`Error: ${e.message}`); } }); +router.get('/add-to-slack', async (req, res) => { + res.setHeader('Content-Type', 'text/html'); + const html = ` + +
+
+ Install pledge in your Slack team +
+ + Add to Slack + +
+ `; + return res.send(html); +}); + export default router; diff --git a/src/slack.js b/src/slack.js index 237a151..f908cdb 100644 --- a/src/slack.js +++ b/src/slack.js @@ -1,13 +1,21 @@ -import request from 'request-promise'; -import config from '../config.json'; +const WebClient = require('@slack/client').WebClient; -const INCOMING_WEBHOOK_URL = config.slack.incomingWebhookURL; - -export const postOnSlack = json => request({ - json: { username: 'pledge', icon_emoji: ':dog:', ...json }, - url: INCOMING_WEBHOOK_URL, - method: 'POST' -}); +export const postOnSlack = async (json, botAccessToken) => { + const web = new WebClient(botAccessToken); + web.im.list((err, result) => { + if (err) { + return console.log(err); + } else { + const imChannel = result.ims.find((im) => im.user === json.channel).id; + return web.chat.postMessage(imChannel, json.text, json.opts, (err) => { + if (err) { + console.log('Error:', err); + } + }); + } + }); +}; export const postOnSlackMultipleChannels = - (json, channels) => Promise.all(channels.map(c => postOnSlack({ channel: c, ...json }))); + (json, channels, botAccessToken) => Promise.all(channels.map(c => + postOnSlack({ channel: c, ...json }, botAccessToken))); diff --git a/tests/index.test.js b/tests/index.test.js index de1f72a..e3b2211 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -10,12 +10,31 @@ import * as slack from '../src/slack'; slack.postOnSlack.mockImplementation(() => Promise.resolve()); slack.postOnSlackMultipleChannels.mockImplementation(() => Promise.resolve()); +const getList = () => { + return request(app).post('/slackCommand') + .send('user_name=requester') + .send('text=list') + .send('team_id=TEAM_ID') + .expect(200); +}; + +const createPledge = (by) => { + return request(app).post('/slackCommand') + .send('user_name=requester') + .send(`text=<@U123456789|performer> content by ${by}`) + .send('team_id=TEAM_ID') + .expect(200); +}; + describe('app', () => { describe('slackCommand', () => { beforeEach(async () => { + slack.postOnSlackMultipleChannels.mockClear(); + slack.postOnSlack.mockClear(); await db.init(`db-${Math.random().toString(36).substr(2, 20)}`); + await db.insertTeam('TEAM_ID', 'TEAM_NAME', 'BOT_USER_ID', 'BOT_ACCESS_TOKEN'); }); afterEach(async () => { @@ -35,65 +54,180 @@ describe('app', () => { }); }); - it('shows an empty list of pledges with the list command', () => { - return request(app).post('/slackCommand') - .send('user_name=luca') - .send('text=list') - .expect(200) - .then((res) => { + it('list: shows an empty list of pledges with the list command', () => { + return getList().then((res) => { + expect(typeof res.text).toBe('string'); + // headers for lists are presents + expect(res.text).toMatch('My pledges'); + expect(res.text).toMatch('My requests'); + // no pledge is present (empty lists) + expect(res.text).not.toMatch('pledged to'); + }); + }); + + it('create: notifies both immediately, appears in list', () => { + MockDate.set(0); + return createPledge('tomorrow at 10am').then((res) => { + expect(typeof res.text).toBe('string'); + // notification for requester sent as response to slack command + expect(res.text).toMatch('You asked @performer to \"content\" by tomorrow at 10am'); + // notification for performer + expect(slack.postOnSlack.mock.calls[0][0].text) + .toMatch('@requester asked you to \"content\" by tomorrow at 10am'); + expect(slack.postOnSlack.mock.calls[0][0].channel) + .toMatch('U123456789'); + return getList().then((res) => { expect(typeof res.text).toBe('string'); - // headers for lists are presents - expect(res.text).toMatch('My pledges'); - expect(res.text).toMatch('My requests'); - // no pledge is present (empty lists) - expect(res.text).not.toMatch('pledged to'); + // pledge is present + expect(res.text).toMatch('_@performer_ pledged to content *by 2 January at 10:00*'); }); + }); }); - it('notifies both requester and performer when a pledge is created', () => { - return request(app).post('/slackCommand') - .send('user_name=requester') - .send('text=@performer pledge content by tomorrow') - .expect(200) - .then((res) => { - expect(typeof res.text).toBe('string'); - // notification for requester sent as response to slack command - expect(res.text).toMatch('You asked @performer to \"pledge content\" by tomorrow'); - // notification for performer - expect(slack.postOnSlack.mock.calls[0][0].text) - .toMatch('@requester asked you to \"pledge content\" by tomorrow'); - expect(slack.postOnSlack.mock.calls[0][0].channel) - .toMatch('@performer'); + it('expired: notifies both at the right time, stays in list', () => { + const timezoneOffset = new Date().getTimezoneOffset(); + const interval = 10 * 60 * 60 * 1000; // 10 hours + const adjInterval = interval + timezoneOffset * 60 * 1000; + MockDate.set(0); + return createPledge('today at 10am').then(async () => { + // number of notifications sent to both performer and requester + const nExp = () => + slack.postOnSlackMultipleChannels.mock.calls.filter((c) => { + return c[0].text.match(/expired/) && c[1].includes('@performer') + && c[1].includes('@requester') && c[1].length === 2; + }).length; + // just before, no notifications + MockDate.set(adjInterval - 1); + await findNewNotifications(); + expect(nExp()).toBe(0); + // right after, it notifies + MockDate.set(adjInterval + 1); + await findNewNotifications(); + expect(nExp()).toBe(1); + // after some time, does not notify twice + MockDate.set(adjInterval + 999); + await findNewNotifications(); + expect(nExp()).toBe(1); + // check that it's still present in list + return getList().then((res) => { + // pledge is present + expect(res.text).toMatch('_@performer_ pledged to content *by 1 January at 10:00*'); }); + }); }); - it('notifies both requester and performer when a pledge has expired', () => { + it('expired: does not notify for completed pledges', () => { const timezoneOffset = new Date().getTimezoneOffset(); const interval = 10 * 60 * 60 * 1000; // 10 hours const adjInterval = interval + timezoneOffset * 60 * 1000; MockDate.set(0); - return request(app).post('/slackCommand') - .send('user_name=requester') - .send('text=@performer pledge content by today at 10am') - .expect(200) - .then(async () => { + return createPledge('today at 10am').then(async () => { + return request(app).get('/completePledge/1').expect(200).then(async () => { const nExp = () => - slack.postOnSlackMultipleChannels.mock.calls.filter((c) => c[0].text.match(/expired/)).length; - // just before, no notifications - MockDate.set(adjInterval - 1); + slack.postOnSlackMultipleChannels.mock.calls.filter((c) => { + return c[0].text.match(/expired/) && c[1].includes('@performer') + && c[1].includes('@requester') && c[1].length === 2; + }).length; + // after some time, no notification ever arrived + MockDate.set(adjInterval + 999); await findNewNotifications(); expect(nExp()).toBe(0); - // right after, it notifies - MockDate.set(adjInterval + 1); - await findNewNotifications(); - expect(nExp()).toBe(1); - // after some time, does not notify twice + }); + }); + }); + + it('expired: does not notify for deleted pledges', () => { + const timezoneOffset = new Date().getTimezoneOffset(); + const interval = 10 * 60 * 60 * 1000; // 10 hours + const adjInterval = interval + timezoneOffset * 60 * 1000; + MockDate.set(0); + return createPledge('today at 10am').then(async () => { + return request(app).get('/deletePledge/1').expect(200).then(async () => { + const nExp = () => + slack.postOnSlackMultipleChannels.mock.calls.filter((c) => { + return c[0].text.match(/expired/) && c[1].includes('@performer') + && c[1].includes('@requester') && c[1].length === 2; + }).length; + // after some time, no notification ever arrived MockDate.set(adjInterval + 999); await findNewNotifications(); - expect(nExp()).toBe(1); + expect(nExp()).toBe(0); }); + }); }); - }); + it('delete: notifies both immediately, disappears from list', () => { + MockDate.set(0); + // create a pledge + return createPledge('tomorrow at 10am').then(() => { + return getList().then((res) => { + // pledge is present in list + expect(res.text).toMatch('_@performer_ pledged to content *by 2 January at 10:00*'); + // delete the pledge + return request(app).get('/deletePledge/1').expect(200).then((res) => { + expect(res.text).toEqual('Successfully deleted pledge #1'); + // notifications are sent + expect(slack.postOnSlackMultipleChannels.mock.calls + .filter((x) => x[0].text.match('has been deleted'))[0]).toEqual([ + { text: 'pledge "content" has been deleted' }, + ['@requester', '@performer'] + ]); + return getList().then((res) => { + // pledge is not in list any more + expect(res.text).not.toMatch('_@performer_ pledged to content *by 2 January at 10:00*'); + }); + }); + }); + }); + }); + it('complete: notifies both immediately, disappears from list', () => { + MockDate.set(0); + // create a pledge + return createPledge('tomorrow at 10am').then(() => { + return getList().then((res) => { + // pledge is present in list + expect(res.text).toMatch('_@performer_ pledged to content *by 2 January at 10:00*'); + // delete the pledge + return request(app).get('/completePledge/1').expect(200).then((res) => { + expect(res.text).toEqual('Successfully completed pledge #1'); + // notifications are sent + expect(slack.postOnSlackMultipleChannels.mock.calls + .filter((x) => x[0].text.match('has been completed'))[0]).toEqual([ + { text: 'pledge "content" has been completed !!!' }, + ['@requester', '@performer'] + ]); + return getList().then((res) => { + // pledge is not in list any more + expect(res.text).not.toMatch('_@performer_ pledged to content *by 2 January at 10:00*'); + }); + }); + }); + }); + }); + + it('complete: notifies both immediately, even if expired', () => { + MockDate.set(0); + const timezoneOffset = new Date().getTimezoneOffset(); + const interval = 10 * 60 * 60 * 1000; // 10 hours + const adjInterval = interval + timezoneOffset * 60 * 1000; + // create a pledge + return createPledge('today at 10am').then(async () => { + // let it expire + MockDate.set(adjInterval + 999); + await findNewNotifications(); + // complete the pledge + return request(app).get('/completePledge/1').expect(200).then((res) => { + expect(res.text).toEqual('Successfully completed pledge #1'); + // notifications are sent + expect(slack.postOnSlackMultipleChannels.mock.calls + .filter((x) => x[0].text.match('has been completed'))[0]).toEqual([ + { text: 'pledge "content" has been completed !!!' }, + ['@requester', '@performer'] + ]); + }); + }); + }); + + }); }); diff --git a/yarn.lock b/yarn.lock index 5d905d1..1ab72fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,23 @@ # yarn lockfile v1 +"@slack/client@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@slack/client/-/client-3.8.1.tgz#ac1744f3e87ebd52f60979ecc084c615544caeeb" + dependencies: + async "^1.5.0" + bluebird "^3.3.3" + eventemitter3 "^1.1.1" + https-proxy-agent "^1.0.0" + inherits "^2.0.1" + lodash "^4.13.1" + pkginfo "^0.4.0" + request "^2.64.0" + retry "^0.9.0" + url-join "0.0.1" + winston "^2.1.1" + ws "^1.0.1" + abab@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -41,6 +58,13 @@ acorn@^4.0.1: version "4.0.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" +agent-base@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.0.1.tgz#bd8f9e86a8eb221fffa07bd14befd55df142815e" + dependencies: + extend "~3.0.0" + semver "~5.0.1" + ajv-keywords@^1.0.0: version "1.5.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.0.tgz#c11e6859eafff83e0dafc416929472eca946aa2c" @@ -160,7 +184,7 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" -async@^1.4.0, async@^1.4.2, async@^1.5.2: +async@^1.4.0, async@^1.4.2, async@^1.5.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -174,6 +198,10 @@ async@~0.2.6: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -888,10 +916,14 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^2.3: +bluebird@^2.10.1, bluebird@^2.3: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" +bluebird@^3.3.3: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + body-parser@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" @@ -1002,7 +1034,7 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chokidar@^1.0.0: +chokidar@^1.0.0, chokidar@^1.4.3: version "1.6.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" dependencies: @@ -1072,7 +1104,7 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -colors@1.0.3: +colors@1.0.3, colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -1104,6 +1136,19 @@ concat-stream@^1.4.6: readable-stream "^2.2.2" typedarray "^0.0.6" +configstore@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" + dependencies: + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -1160,6 +1205,10 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": dependencies: cssom "0.3.x" +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + d@^0.1.1, d@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" @@ -1172,6 +1221,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^1.3.0: + version "1.27.2" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.27.2.tgz#ce82f420bc028356cc661fc55c0494a56a990c9c" + date.js@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/date.js/-/date.js-0.3.1.tgz#39e7c7c77adc765d10becf496cacd391332d7cc8" @@ -1185,7 +1238,7 @@ date.js@^0.3.1: lodash.partition "^4.2.0" lodash.trim "^4.2.0" -debug@^2.1.1, debug@^2.2.0: +debug@2, debug@^2.1.1, debug@^2.2.0: version "2.6.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" dependencies: @@ -1258,6 +1311,19 @@ doctrine@^1.2.2: esutils "^2.0.2" isarray "^1.0.0" +duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +duplexify@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" + dependencies: + end-of-stream "1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -1272,6 +1338,12 @@ encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +end-of-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" + dependencies: + once "~1.3.0" + "errno@>=0.1.1 <0.2.0-0": version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" @@ -1310,6 +1382,10 @@ es6-map@^0.1.3: es6-symbol "~3.1.0" event-emitter "~0.3.4" +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + es6-set@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" @@ -1474,6 +1550,22 @@ event-emitter@~0.3.4: d "~0.1.1" es5-ext "~0.10.7" +event-stream@~3.3.0: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +eventemitter3@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + exec-sh@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" @@ -1527,7 +1619,7 @@ express@^4.13.4: utils-merge "1.0.0" vary "~1.1.0" -extend@^3.0.0, extend@~3.0.0: +extend@3, extend@^3.0.0, extend@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" @@ -1541,6 +1633,10 @@ extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -1654,6 +1750,10 @@ fresh@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" +from@~0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.3.tgz#ef63ac2062ac32acf7862e0d40b44b896f22f3bc" + fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" @@ -1769,7 +1869,22 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: +got@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" + dependencies: + duplexify "^3.2.0" + infinity-agent "^2.0.0" + is-redirect "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + nested-error-stacks "^1.0.0" + object-assign "^3.0.0" + prepend-http "^1.0.0" + read-all-stream "^3.0.0" + timed-out "^2.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1827,6 +1942,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.0.tgz#4a4557460f69842ed463aa00628cc26d2683afa7" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -1860,10 +1979,22 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" + dependencies: + agent-base "2" + debug "2" + extend "3" + iconv-lite@0.4.13, iconv-lite@^0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" +ignore-by-default@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + ignore@^3.1.2: version "3.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" @@ -1872,6 +2003,10 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +infinity-agent@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1994,6 +2129,10 @@ is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: jsonpointer "^4.0.0" xtend "^4.0.0" +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + is-number@^2.0.2, is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -2028,12 +2167,20 @@ is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + is-resolvable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" dependencies: tryit "^1.0.1" +is-stream@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -2046,6 +2193,10 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isemail@2.x.x: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6" + isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" @@ -2056,7 +2207,7 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" -isstream@~0.1.2: +isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -2124,6 +2275,10 @@ istanbul-reports@^1.0.0: dependencies: handlebars "^4.0.3" +items@2.x.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" + jest-changed-files@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-17.0.2.tgz#f5657758736996f590a51b87e5c9369d904ba7b7" @@ -2312,6 +2467,16 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" +joi@^9.0.4: + version "9.2.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-9.2.0.tgz#3385ac790192130cbe230e802ec02c9215bbfeda" + dependencies: + hoek "4.x.x" + isemail "2.x.x" + items "2.x.x" + moment "2.x.x" + topo "2.x.x" + js-tokens@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" @@ -2407,6 +2572,12 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" +latest-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" + dependencies: + package-json "^1.0.0" + lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -2472,10 +2643,30 @@ lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash.assign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + lodash.assign@^4.0.0, lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" @@ -2487,6 +2678,13 @@ lodash.clonedeep@^3.0.0: lodash._baseclone "^3.0.0" lodash._bindcallback "^3.0.0" +lodash.defaults@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" + dependencies: + lodash.assign "^3.0.0" + lodash.restparam "^3.0.0" + lodash.filter@^4.2.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" @@ -2531,11 +2729,15 @@ lodash.pickby@^4.0.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + lodash.trim@^4.2.0: version "4.5.1" resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2549,12 +2751,20 @@ loose-envify@^1.0.0: dependencies: js-tokens "^2.0.0" +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" dependencies: tmpl "1.0.x" +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + marked-terminal@^1.6.2: version "1.7.0" resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" @@ -2641,6 +2851,10 @@ mockdate@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.1.tgz#51bc309e2c4396600d56b6c23a6a0f4182943a36" +moment@2.x.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -2665,6 +2879,12 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +nested-error-stacks@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" + dependencies: + inherits "~2.0.1" + node-emoji@^1.4.1: version "1.5.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.0.tgz#9a0d9fe03fd43afa357d6d8e439aa31e599959b7" @@ -2701,6 +2921,27 @@ node-pre-gyp@^0.6.29, node-pre-gyp@~0.6.31: tar "~2.2.1" tar-pack "~3.3.0" +nodemon@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" + dependencies: + chokidar "^1.4.3" + debug "^2.2.0" + es6-promise "^3.0.2" + ignore-by-default "^1.0.0" + lodash.defaults "^3.1.2" + minimatch "^3.0.0" + ps-tree "^1.0.1" + touch "1.0.0" + undefsafe "0.0.3" + update-notifier "0.5.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + dependencies: + abbrev "1" + nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -2741,6 +2982,10 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" @@ -2764,7 +3009,7 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" -once@~1.3.3: +once@~1.3.0, once@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" dependencies: @@ -2792,6 +3037,10 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -2802,10 +3051,17 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-tmpdir@^1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" +osenv@^0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + output-file-sync@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" @@ -2814,6 +3070,13 @@ output-file-sync@^1.1.0: mkdirp "^0.5.1" object-assign "^4.1.0" +package-json@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" + dependencies: + got "^3.2.0" + registry-url "^3.0.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2867,6 +3130,12 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2881,6 +3150,10 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" +pkginfo@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65" + pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -2889,6 +3162,10 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" @@ -2922,6 +3199,12 @@ prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" +ps-tree@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" + dependencies: + event-stream "~3.3.0" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -2953,7 +3236,7 @@ raw-body@~2.1.7: iconv-lite "0.4.13" unpipe "1.0.0" -rc@~1.1.6: +rc@^1.0.1, rc@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" dependencies: @@ -2962,6 +3245,13 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -2977,7 +3267,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2: +readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" dependencies: @@ -3055,6 +3345,12 @@ regexpu-core@^2.0.0: regjsgen "^0.2.0" regjsparser "^0.1.4" +registry-url@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + regjsgen@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" @@ -3073,6 +3369,12 @@ repeat-string@^1.5.2: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" +repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" @@ -3087,7 +3389,7 @@ request-promise@^2.0.1: lodash "^4.5.0" request "^2.34" -request@^2.34, request@^2.55.0, request@^2.75.0, request@^2.79.0: +request@^2.34, request@^2.55.0, request@^2.64.0, request@^2.67.0, request@^2.75.0, request@^2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" dependencies: @@ -3146,6 +3448,10 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" +retry@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -3183,10 +3489,20 @@ sax@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@~5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" + send@0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" @@ -3238,6 +3554,16 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simple-oauth2@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/simple-oauth2/-/simple-oauth2-1.0.3.tgz#949e5bec0674904fde3eda5e7a87b1677dbe1ef8" + dependencies: + bluebird "^2.10.1" + date-fns "^1.3.0" + debug "^2.2.0" + joi "^9.0.4" + request "^2.67.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -3246,6 +3572,10 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" @@ -3288,6 +3618,12 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + dependencies: + through "2" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -3320,10 +3656,30 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +stack-trace@0.0.x: + version "0.0.9" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" + "statuses@>= 1.3.1 < 2", statuses@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + dependencies: + duplexer "~0.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -3457,10 +3813,14 @@ throat@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-3.0.0.tgz#e7c64c867cbb3845f10877642f7b60055b8ec0d6" -through@^2.3.6: +through@2, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +timed-out@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -3469,6 +3829,18 @@ to-fast-properties@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + dependencies: + hoek "4.x.x" + +touch@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de" + dependencies: + nopt "~1.0.10" + tough-cookie@^2.3.1, tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" @@ -3525,10 +3897,34 @@ uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +undefsafe@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +update-notifier@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" + dependencies: + chalk "^1.0.0" + configstore "^1.0.0" + is-npm "^1.0.0" + latest-version "^1.0.0" + repeating "^1.1.2" + semver-diff "^2.0.0" + string-length "^1.0.0" + +url-join@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -3547,6 +3943,10 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" @@ -3621,6 +4021,17 @@ window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" +winston@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -3651,12 +4062,33 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write-file-atomic@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + write@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" dependencies: mkdirp "^0.5.1" +ws@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +xdg-basedir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" + dependencies: + os-homedir "^1.0.0" + "xml-name-validator@>= 2.0.1 < 3.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"