Merge from master + add MCI codes

* FT, DD, FB, DB MCI codes and backing system stats
This commit is contained in:
Bryan Ashby 2020-11-26 15:53:21 -07:00
commit 3a7f7750ab
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
20 changed files with 346 additions and 107 deletions

View File

@ -9,6 +9,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* New `PV` ACS check for arbitrary user properties. See [ACS](./docs/configuration/acs.md) for details.
* The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future.
* A number of new MCI codes (see [MCI](./docs/art/mci.md))
* Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system!
## 0.0.11-beta
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!

Binary file not shown.

View File

@ -13,6 +13,7 @@ const resolvePath = require('./misc_util.js').resolvePath;
const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js');
const SysLogKeys = require('./system_log.js');
const UserLogNames = require('./user_log_name');
// deps
const async = require('async');
@ -209,7 +210,7 @@ function initialize(cb) {
process.on('SIGINT', shutdownSystem);
require('later').date.localTime(); // use local times for later.js/scheduling
require('@breejs/later').date.localTime(); // use local times for later.js/scheduling
return callback(null);
},
@ -287,6 +288,42 @@ function initialize(cb) {
return callback(null);
});
},
function initUserTransferStats(callback) {
const StatLog = require('./stat_log');
const entries = [
[ UserLogNames.UlFiles, [ SysProps.FileUlTodayCount, 'count' ] ],
[ UserLogNames.UlFileBytes, [ SysProps.FileUlTodayBytes, 'obj' ] ],
[ UserLogNames.DlFiles, [ SysProps.FileDlTodayCount, 'count' ] ],
[ UserLogNames.DlFileBytes, [ SysProps.FileDlTodayBytes, 'obj' ] ],
];
async.each(entries, (entry, nextEntry) => {
const [ logName, [sysPropName, resultType] ] = entry;
const filter = {
logName,
resultType,
date : moment(),
};
filter.logName = logName;
StatLog.findUserLogEntries(filter, (err, stat) => {
if (!err) {
if (resultType === 'obj') {
stat = stat.reduce( (bytes, entry) => bytes + parseInt(entry.log_value) || 0, 0);
}
StatLog.setNonPersistentSystemStat(sysPropName, stat);
}
return nextEntry(null);
});
},
() => {
return callback(null);
});
},
function initMessageStats(callback) {
return require('./message_area.js').startup(callback);
},

View File

@ -132,6 +132,9 @@ ClientTerminal.prototype.isANSI = function() {
//
// Reports from various terminals
//
// NetRunner v2.00beta 20
// * This version adds 256 colors and reports as "ansi-256color"
//
// syncterm:
// * SyncTERM
//
@ -150,7 +153,7 @@ ClientTerminal.prototype.isANSI = function() {
// linux:
// * JuiceSSH (note: TERM=linux also)
//
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType);
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType);
};
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)

View File

@ -8,7 +8,7 @@ const Log = require('./logger.js').log;
const { Errors } = require('./enig_error.js');
const _ = require('lodash');
const later = require('later');
const later = require('@breejs/later');
const path = require('path');
const pty = require('node-pty');
const sane = require('sane');

View File

@ -478,6 +478,9 @@ class FileAreaWebAccess {
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1);
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, dlBytes);
return callback(null, user);
},
function sendEvent(user, callback) {

View File

@ -544,6 +544,9 @@ exports.getModule = class TransferFileModule extends MenuModule {
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, downloadCount);
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, downloadBytes);
fileIds.forEach(fileId => {
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
});
@ -575,6 +578,9 @@ exports.getModule = class TransferFileModule extends MenuModule {
StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount);
StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes);
StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayCount, uploadCount);
StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayBytes, uploadBytes);
return cb(null);
});
}

View File

@ -21,17 +21,25 @@ const {
isAnsi, stripAnsiControlCodes,
insert
} = require('./string_util.js');
const { stripMciColorCodes } = require('./color_codes.js');
const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js');
const Events = require('./events.js');
const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js');
const FileArea = require('./file_base_area.js');
const FileEntry = require('./file_entry.js');
const DownloadQueue = require('./download_queue.js');
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const moment = require('moment');
const fse = require('fs-extra');
const fs = require('graceful-fs');
const paths = require('path');
const sanatizeFilename = require('sanitize-filename');
exports.moduleInfo = {
name : 'Full Screen Editor (FSE)',
@ -255,7 +263,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
viewModeMenuHelp : function(formData, extraArgs, cb) {
self.viewControllers.footerView.setFocus(false);
return self.displayHelp(cb);
}
},
addToDownloadQueue : (formData, extraArgs, cb) => {
this.viewControllers.footerView.setFocus(false);
return this.addToDownloadQueue(cb);
},
};
}
@ -853,7 +866,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
this.setHeaderText(MciViewIds.header.subject, this.message.subject);
this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat()));
this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(
this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat())
);
this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString());
this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString());
@ -901,6 +918,98 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
);
}
addToDownloadQueue(cb) {
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads);
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
const msgInfo = this.getHeaderFormatObj();
const outputFileName = paths.join(
sysTempDownloadDir,
sanatizeFilename(
`(${msgInfo.messageId}) ${msgInfo.subject}_(${this.message.modTimestamp.format('YYYY-MM-DD')}).txt`)
);
async.waterfall(
[
(callback) => {
const header =
`+${'-'.repeat(79)}
| To : ${msgInfo.toUserName}
| From : ${msgInfo.fromUserName}
| When : ${moment(this.message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')}
| Subject : ${msgInfo.subject}
| ID : ${this.message.messageUuid} (${msgInfo.messageId})
+${'-'.repeat(79)}
`;
const body = this.viewControllers.body
.getView(MciViewIds.body.message)
.getData( { forceLineTerms : true } );
const cleanBody = stripMciColorCodes(
stripAnsiControlCodes(body, { all : true } )
);
const exportedMessage = `${header}\r\n${cleanBody}`;
fse.mkdirs(sysTempDownloadDir, err => {
return callback(err, exportedMessage);
});
},
(exportedMessage, callback) => {
return fs.writeFile(outputFileName, exportedMessage, 'utf8', callback);
},
(callback) => {
fs.stat(outputFileName, (err, stats) => {
return callback(err, stats.size);
});
},
(fileSize, callback) => {
const newEntry = new FileEntry({
areaTag : sysTempDownloadArea.areaTag,
fileName : paths.basename(outputFileName),
storageTag : sysTempDownloadArea.storageTags[0],
meta : {
upload_by_username : this.client.user.username,
upload_by_user_id : this.client.user.userId,
byte_size : fileSize,
session_temp_dl : 1, // download is valid until session is over
}
});
newEntry.desc = `${msgInfo.messageId} - ${msgInfo.subject}`;
newEntry.persist(err => {
if(!err) {
// queue it!
DownloadQueue.get(this.client).addTemporaryDownload(newEntry);
}
return callback(err);
});
},
(callback) => {
const artSpec = this.menuConfig.config.art.expToDlQueue ||
Buffer.from('Exported message has been added to your download queue!');
this.displayAsset(
artSpec,
{ clearScreen : true },
() => {
this.client.waitForKeyPress( () => {
this.redrawScreen( () => {
this.viewControllers[this.getFooterName()].setFocus(true);
return callback(null);
});
});
}
);
}
],
err => {
return cb(err);
}
);
}
displayQuoteBuilder() {
//
// Clear body area

View File

@ -634,7 +634,9 @@ module.exports = class Message {
self.fromUserName = msgRow.from_user_name;
self.subject = msgRow.subject;
self.message = msgRow.message;
self.modTimestamp = moment(msgRow.modified_timestamp);
// We use parseZone() to *preserve* the time zone information
self.modTimestamp = moment.parseZone(msgRow.modified_timestamp);
return callback(err);
}

View File

@ -102,11 +102,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
self.client.log(extraArgs, 'Missing extraArgs.menu');
return cb(null);
}
},
});
}
loadMessageByUuid(uuid, cb) {
const msg = new Message();
msg.load( { uuid : uuid, user : this.client.user }, () => {

View File

@ -292,10 +292,22 @@ const PREDEFINED_MCI_GENERATORS = {
TP : function totalMessagesOnSystem() { // Obv/2
return StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0);
},
FT : function totalUploadsToday() { // Obv/2
return StatLog.getFriendlySystemStat(SysProps.FileUlTodayCount, 0);
},
FB : function totalUploadBytesToday() {
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTodayBytes);
return formatByteSize(byteSize, true); // true=withAbbr
},
DD : function totalDownloadsToday() { // iNiQUiTY
return StatLog.getFriendlySystemStat(SysProps.FileDlTodayCount, 0);
},
DB : function totalDownloadBytesToday() {
const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTodayBytes);
return formatByteSize(byteSize, true); // true=withAbbr
},
// :TODO: NT - New users today (Obv/2)
// :TODO: FT - Files uploaded/added *today* (Obv/2)
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
// :TODO: LC - name of last caller to system (Obv/2)
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
// :TODO: ?? - Total users on system

View File

@ -30,7 +30,7 @@ const _ = require('lodash');
const paths = require('path');
const async = require('async');
const fs = require('graceful-fs');
const later = require('later');
const later = require('@breejs/later');
const temptmp = require('temptmp').createTrackedSession('ftn_bso');
const assert = require('assert');
const sane = require('sane');

View File

@ -259,75 +259,18 @@ class StatLog {
);
}
/*
Find System Log entries by |filter|:
filter.logName (required)
filter.resultType = (obj) | count
where obj contains timestamp and log_value
filter.limit
filter.date - exact date to filter against
filter.order = (timestamp) | timestamp_asc | timestamp_desc | random
*/
//
// Find System Log entry(s) by |filter|:
//
// - logName: Name of log (required)
// - resultType: 'obj' | 'count' (default='obj')
// - limit: Limit returned results
// - date: exact date to filter against
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
// (default='timestamp')
//
findSystemLogEntries(filter, cb) {
filter = filter || {};
if(!_.isString(filter.logName)) {
return cb(Errors.MissingParam('filter.logName is required'));
}
filter.resultType = filter.resultType || 'obj';
filter.order = filter.order || 'timestamp';
let sql;
if('count' === filter.resultType) {
sql =
`SELECT COUNT() AS count
FROM system_event_log`;
} else {
sql =
`SELECT timestamp, log_value
FROM system_event_log`;
}
sql += ' WHERE log_name = ?';
if(filter.date) {
filter.date = moment(filter.date);
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`;
}
if('count' !== filter.resultType) {
switch(filter.order) {
case 'timestamp' :
case 'timestamp_asc' :
sql += ' ORDER BY timestamp ASC';
break;
case 'timestamp_desc' :
sql += ' ORDER BY timestamp DESC';
break;
case 'random' :
sql += ' ORDER BY RANDOM()';
break;
}
}
if(_.isNumber(filter.limit) && 0 !== filter.limit) {
sql += ` LIMIT ${filter.limit}`;
}
sql += ';';
if('count' === filter.resultType) {
sysDb.get(sql, [ filter.logName ], (err, row) => {
return cb(err, row ? row.count : 0);
});
} else {
sysDb.all(sql, [ filter.logName ], (err, rows) => {
return cb(err, rows);
});
}
return this._findLogEntries('system_event_log', filter, cb);
}
getSystemLogEntries(logName, order, limit, cb) {
@ -389,6 +332,22 @@ class StatLog {
return cb(null);
}
//
// Find User Log entry(s) by |filter|:
//
// - logName: Name of log (required)
// - userId: User ID in which to restrict entries to (missing=all)
// - sessionId: Session ID in which to restrict entries to (missing=any)
// - resultType: 'obj' | 'count' (default='obj')
// - limit: Limit returned results
// - date: exact date to filter against
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
// (default='timestamp')
//
findUserLogEntries(filter, cb) {
return this._findLogEntries('user_event_log', filter, cb);
}
_refreshSystemStat(statName) {
switch (statName) {
case SysProps.SystemLoadStats :
@ -431,6 +390,75 @@ class StatLog {
// :TODO: log me
});
}
_findLogEntries(logTable, filter, cb) {
filter = filter || {};
if(!_.isString(filter.logName)) {
return cb(Errors.MissingParam('filter.logName is required'));
}
filter.resultType = filter.resultType || 'obj';
filter.order = filter.order || 'timestamp';
let sql;
if('count' === filter.resultType) {
sql =
`SELECT COUNT() AS count
FROM ${logTable}`;
} else {
sql =
`SELECT timestamp, log_value
FROM ${logTable}`;
}
sql += ' WHERE log_name = ?';
if (_.isNumber(filter.userId)) {
sql += ` AND user_id = ${filter.userId}`;
}
if (filter.sessionId) {
sql += ` AND session_id = ${filter.sessionId}`;
}
if(filter.date) {
filter.date = moment(filter.date);
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`;
}
if('count' !== filter.resultType) {
switch(filter.order) {
case 'timestamp' :
case 'timestamp_asc' :
sql += ' ORDER BY timestamp ASC';
break;
case 'timestamp_desc' :
sql += ' ORDER BY timestamp DESC';
break;
case 'random' :
sql += ' ORDER BY RANDOM()';
break;
}
}
if(_.isNumber(filter.limit) && 0 !== filter.limit) {
sql += ` LIMIT ${filter.limit}`;
}
sql += ';';
if('count' === filter.resultType) {
sysDb.get(sql, [ filter.logName ], (err, row) => {
return cb(err, row ? row.count : 0);
});
} else {
sysDb.all(sql, [ filter.logName ], (err, rows) => {
return cb(err, rows);
});
}
}
}
module.exports = new StatLog();

View File

@ -6,6 +6,6 @@
//
module.exports = {
UserAddedRumorz : 'system_rumorz',
UserLoginHistory : 'user_login_history',
UserLoginHistory : 'user_login_history', // JSON object
};

View File

@ -17,6 +17,11 @@ module.exports = {
FileDlTotalCount : 'dl_total_count',
FileDlTotalBytes : 'dl_total_bytes',
FileUlTodayCount : 'ul_today_count', // non-persistent
FileUlTodayBytes : 'ul_today_bytes', // non-persistent
FileDlTodayCount : 'dl_today_count', // non-persistent
FileDlTodayBytes : 'dl_today_bytes', // non-persistent
MessageTotalCount : 'message_post_total_count', // total non-private messages on the system; non-persistent
MessagesToday : 'message_post_today', // non-private messages posted/imported today; non-persistent

View File

@ -88,9 +88,13 @@ There are many predefined MCI codes that can be used anywhere on the system (pla
| `SU` | Total uploads, system wide |
| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) |
| `TF` | Total number of files on the system |
| `TB` | Total amount of files on the system (formatted to appropriate bytes/megs/gigs/etc.) |
| `TB` | Total file base size (formatted to appropriate bytes/megs/gigs/etc.) |
| `TP` | Total messages posted/imported to the system *currently* |
| `PT` | Total messages posted/imported to the system *today* |
| `FT` | Total number of uploads to the system *today* |
| `FB` | Total upload amount *today* (formatted to appropriate bytes/megs/etc. ) |
| `DD` | Total number of downloads from the system *today* |
| `DB` | Total download amount *today* (formatted to appropriate bytes/megs/etc. ) |
| `MB` | System memory |
| `MF` | System _free_ memory |
| `LA` | System load average (e.g. 0.25)<br>(Not available for all platforms) |
@ -113,32 +117,53 @@ Some additional special case codes also exist:
the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js)
for a full listing.
:note: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc.
:memo: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc.
## Views
A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is
a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu.
| Code | Name | Description |
|------|----------------------|------------------|
| `TL` | Text Label | Displays text |
| `ET` | Edit Text | Collect user input |
| `ME` | Masked Edit Text | Collect user input using a *mask* |
| `MT` | Multi Line Text Edit | Multi line edit control |
| `BT` | Button | A button |
| `VM` | Vertical Menu | A vertical menu aka a vertical lightbar |
| `HM` | Horizontal Menu | A horizontal menu aka a horizontal lightbar |
| `SM` | Spinner Menu | A spinner input control |
| `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input |
| `KE` | Key Entry | A *single* key input control |
| Code | Name | Description | Notes |
|------|----------------------|------------------|-------|
| `TL` | Text Label | Displays text | Static content |
| `ET` | Edit Text | Collect user input | Single line entry |
| `ME` | Masked Edit Text | Collect user input using a *mask* | See **Mask Edits** below |
| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. |
| `BT` | Button | A button | ...it's a button |
| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists |
| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar |
| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options |
| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input |
| `KE` | Key Entry | A *single* key input control | Think hotkeys |
:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to
see additional information.
:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information.
### Mask Edits
Mask Edits (`%ME`) use the special `maskPattern` property to control a _mask_. This can be useful for gathering dates, phone numbers, so on.
`maskPattern`'s can be composed of the following characters:
* `#`: Numeric 0-9
* `A`: Alpha a-z, A-Z
* `@`: Alphanumeric (combination of the previous patterns)
* `&`: Any "printable" character
Any other characters are literals.
An example of a mask for a date may look like this: `##/##/####`.
Additionally, the following theme stylers can be applied:
* `styleSGR1`: Controls literal character colors for non-focused controls
* `styleSGR2`: Controls literal character colors for focused controls
* `styleSGR3`: Controls fill colors (characters that have not yet received input).
All of the style properties can take pipe codes such as `|00|08`.
### View Identifiers
As mentioned above, MCI codes can (and often should) be explicitly tied to a *View Identifier*. Simply speaking this is a number representing the particular view. These can be useful to reference in code, apply themes, etc.
A view ID is tied to a MCI code by specifying it after the code. For example: `%VM1` or `%SM10`.
## Properties & Theming
Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. See [Themes](themes.md) for more information on this subject.

View File

@ -1,6 +1,6 @@
---
layout: page
title: User List
title: The Show Art Module
---
## The Show Art Module
The built in `show_art` module add some advanced ways in which you can configure your system to display art assets beyond what a standard menu entry can provide. For example, based on user selection of a file or message base area.

View File

@ -481,6 +481,7 @@
body: MSGBODY
footerView: MSGVFTR
help: MSGVHLP
expToDlQueue: mb_export_dl_queue
},
editorMode: view
editorType: area
@ -525,7 +526,7 @@
mci: {
HM1: {
// :TODO: (#)Jump/(L)Index (msg list)/Last
items: [ "prev", "next", "reply", "quit", "help" ]
items: [ "prev", "next", "reply", "quit", "download", "help" ]
focusItemIndex: 1
}
}
@ -552,6 +553,10 @@
}
{
value: { 1: 4 }
action: @method:addToDownloadQueue
}
{
value: { 1: 5 }
action: @method:viewModeMenuHelp
}
]
@ -576,6 +581,10 @@
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "d", "shift + d" ]
action: @method:addToDownloadQueue
}
{
keys: [ "?" ]
action: @method:viewModeMenuHelp

View File

@ -36,7 +36,7 @@
"iconv-lite": "^0.6.2",
"ini-config-parser": "^1.0.4",
"inquirer": "7.3.3",
"later": "1.2.0",
"@breejs/later" : "^4.0.2",
"lodash": "4.17.20",
"lru-cache": "^5.1.1",
"mime-types": "2.1.27",

View File

@ -23,6 +23,11 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@breejs/later@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@breejs/later/-/later-4.0.2.tgz#38c85cc98b717c7a196f87238090adaea01f8c9e"
integrity sha512-EN0SlbyYouBdtZis1htdsgGlwFePzkXPwdIeqaBaavxkJT1G2/bitc2LSixjv45z2njXslxlJI1mW2O/Gmrb+A==
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@ -1392,11 +1397,6 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
later@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f"
integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8=
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"