Merge pull request #292 from NuSkooler/280-config-revamp

#280 Configuration Revamp
This commit is contained in:
Bryan Ashby 2020-07-13 18:04:08 -06:00 committed by GitHub
commit 9fa0c4458e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 6490 additions and 6363 deletions

View File

@ -3,7 +3,7 @@ For :bug: bug reports, please fill out the information below plus any additional
**Short problem description**
**Environment**
- [ ] I am using Node.js v10.x LTS or higher
- [ ] I am using Node.js v12.x LTS or higher
- [ ] `npm install` or `yarn` reports success
- Actual Node.js version (`node --version`):
- Operating system (`uname -a` on *nix systems):

View File

@ -2,29 +2,17 @@
This document covers basic upgrade notes for major ENiGMA½ version updates.
# Before Upgrading
* Always back up your system!
* Always back up your system! (See [Administration](/docs/admin/administration.md))
* Seriously, always back up your system!
* At least back up the `db` directory and your `menu.hjson` (or renamed equivalent)
# General Notes
## Configuration File Updates
In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the default `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system!
In general, look at template menu files in `misc/menu_templates`, and `config_template.in.hjson` as well as the default `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system!
### menu.hjson
Upgrades often come with changes to the default `menu_template.in.hjson`. It is wise to use a *different* file name for your BBS's version of this file and point to it via `config.hjson`. For example:
```hjson
general: {
menuFile: my_bbs.hjson
}
```
After updating code, use a program such as DiffMerge to merge in updates to
`my_bbs.hjson` from the shipping `menu.hjson`.
### theme.hjson
Any custom themes you have created may now be missing features as well. Take a look at the default `luciano_blocktronics/theme.hjson` file. You can use missing sections in your `theme.hjson` (which will generally correspond to sections you've also merged in to your `menu.hjson`).
### Menus & Theme Updates
Upgrades often come with changes to the default menu templates found in `misc/menu_tempaltes`. You can use these as references for changes and additions to the default menu sets. This also applies to the default `luciano_blocktronics` theme and it's `theme.hjson` file.
See [Updating](/docs/admin/updating.md) for details on menu files/etc.
# Upgrading the Code
Upgrading from GitHub is easy:
@ -33,7 +21,7 @@ Upgrading from GitHub is easy:
cd /path/to/enigma-bbs
git pull
rm -rf npm_modules # do this any time you update Node.js itself
npm install
npm install # or simply 'yarn'
```
# Problems
@ -42,6 +30,15 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or
# 0.0.11-beta to 0.0.12-beta
* Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information.
* **BREAKING CHANGE** There is no longer a `prompt.hjson` file. Prompts are now simply part of the menu set in the `prompts` section. If you have an existing system you will need to add your `prompt.hjson` to your `menu.hjson`'s `includes` section at a minimum. Example:
```hjson
// menu.hjson
{
includes: [
my-prompts.hjson // ref your old prompts here
]
}
```
# 0.0.10-alpha to 0.0.11-beta
* Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`).

View File

@ -3,7 +3,11 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
## 0.0.12-beta
* The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276).
* The default configuration has been moved to [config_default.js](/core/config_default.js).
* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`.
* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired.
* 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.
## 0.0.11-beta
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!

View File

@ -4,6 +4,11 @@
author: Luciano Ayres
group: blocktronics
enabled: true
//
// Also check out Luciano's ANSIGARDEN:
// http://www.ansigarden.com/
//
}
customization: {
@ -32,7 +37,7 @@
ET2: { width: 23 }
ET5: { width: 23 }
ET6: { width: 23 }
ET7: { width: 23 }
ET8: { width: 23 }
ET9: { width: 23 }
@ -43,14 +48,14 @@
}
}
}
newUserApplicationSsh: {
mci: {
ET1: { width: 23 }
ET2: { width: 23 }
ET5: { width: 23 }
ET6: { width: 23 }
ET7: { width: 23 }
ET8: { width: 23 }
ET9: { width: 23 }
@ -97,7 +102,7 @@
}
TM2: {
focusTextStyle: first lower
}
}
}
}
1: {
@ -227,7 +232,7 @@
}
TM2: {
focusTextStyle: first lower
}
}
}
}
1: {
@ -241,8 +246,8 @@
}
}
messageAreaMessageList: {
config: {
messageBaseMessageList: {
config: {
dateTimeFormat: ddd MMM Do
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
}
@ -250,13 +255,13 @@
VM1: {
height: 14
width: 70
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts:<15.16} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts:<15.16} {newIndicator}"
}
}
}
messageAreaChangeCurrentConference: {
messageBaseChangeCurrentConference: {
mci: {
VM1: {
width: 26
@ -267,7 +272,7 @@
}
}
messageAreaChangeCurrentArea: {
messageBaseChangeCurrentArea: {
mci: {
VM1: {
width: 26
@ -278,7 +283,7 @@
}
}
messageAreaSetNewScanDate: {
messageBaseSetNewScanDate: {
mci: {
SM2: {
width: 54
@ -299,7 +304,7 @@
}
}
mailMenuCreateMessage: {
privateMailMenuCreateMessage: {
0: {
mci: {
TL1: { width: 19, textOverflow: "..." }
@ -314,7 +319,7 @@
}
}
mailMenuInbox: {
privateMailMenuInbox: {
config: {
dateTimeFormat: ddd MMM Do
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
@ -344,7 +349,7 @@
TM2: {
focusTextStyle: upper
items: [ "yes", "no" ]
}
}
}
}
1: {
@ -375,7 +380,7 @@
TL6: { width: 28 }
TL7: { width: 28 }
TL8: { width: 28 }
TL9: { width: 28 }
TL9: { width: 28 }
}
},
1: {
@ -500,7 +505,7 @@
}
}
messageAreaSearchMessageList: {
messageBaseSearchMessageList: {
config: {
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
// Fri Sep 25th
@ -516,7 +521,7 @@
}
}
messageAreaMyMessagesList: {
messageBaseMyMessagesList: {
config: {
// Fri Sep 25th
dateTimeFormat: ddd MMM Do
@ -531,22 +536,23 @@
}
}
// The 'msg_list' module looks for this entry by default
messageAreaViewPost: {
0: {
mci: {
TL1: {
TL1: {
width: 19
textOverflow: ...
textOverflow: ...
}
TL2: {
width: 19
textOverflow: ...
}
TL3: {
TL3: {
width: 19
textOverflow: ...
}
TL5: {
TL5: {
width: 19
textOverflow: ...
}
@ -566,7 +572,7 @@
}
}
messageAreaNewPost: {
messageBaseNewPost: {
0: {
mci: {
TL1: { width: 19, textOverflow: "..." }
@ -582,7 +588,7 @@
}
}
messageAreaReplyPost: {
messageBaseReplyPost: {
0: {
mci: {
TL1: { width: 19, textOverflow: "..." }
@ -664,7 +670,7 @@
}
newScanMessageList: {
config: {
config: {
dateTimeFormat: ddd MMM Do
allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}"
}
@ -693,7 +699,7 @@
config: {
hashTagsSep: "|08, |07"
browseInfoFormat10: "|00|10{fileName:<.44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat12: "|00|07{hashTags}"
browseInfoFormat13: "|00|07{estReleaseYear}"
browseInfoFormat14: "|00|07{dlCount}"
@ -706,7 +712,7 @@
isQueuedIndicator: "|00|10YES"
isNotQueuedIndicator: "|00|07no"
userRatingTicked: "|00|15*"
userRatingUnticked: "|00|07-"
@ -749,7 +755,7 @@
TL13: { width: 21 }
TL14: { width: 21 }
TL15: { width: 21 }
TL16: { width: 21 }
TL16: { width: 21 }
TL17: { width: 73 }
}
@ -793,7 +799,7 @@
config: {
hashTagsSep: "|08, |07"
browseInfoFormat10: "|00|10{fileName:<44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat12: "|00|07{hashTags}"
browseInfoFormat13: "|00|07{estReleaseYear}"
browseInfoFormat14: "|00|07{dlCount}"
@ -806,7 +812,7 @@
isQueuedIndicator: "|00|10YES"
isNotQueuedIndicator: "|00|07no"
userRatingTicked: "|00|15*"
userRatingUnticked: "|00|07-"
@ -849,7 +855,7 @@
TL13: { width: 21 }
TL14: { width: 21 }
TL15: { width: 21 }
TL16: { width: 21 }
TL16: { width: 21 }
TL17: { width: 73 }
}
@ -901,7 +907,7 @@
}
}
}
}
}
fileBaseSearch: {
mci: {
@ -977,7 +983,7 @@
}
}
fileAreaFilterEditor: {
fileBaseFilterEditor: {
mci: {
ET1: {
width: 26
@ -1086,7 +1092,7 @@
mci: {
TL1: { width: 48 }
TL2: { width: 48 }
TL3: { width: 48 }
TL3: { width: 48 }
MT4: {
height: 6
width: 68

View File

@ -4,10 +4,8 @@
// ENiGMA½
const Events = require('./events.js');
const Config = require('./config.js').get;
const {
getConfigPath,
getFullConfig,
} = require('./config_util.js');
const ConfigLoader = require('./config_loader');
const { getConfigPath } = require('./config_util');
const UserDb = require('./database.js').dbs.user;
const {
getISOTimestampString
@ -29,13 +27,11 @@ const {
const stringFormat = require('./string_format.js');
const StatLog = require('./stat_log.js');
const Log = require('./logger.js').log;
const ConfigCache = require('./config_cache.js');
// deps
const _ = require('lodash');
const async = require('async');
const moment = require('moment');
const paths = require('path');
exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser;
@ -136,63 +132,60 @@ class UserStatAchievement extends Achievement {
class Achievements {
constructor(events) {
this.events = events;
this.enabled = false;
}
getAchievementByTag(tag) {
return this.achievementConfig.achievements[tag];
return this.config.get().achievements[tag];
}
isEnabled() {
return !_.isUndefined(this.achievementConfig);
return this.enabled;
}
init(cb) {
let achievementConfigPath = _.get(Config(), 'general.achievementFile');
if(!achievementConfigPath) {
const configPath = this._getConfigPath();
if (!configPath) {
Log.info('Achievements are not configured');
return cb(null);
}
achievementConfigPath = getConfigPath(achievementConfigPath); // qualify
const configLoaded = (achievementConfig) => {
if(true !== achievementConfig.enabled) {
const configLoaded = () => {
if(true !== this.config.get().enabled) {
Log.info('Achievements are not enabled');
this.enabled = false;
this.stopMonitoringUserStatEvents();
delete this.achievementConfig;
} else {
Log.info('Achievements are enabled');
this.achievementConfig = achievementConfig;
this.enabled = true;
this.monitorUserStatEvents();
}
};
const changed = ( { fileName, fileRoot } ) => {
const reCachedPath = paths.join(fileRoot, fileName);
if(reCachedPath === achievementConfigPath) {
getFullConfig(achievementConfigPath, (err, achievementConfig) => {
if(err) {
return Log.error( { error : err.message }, 'Failed to reload achievement config from cache');
}
configLoaded(achievementConfig);
});
}
};
ConfigCache.getConfigWithOptions(
{
filePath : achievementConfigPath,
forceReCache : true,
callback : changed,
},
(err, achievementConfig) => {
if(err) {
return cb(err);
this.config = new ConfigLoader({
onReload : err => {
if (!err) {
configLoaded();
}
configLoaded(achievementConfig);
return cb(null);
}
);
});
this.config.init(configPath, err => {
if (err) {
return cb(err);
}
configLoaded();
return cb(null);
});
}
_getConfigPath() {
const path = _.get(Config(), 'general.achievementFile');
if(!path) {
return;
}
return getConfigPath(path); // qualify
}
loadAchievementHitCount(user, achievementTag, field, cb) {
@ -298,7 +291,7 @@ class Achievements {
// :TODO: Make this code generic - find + return factory created object
const achievementTags = Object.keys(_.pickBy(
_.get(this.achievementConfig, 'achievements', {}),
_.get(this.config.get(), 'achievements', {}),
achievement => {
if(false === achievement.enabled) {
return false;
@ -498,7 +491,7 @@ class Achievements {
const spec =
_.get(info.details, `art.${name}`) ||
_.get(info.achievement, `art.${name}`) ||
_.get(this.achievementConfig, `art.${name}`);
_.get(this.config.get(), `art.${name}`);
if(!spec) {
return callback(null);
}

View File

@ -50,17 +50,17 @@ module.exports = class ArchiveUtil {
}
// singleton access
static getInstance(noWatch = false) {
static getInstance(hotReload = true) {
if(!archiveUtil) {
archiveUtil = new ArchiveUtil();
archiveUtil.init(noWatch);
archiveUtil.init(hotReload);
}
return archiveUtil;
}
init(noWatch = false) {
init(hotReload = true) {
this.reloadConfig();
if(!noWatch) {
if(hotReload) {
Events.on(Events.getSystemEvents().ConfigChanged, () => {
this.reloadConfig();
});

View File

@ -54,6 +54,8 @@ function printVersionAndExit() {
}
function main() {
let errorDisplayed = false;
async.waterfall(
[
function processArgs(callback) {
@ -69,12 +71,12 @@ function main() {
const configOverridePath = argv.config;
return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath));
return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath));
},
function initConfig(configPath, configPathSupplied, callback) {
const configFile = configPath + 'config.hjson';
conf.init(resolvePath(configFile), function configInit(err) {
conf.Config.create(resolvePath(configFile), err => {
//
// If the user supplied a path and we can't read/parse it
// then it's a fatal error
@ -87,7 +89,14 @@ function main() {
configPathSupplied = null; // make non-fatal; we'll go with defaults
}
} else {
console.error(err.message);
errorDisplayed = true;
console.error(`Configuration error: ${err.message}`); // eslint-disable-line no-console
if (err.hint) {
console.error(`Hint: ${err.hint}`);
}
if (err.configPath) {
console.error(`Note: ${err.configPath}`);
}
}
}
return callback(err);
@ -114,8 +123,9 @@ function main() {
});
}
if(err) {
if(err && !errorDisplayed) {
console.error('Error initializing: ' + util.inspect(err));
return process.exit();
}
}
);
@ -175,10 +185,11 @@ function initialize(cb) {
async.series(
[
function createMissingDirectories(callback) {
async.each(Object.keys(conf.config.paths), function entry(pathKey, next) {
mkdirs(conf.config.paths[pathKey], function dirCreated(err) {
const Config = conf.get();
async.each(Object.keys(Config.paths), function entry(pathKey, next) {
mkdirs(Config.paths[pathKey], function dirCreated(err) {
if(err) {
console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString());
console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString());
}
return next(err);
});
@ -211,15 +222,9 @@ function initialize(cb) {
function initStatLog(callback) {
return require('./stat_log.js').init(callback);
},
function initConfigs(callback) {
return require('./config_util.js').init(callback);
},
function initThemes(callback) {
// Have to pull in here so it's after Config init
require('./theme.js').initAvailableThemes( (err, themeCount) => {
logger.log.info({ themeCount }, 'Themes initialized');
return callback(err);
});
function initMenusAndThemes(callback) {
const { ThemeManager } = require('./theme');
return ThemeManager.create(callback);
},
function loadSysOpInformation(callback) {
//

View File

@ -83,7 +83,7 @@ function Client(/*input, output*/) {
const self = this;
this.user = new User();
this.currentTheme = { info : { name : 'N/A', description : 'None' } };
this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } };
this.lastActivityTime = Date.now();
this.menuStack = new MenuStack(this);
this.acs = new ACS( { client : this, user : this.user } );
@ -94,6 +94,26 @@ function Client(/*input, output*/) {
this.mciCache = {};
};
Object.defineProperty(this, 'currentTheme', {
get : () => {
if (this.currentThemeConfig) {
return this.currentThemeConfig.get();
} else {
return {
info : {
name : 'N/A',
author : 'N/A',
description : 'N/A',
group : 'N/A',
}
};
}
},
set : (theme) => {
this.currentThemeConfig = theme;
}
});
Object.defineProperty(this, 'node', {
get : function() {
return self.session.id;
@ -120,7 +140,7 @@ function Client(/*input, output*/) {
this.themeChangedListener = function( { themeId } ) {
if(_.get(self.currentTheme, 'info.themeId') === themeId) {
self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId);
self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,11 @@
'use strict';
// deps
const paths = require('path');
const fs = require('graceful-fs');
const hjson = require('hjson');
const sane = require('sane');
const paths = require('path');
const fs = require('graceful-fs');
const hjson = require('hjson');
const sane = require('sane');
const _ = require('lodash');
module.exports = new class ConfigCache
{
@ -14,12 +15,13 @@ module.exports = new class ConfigCache
}
getConfigWithOptions(options, cb) {
options.hotReload = _.get(options, 'hotReload', true);
const cached = this.cache.has(options.filePath);
if(options.forceReCache || !cached) {
this.recacheConfigFromFile(options.filePath, (err, config) => {
if(!err && !cached) {
if(!options.noWatch) {
if(options.hotReload) {
const watcher = sane(
paths.dirname(options.filePath),
{
@ -33,7 +35,7 @@ module.exports = new class ConfigCache
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => {
if(!err) {
if(options.callback) {
options.callback( { fileName, fileRoot } );
options.callback( { fileName, fileRoot, configCache : this } );
}
}
});

927
core/config_default.js Normal file
View File

@ -0,0 +1,927 @@
const paths = require('path');
module.exports = () => {
return {
general : {
boardName : 'Another Fine ENiGMA½ BBS',
prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS',
telnetHostname : '',
sshHostname : '',
website : 'https://enigma-bbs.github.io',
description : 'An ENiGMA½ BBS',
// :TODO: closedSystem prob belongs under users{}?
closedSystem : false, // is the system closed to new users?
menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path
achievementFile : 'achievements.hjson',
},
users : {
usernameMin : 2,
usernameMax : 16, // Note that FidoNet wants 36 max
usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$',
passwordMin : 6,
passwordMax : 128,
//
// The bad password list is a text file containing a password per line.
// Entries in this list are not allowed to be used on the system as they
// are known to be too common.
//
// A great resource can be found at https://github.com/danielmiessler/SecLists
//
// Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt
//
badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'),
realNameMax : 32,
locationMax : 32,
affilsMax : 32,
emailMax : 255,
webMax : 255,
requireActivation : false, // require SysOp activation? false = auto-activate
groups : [ 'users', 'sysops' ], // built in groups
defaultGroups : [ 'users' ], // default groups new users belong to
newUserNames : [ 'new', 'apply' ], // Names reserved for applying
badUserNames : [
'sysop', 'admin', 'administrator', 'root', 'all',
'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix',
'server', 'client', 'notme'
],
preAuthIdleLogoutSeconds : 60 * 3, // 3m
idleLogoutSeconds : 60 * 6, // 6m
failedLogin : {
disconnect : 3, // 0=disabled
lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N
autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes.
},
unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts
twoFactorAuth : {
method : 'googleAuth',
otp : {
registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'),
registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'),
registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'),
}
}
},
theme : {
default : 'luciano_blocktronics',
preLogin : 'luciano_blocktronics',
passwordChar : '*',
dateFormat : {
short : 'MM/DD/YYYY',
long : 'ddd, MMMM Do, YYYY',
},
timeFormat : {
short : 'h:mm a',
},
dateTimeFormat : {
short : 'MM/DD/YYYY h:mm a',
long : 'ddd, MMMM Do, YYYY, h:mm a',
}
},
menus : {
cls : true, // Clear screen before each menu by default?
},
paths : {
config : paths.join(__dirname, './../config/'),
security : paths.join(__dirname, './../config/security'), // certs, keys, etc.
mods : paths.join(__dirname, './../mods/'),
loginServers : paths.join(__dirname, './servers/login/'),
contentServers : paths.join(__dirname, './servers/content/'),
chatServers : paths.join(__dirname, './servers/chat/'),
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
mailers : paths.join(__dirname, './mailers/') ,
art : paths.join(__dirname, './../art/general/'),
themes : paths.join(__dirname, './../art/themes/'),
logs : paths.join(__dirname, './../logs/'),
db : paths.join(__dirname, './../db/'),
modsDb : paths.join(__dirname, './../db/mods/'),
dropFiles : paths.join(__dirname, './../drop/'), // + "/node<x>/
misc : paths.join(__dirname, './../misc/'),
},
loginServers : {
telnet : {
port : 8888,
enabled : true,
firstMenu : 'telnetConnected',
},
ssh : {
port : 8889,
enabled : false, // default to false as PK/pass in config.hjson are required
//
// To enable SSH, perform the following steps:
//
// 1 - Generate a Private Key (PK):
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
// To generate a secure PK, issue the following command:
//
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
// -out ./config/security/ssh_private_key.pem -aes128
//
// (The above is a more modern equivalent of the following):
// > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
//
// 2 - Set 'privateKeyPass' to the password you used in step #1
//
// 3 - Finally, set 'enabled' to 'true'
//
// Additional reading:
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
//
privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'),
firstMenu : 'sshConnected',
firstMenuNewUser : 'sshConnectedNewUser',
//
// SSH details that can affect security. Stronger ciphers are better for example,
// but terminals such as SyncTERM require KEX diffie-hellman-group14-sha1,
// cipher 3des-cbc, etc.
//
// See https://github.com/mscdex/ssh2-streams for the full list of supported
// algorithms.
//
algorithms : {
kex : [
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1',
'diffie-hellman-group-exchange-sha1',
'diffie-hellman-group1-sha1',
],
cipher : [
'aes128-ctr',
'aes192-ctr',
'aes256-ctr',
'aes128-gcm',
'aes128-gcm@openssh.com',
'aes256-gcm',
'aes256-gcm@openssh.com',
'aes256-cbc',
'aes192-cbc',
'aes128-cbc',
'blowfish-cbc',
'3des-cbc',
'arcfour256',
'arcfour128',
'cast128-cbc',
'arcfour',
],
hmac : [
'hmac-sha2-256',
'hmac-sha2-512',
'hmac-sha1',
'hmac-md5',
'hmac-sha2-256-96',
'hmac-sha2-512-96',
'hmac-ripemd160',
'hmac-sha1-96',
'hmac-md5-96',
],
// note that we disable compression by default due to issues with many clients. YMMV.
compress : [ 'none' ]
},
},
webSocket : {
ws : {
// non-secure ws://
enabled : false,
port : 8810,
},
wss : {
// secure ws://
// must provide valid certPem and keyPem
enabled : false,
port : 8811,
certPem : paths.join(__dirname, './../config/https_cert.pem'),
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
},
},
},
contentServers : {
web : {
domain : 'another-fine-enigma-bbs.org',
staticRoot : paths.join(__dirname, './../www'),
resetPassword : {
//
// The following templates have these variables available to them:
//
// * %BOARDNAME% : Name of BBS
// * %USERNAME% : Username of whom to reset password
// * %TOKEN% : Reset token
// * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page,
// URL to POST submit reset form.
// templates for pw reset *email*
resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version
resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version
// tempalte for pw reset *landing page*
//
resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'),
},
http : {
enabled : false,
port : 8080,
},
https : {
enabled : false,
port : 8443,
certPem : paths.join(__dirname, './../config/https_cert.pem'),
keyPem : paths.join(__dirname, './../config/https_cert_key.pem'),
}
},
gopher : {
enabled : false,
port : 8070,
publicHostname : 'another-fine-enigma-bbs.org',
publicPort : 8070, // adjust if behind NAT/etc.
bannerFile : 'gopher_banner.asc',
//
// Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ]
// to export message confs/areas
//
},
nntp : {
// internal caching of groups, message lists, etc.
cache : {
maxItems : 200,
maxAge : 1000 * 30, // 30s
},
//
// Set publicMessageConferences{} to a map of confTag -> [ areaTag1, areaTag2, ... ]
// in order to export *public* conf/areas that are available to anonymous
// NNTP users. Other conf/areas: Standard ACS rules apply.
//
publicMessageConferences: {},
nntp : {
enabled : false,
port : 8119,
},
nntps : {
enabled : false,
port : 8563,
certPem : paths.join(__dirname, './../config/nntps_cert.pem'),
keyPem : paths.join(__dirname, './../config/nntps_key.pem'),
}
}
},
chatServers : {
mrc: {
enabled : false,
serverHostname : 'mrc.bottomlessabyss.net',
serverPort : 5000,
retryDelay : 10000,
multiplexerPort : 5000,
}
},
infoExtractUtils : {
Exiftool2Desc : {
cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x
},
Exiftool : {
cmd : 'exiftool',
args : [
'-charset', 'utf8', '{filePath}',
// exclude the following:
'--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize',
'--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate',
'--metadatadate', '--xmptoolkit'
]
},
XDMS2Desc : {
// http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html
cmd : 'xdms',
args : [ 'd', '{filePath}' ]
},
XDMS2LongDesc : {
// http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html
cmd : 'xdms',
args : [ 'f', '{filePath}' ]
},
},
fileTypes : {
//
// File types explicitly known to the system. Here we can configure
// information extraction, archive treatment, etc.
//
// MIME types can be found in mime-db: https://github.com/jshttp/mime-db
//
// Resources for signature/magic bytes:
// * http://www.garykessler.net/library/file_sigs.html
//
//
// :TODO: text/x-ansi -> SAUCE extraction for .ans uploads
// :TODO: textual : bool -- if text, we can view.
// :TODO: asText : { cmd, args[] } -> viewable text
//
// Audio
//
'audio/mpeg' : {
desc : 'MP3 Audio',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'application/pdf' : {
desc : 'Adobe PDF',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
//
// Video
//
'video/mp4' : {
desc : 'MPEG Video',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'video/x-matroska ' : {
desc : 'Matroska Video',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'video/x-msvideo' : {
desc : 'Audio Video Interleave',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
//
// Images
//
'image/jpeg' : {
desc : 'JPEG Image',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'image/png' : {
desc : 'Portable Network Graphic Image',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'image/gif' : {
desc : 'Graphics Interchange Format Image',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'image/webp' : {
desc : 'WebP Image',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
//
// Archives
//
'application/zip' : {
desc : 'ZIP Archive',
sig : '504b0304',
offset : 0,
archiveHandler : 'InfoZip',
},
/*
'application/x-cbr' : {
desc : 'Comic Book Archive',
sig : '504b0304',
},
*/
'application/x-arj' : {
desc : 'ARJ Archive',
sig : '60ea',
offset : 0,
archiveHandler : 'Arj',
},
'application/x-rar-compressed' : {
desc : 'RAR Archive',
sig : '526172211a07',
offset : 0,
archiveHandler : 'Rar',
},
'application/gzip' : {
desc : 'Gzip Archive',
sig : '1f8b',
offset : 0,
archiveHandler : 'TarGz',
},
// :TODO: application/x-bzip
'application/x-bzip2' : {
desc : 'BZip2 Archive',
sig : '425a68',
offset : 0,
archiveHandler : '7Zip',
},
'application/x-lzh-compressed' : {
desc : 'LHArc Archive',
sig : '2d6c68',
offset : 2,
archiveHandler : 'Lha',
},
'application/x-lzx' : {
desc : 'LZX Archive',
sig : '4c5a5800',
offset : 0,
archiveHandler : 'Lzx',
},
'application/x-7z-compressed' : {
desc : '7-Zip Archive',
sig : '377abcaf271c',
offset : 0,
archiveHandler : '7Zip',
},
//
// Generics that need further mapping
//
'application/octet-stream' : [
{
desc : 'Amiga DISKMASHER',
sig : '444d5321', // DMS!
ext : '.dms',
shortDescUtil : 'XDMS2Desc',
longDescUtil : 'XDMS2LongDesc',
},
{
desc : 'SIO2PC Atari Disk Image',
sig : '9602', // 16bit sum of "NICKATARI"
ext : '.atr',
archiveHandler : 'Atr',
}
]
},
archives : {
archivers : {
'7Zip' : { // p7zip package
compress : {
cmd : '7za',
args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ],
},
decompress : {
cmd : '7za',
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
},
list : {
cmd : '7za',
args : [ 'l', '{archivePath}' ],
entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$',
},
extract : {
cmd : '7za',
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
},
},
InfoZip: {
compress : {
cmd : 'zip',
args : [ '{archivePath}', '{fileList}' ],
},
decompress : {
cmd : 'unzip',
args : [ '-n', '{archivePath}', '-d', '{extractPath}' ],
},
list : {
cmd : 'unzip',
args : [ '-l', '{archivePath}' ],
// Annoyingly, dates can be in YYYY-MM-DD or MM-DD-YYYY format
entryMatch : '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$',
},
extract : {
cmd : 'unzip',
args : [ '-n', '{archivePath}', '{fileList}', '-d', '{extractPath}' ],
}
},
Lha : {
//
// 'lha' command can be obtained from:
// * apt-get: lhasa
//
// (compress not currently supported)
//
decompress : {
cmd : 'lha',
args : [ '-efw={extractPath}', '{archivePath}' ],
},
list : {
cmd : 'lha',
args : [ '-l', '{archivePath}' ],
entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$',
},
extract : {
cmd : 'lha',
args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ]
}
},
Lzx : {
//
// 'unlzx' command can be obtained from:
// * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64)
// * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html
// * Source: http://xavprods.free.fr/lzx/
//
decompress : {
cmd : 'unlzx',
// unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first
args : [ '-x', '{archivePath}' ],
},
list : {
cmd : 'unlzx',
args : [ '-v', '{archivePath}' ],
entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$',
}
},
Arj : {
//
// 'arj' command can be obtained from:
// * apt-get: arj
//
decompress : {
cmd : 'arj',
args : [ 'x', '{archivePath}', '{extractPath}' ],
},
list : {
cmd : 'arj',
args : [ 'l', '{archivePath}' ],
entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$',
entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 }
fileName : 1,
byteSize : 2,
}
},
extract : {
cmd : 'arj',
args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ],
}
},
Rar : {
decompress : {
cmd : 'unrar',
args : [ 'x', '{archivePath}', '{extractPath}' ],
},
list : {
cmd : 'unrar',
args : [ 'l', '{archivePath}' ],
entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$',
},
extract : {
cmd : 'unrar',
args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ],
}
},
TarGz : {
decompress : {
cmd : 'tar',
args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ],
},
list : {
cmd : 'tar',
args : [ '-tvf', '{archivePath}' ],
entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$',
},
extract : {
cmd : 'tar',
args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ],
}
},
Atr : {
decompress : {
cmd : 'atr',
args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ]
},
list : {
cmd : 'atr',
args : [ '{archivePath}', 'ls', '-la1' ],
entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$',
},
extract : {
cmd : 'atr',
// note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course.
args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ]
}
}
},
},
fileTransferProtocols : {
//
// See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ
//
zmodem8kSexyz : {
name : 'ZModem 8k (SEXYZ)',
type : 'external',
sort : 1,
external : {
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
// Linux x86_64 binary: https://l33t.codes/outgoing/sexyz
sendCmd : 'sexyz',
sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ],
recvCmd : 'sexyz',
recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ],
recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ],
}
},
xmodemSexyz : {
name : 'XModem (SEXYZ)',
type : 'external',
sort : 3,
external : {
sendCmd : 'sexyz',
sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ],
recvCmd : 'sexyz',
recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ]
}
},
ymodemSexyz : {
name : 'YModem (SEXYZ)',
type : 'external',
sort : 4,
external : {
sendCmd : 'sexyz',
sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ],
recvCmd : 'sexyz',
recvArgs : [ '-telnet', 'ry', '{uploadDir}' ],
}
},
zmodem8kSz : {
name : 'ZModem 8k',
type : 'external',
sort : 2,
external : {
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
sendArgs : [
// :TODO: try -q
'--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}'
],
recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
recvArgs : [
'--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir}
],
processIACs : true, // escape/de-escape IACs (0xff)
}
}
},
messageAreaDefaults : {
//
// The following can be override per-area as well
//
maxMessages : 1024, // 0 = unlimited
maxAgeDays : 0, // 0 = unlimited
},
messageConferences : {
system_internal : {
name : 'System Internal',
desc : 'Built in conference for private messages, bulletins, etc.',
areas : {
private_mail : {
name : 'Private Mail',
desc : 'Private user to user mail/email',
maxExternalSentAgeDays : 30, // max external "outbox" item age
},
local_bulletin : {
name : 'System Bulletins',
desc : 'Bulletin messages for all users',
}
}
}
},
scannerTossers : {
ftn_bso : {
paths : {
outbound : paths.join(__dirname, './../mail/ftn_out/'),
inbound : paths.join(__dirname, './../mail/ftn_in/'),
secInbound : paths.join(__dirname, './../mail/ftn_secin/'),
reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc.
//outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'),
// set 'retain' to a valid path to keep good pkt files
},
//
// Packet and (ArcMail) bundle target sizes are just that: targets.
// Actual sizes may be slightly larger when we must place a full
// PKT contents *somewhere*
//
packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt
bundleTargetByteSize : 2048000, // 2M, before creating another archive
packetMsgEncoding : 'utf8', // default packet encoding. Override per node if desired.
packetAnsiMsgEncoding : 'cp437', // packet encoding for *ANSI ART* messages
tic : {
secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected)
uploadBy : 'ENiGMA TIC', // default upload by username (override @ network)
allowReplace : false, // use "Replaces" TIC field
descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc
}
}
},
fileBase: {
// areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|:
areaStoragePrefix : paths.join(__dirname, './../file_base/'),
maxDescFileByteSize : 471859, // ~1/4 MB
maxDescLongFileByteSize : 524288, // 1/2 MB
fileNamePatterns: {
// These are NOT case sensitive
// FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ
// Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available.
desc : [
'^.*FILE_ID\.ANS$', '^.*FILE_ID\.DIZ$', // eslint-disable-line no-useless-escape
'^.*DESC\.SDI$', // eslint-disable-line no-useless-escape
'^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape
'^.*FILE\.DES$', // eslint-disable-line no-useless-escape
'^.*FILE\.SDI$', // eslint-disable-line no-useless-escape
'^.*DISK\.ID$' // eslint-disable-line no-useless-escape
],
// common README filename - https://en.wikipedia.org/wiki/README
descLong : [
'^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape
'^.*README\.1ST$', // eslint-disable-line no-useless-escape
'^.*README\.NOW$', // eslint-disable-line no-useless-escape
'^.*README\.TXT$', // eslint-disable-line no-useless-escape
'^.*READ\.ME$', // eslint-disable-line no-useless-escape
'^.*README$', // eslint-disable-line no-useless-escape
'^.*README\.md$', // eslint-disable-line no-useless-escape
'^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape
],
},
yearEstPatterns: [
//
// Patterns should produce the year in the first submatch.
// The extracted year may be YY or YYYY
//
'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ...
'\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ...
'\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ...
'\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ...
//'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc.
//"\\b('[1789][0-9])\\b", // eslint-disable-line quotes
'\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b',
'\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997
'\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority
'\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries
'\\b\'([17-9][0-9])\\b', // '95, '17, ...
// :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc.
],
web : {
path : '/f/',
routePath : '/f/[a-zA-Z0-9]+$',
expireMinutes : 1440, // 1 day
},
//
// File area storage location tag/value pairs.
// Non-absolute paths are relative to |areaStoragePrefix|.
//
storageTags : {
sys_msg_attach : 'sys_msg_attach',
sys_temp_download : 'sys_temp_download',
},
areas: {
system_message_attachment : {
name : 'System Message Attachments',
desc : 'File attachments to messages',
storageTags : [ 'sys_msg_attach' ],
},
system_temporary_download : {
name : 'System Temporary Downloads',
desc : 'Temporary downloadables',
storageTags : [ 'sys_temp_download' ],
}
}
},
eventScheduler : {
events : {
dailyMaintenance : {
schedule : 'at 11:59pm',
action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent',
},
trimMessageAreas : {
// may optionally use [or ]@watch:/path/to/file
schedule : 'every 24 hours',
// action:
// - @method:path/to/module.js:theMethodName
// (path is relative to ENiGMA base dir)
//
// - @execute:/path/to/something/executable.sh
//
action : '@method:core/message_area.js:trimMessageAreasScheduledEvent',
},
nntpMaintenance : {
schedule : 'every 12 hours', // should generally be < trimMessageAreas interval
action : '@method:core/servers/content/nntp.js:performMaintenanceTask',
},
updateFileAreaStats : {
schedule : 'every 1 hours',
action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent',
},
forgotPasswordMaintenance : {
schedule : 'every 24 hours',
action : '@method:core/web_password_reset.js:performMaintenanceTask',
args : [ '24 hours' ] // items older than this will be removed
},
twoFactorRegisterTokenMaintenance : {
schedule : 'every 24 hours',
action : '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask',
args : [
'auth_factor2_otp_register',
'24 hours', // expire time
]
},
//
// Enable the following entry in your config.hjson to periodically create/update
// DESCRIPT.ION files for your file base
//
/*
updateDescriptIonFiles : {
schedule : 'on the last day of the week',
action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent',
}
*/
}
},
logging : {
rotatingFile : { // set to 'disabled' or false to disable
type : 'rotating-file',
fileName : 'enigma-bbs.log',
period : '1d',
count : 3,
level : 'debug',
}
// :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog
},
debug : {
assertsEnabled : false,
},
statLog : {
systemEvents : {
loginHistoryMax: -1, // set to -1 for forever
}
},
};
};

248
core/config_loader.js Normal file
View File

@ -0,0 +1,248 @@
// deps
const paths = require('path');
const async = require('async');
const moment = require('moment');
const _ = require('lodash');
const mapValuesDeep = require('deepdash/getMapValuesDeep')(_);
module.exports = class ConfigLoader {
constructor(
{
hotReload = true,
defaultConfig = {},
defaultsCustomizer = null,
onReload = null,
keepWsc = false,
} =
{
hotReload : true,
defaultConfig : {},
defaultsCustomizer : null,
onReload : null,
keepWsc : false,
}
)
{
this.current = {};
this.hotReload = hotReload;
this.defaultConfig = defaultConfig;
this.defaultsCustomizer = defaultsCustomizer;
this.onReload = onReload;
this.keepWsc = keepWsc;
}
init(baseConfigPath, cb) {
this.baseConfigPath = baseConfigPath;
return this._reload(baseConfigPath, cb);
}
get() {
return this.current;
}
_reload(baseConfigPath, cb) {
let defaultConfig;
if (_.isFunction(this.defaultConfig)) {
defaultConfig = this.defaultConfig();
} else if (_.isObject(this.defaultConfig)) {
defaultConfig = this.defaultConfig;
} else {
defaultConfig = {};
}
//
// 1 - Fetch base configuration from |baseConfigPath|
// 2 - Merge with |defaultConfig|
// 3 - Resolve any includes
// 4 - Resolve @reference and @environment
// 5 - Perform any validation
//
async.waterfall(
[
(callback) => {
return this._loadConfigFile(baseConfigPath, callback);
},
(config, callback) => {
if (_.isFunction(this.defaultsCustomizer)) {
const stack = [];
const mergedConfig = _.mergeWith(
defaultConfig,
config,
(defaultVal, configVal, key, target, source) => {
var path;
while (true) { // eslint-disable-line no-constant-condition
if (!stack.length) {
stack.push({source, path : []});
}
const prev = stack[stack.length - 1];
if (source === prev.source) {
path = prev.path.concat(key);
stack.push({source : configVal, path});
break;
}
stack.pop();
}
path = path.join('.');
return this.defaultsCustomizer(defaultVal, configVal, key, path);
}
);
return callback(null, mergedConfig);
}
return callback(null, _.merge(defaultConfig, config));
},
(config, callback) => {
const configRoot = paths.dirname(baseConfigPath);
return this._resolveIncludes(configRoot, config, callback);
},
(config, callback) => {
config = this._resolveAtSpecs(config);
return callback(null, config);
},
],
(err, config) => {
if (!err) {
this.current = config;
}
return cb(err);
}
);
}
_convertTo(value, type) {
switch (type) {
case 'bool' :
case 'boolean' :
value = ('1' === value || 'true' === value.toLowerCase());
break;
case 'number' :
{
const num = parseInt(value);
if (!isNaN(num)) {
value = num;
}
}
break;
case 'object' :
try {
value = JSON.parse(value);
} catch(e) {
// ignored
}
break;
case 'timestamp' :
{
const m = moment(value);
if (m.isValid()) {
value = m;
}
}
break;
}
return value;
}
_resolveEnvironmentVariable(spec) {
const [, varName, type, array] = spec.split(':');
if (!varName) {
return;
}
let value = process.env[varName];
if (!value) {
// console is about as good as we can do here
return console.info(`WARNING: environment variable "${varName}" from spec "${spec}" not found!`);
}
if ('array' === array) {
value = value.split(',').map(v => this._convertTo(v, type));
} else {
value = this._convertTo(value, type);
}
return value;
}
_loadConfigFile(filePath, cb) {
const ConfigCache = require('./config_cache');
const options = {
filePath,
hotReload : this.hotReload,
keepWsc : this.keepWsc,
callback : this._configFileChanged.bind(this),
};
ConfigCache.getConfigWithOptions(options, (err, config) => {
if (err) {
err.configPath = options.filePath;
}
return cb(err, config);
});
}
_configFileChanged({fileName, fileRoot}) {
const reCachedPath = paths.join(fileRoot, fileName);
if (this.configPaths.includes(reCachedPath)) {
this._reload(this.baseConfigPath, err => {
if (_.isFunction(this.onReload)) {
this.onReload(err, reCachedPath);
}
});
}
}
_resolveIncludes(configRoot, config, cb) {
if (!Array.isArray(config.includes)) {
this.configPaths = [ this.baseConfigPath ];
return cb(null, config);
}
// If a included file is changed, we need to re-cache, so this
// must be tracked...
const includePaths = config.includes.map(inc => paths.join(configRoot, inc));
async.eachSeries(includePaths, (includePath, nextIncludePath) => {
this._loadConfigFile(includePath, (err, includedConfig) => {
if (err) {
return nextIncludePath(err);
}
_.defaultsDeep(config, includedConfig);
return nextIncludePath(null);
});
},
err => {
this.configPaths = [ this.baseConfigPath, ...includePaths ];
return cb(err, config);
});
}
_resolveAtSpecs(config) {
return mapValuesDeep(
config,
value => {
if (_.isString(value) && '@' === value.charAt(0)) {
if (value.startsWith('@reference:')) {
const refPath = value.slice(11);
value = _.get(config, refPath, value);
} else if (value.startsWith('@environment:')) {
value = this._resolveEnvironmentVariable(value) || value;
}
}
return value;
}
);
}
};

View File

@ -1,67 +1,17 @@
/* jslint node: true */
'use strict';
const Config = require('./config.js').get;
const ConfigCache = require('./config_cache.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
// deps
const paths = require('path');
const async = require('async');
const paths = require('path');
exports.init = init;
exports.getConfigPath = getConfigPath;
exports.getFullConfig = getFullConfig;
exports.getConfigPath = getConfigPath;
function getConfigPath(filePath) {
// |filePath| is assumed to be in the config path if it's only a file name
if('.' === paths.dirname(filePath)) {
filePath = paths.join(Config().paths.config, filePath);
if (paths.isAbsolute(filePath)) {
return filePath;
}
return filePath;
}
function init(cb) {
// pre-cache menu.hjson and prompt.hjson + establish events
const changed = ( { fileName, fileRoot } ) => {
const reCachedPath = paths.join(fileRoot, fileName);
if(reCachedPath === getConfigPath(Config().general.menuFile)) {
Events.emit(Events.getSystemEvents().MenusChanged);
} else if(reCachedPath === getConfigPath(Config().general.promptFile)) {
Events.emit(Events.getSystemEvents().PromptsChanged);
}
};
const config = Config();
async.series(
[
function menu(callback) {
return ConfigCache.getConfigWithOptions(
{
filePath : getConfigPath(config.general.menuFile),
callback : changed,
},
callback
);
},
function prompt(callback) {
return ConfigCache.getConfigWithOptions(
{
filePath : getConfigPath(config.general.promptFile),
callback : changed,
},
callback
);
}
],
err => {
return cb(err);
}
);
}
function getFullConfig(filePath, cb) {
ConfigCache.getConfig(getConfigPath(filePath), (err, config) => {
return cb(err, config);
});
return paths.join(Config().paths.config, filePath);
}

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const conf = require('./config.js');
const conf = require('./config');
// deps
const sqlite3 = require('sqlite3');
@ -30,7 +30,8 @@ function getTransactionDatabase(db) {
}
function getDatabasePath(name) {
return paths.join(conf.config.paths.db, `${name}.sqlite3`);
const Config = conf.get();
return paths.join(Config.paths.db, `${name}.sqlite3`);
}
function getModDatabasePath(moduleInfo, suffix) {
@ -53,7 +54,8 @@ function getModDatabasePath(moduleInfo, suffix) {
(full.split('.').length > 1 && HOST_RE.test(full)),
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation');
return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`);
const Config = conf.get();
return paths.join(Config.paths.modsDb, `${full}.sqlite3`);
}
function loadDatabaseForMod(modInfo, cb) {
@ -162,9 +164,9 @@ const DB_INIT_TABLE = {
enableForeignKeys(dbs.user);
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user (
`CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY,
user_name VARCHAR NOT NULL,
user_name VARCHAR NOT NULL,
UNIQUE(user_name)
);`
);
@ -177,13 +179,13 @@ const DB_INIT_TABLE = {
prop_name VARCHAR NOT NULL,
prop_value VARCHAR,
UNIQUE(user_id, prop_name),
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);`
);
dbs.user.run(
`CREATE TABLE IF NOT EXISTS user_group_member (
group_name VARCHAR NOT NULL,
`CREATE TABLE IF NOT EXISTS user_group_member (
group_name VARCHAR NOT NULL,
user_id INTEGER NOT NULL,
UNIQUE(group_name, user_id)
);`
@ -227,9 +229,9 @@ const DB_INIT_TABLE = {
dbs.message.run(
`CREATE TABLE IF NOT EXISTS message (
message_id INTEGER PRIMARY KEY,
message_id INTEGER PRIMARY KEY,
area_tag VARCHAR NOT NULL,
message_uuid VARCHAR(36) NOT NULL,
message_uuid VARCHAR(36) NOT NULL,
reply_to_message_id INTEGER,
to_user_name VARCHAR NOT NULL,
from_user_name VARCHAR NOT NULL,
@ -237,7 +239,7 @@ const DB_INIT_TABLE = {
message, /* FTS @ message_fts */
modified_timestamp DATETIME NOT NULL,
view_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(message_uuid)
UNIQUE(message_uuid)
);`
);
@ -284,7 +286,7 @@ const DB_INIT_TABLE = {
meta_category INTEGER NOT NULL,
meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL,
UNIQUE(message_id, meta_category, meta_name, meta_value),
UNIQUE(message_id, meta_category, meta_name, meta_value),
FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE
);`
);
@ -342,7 +344,7 @@ const DB_INIT_TABLE = {
file_name, /* FTS @ file_fts */
storage_tag VARCHAR NOT NULL,
desc, /* FTS @ file_fts */
desc_long, /* FTS @ file_fts */
desc_long, /* FTS @ file_fts */
upload_timestamp DATETIME NOT NULL
);`
);
@ -395,7 +397,7 @@ const DB_INIT_TABLE = {
file_id INTEGER NOT NULL,
meta_name VARCHAR NOT NULL,
meta_value VARCHAR NOT NULL,
UNIQUE(file_id, meta_name, meta_value),
UNIQUE(file_id, meta_name, meta_value),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);`
);
@ -404,7 +406,7 @@ const DB_INIT_TABLE = {
`CREATE TABLE IF NOT EXISTS hash_tag (
hash_tag_id INTEGER PRIMARY KEY,
hash_tag VARCHAR NOT NULL,
UNIQUE(hash_tag)
);`
);
@ -413,7 +415,7 @@ const DB_INIT_TABLE = {
`CREATE TABLE IF NOT EXISTS file_hash_tag (
hash_tag_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
UNIQUE(hash_tag_id, file_id)
);`
);

View File

@ -2,7 +2,7 @@
'use strict';
// ENiGMA½
const conf = require('./config.js');
const Config = require('./config').get;
const logger = require('./logger.js');
const ServerModule = require('./server_module.js').ServerModule;
const clientConns = require('./client_connections.js');
@ -28,7 +28,7 @@ module.exports = class LoginServerModule extends ServerModule {
//
// Choose initial theme before we have user context
//
const preLoginTheme = _.get(conf.config, 'theme.preLogin');
const preLoginTheme = _.get(Config(), 'theme.preLogin');
if('*' === preLoginTheme) {
client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
} else {

View File

@ -24,10 +24,11 @@ function getMenuConfig(client, name, cb) {
async.waterfall(
[
function locateMenuConfig(callback) {
if(_.has(client.currentTheme, [ 'menus', name ])) {
const menuConfig = client.currentTheme.menus[name];
const menuConfig = _.get(client.currentTheme, [ 'menus', name ]);
if (menuConfig) {
return callback(null, menuConfig);
}
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
},
function locatePromptConfig(menuConfig, callback) {
@ -36,7 +37,7 @@ function getMenuConfig(client, name, cb) {
menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt];
return callback(null, menuConfig);
}
return callback(Error.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`));
return callback(Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`));
}
return callback(null, menuConfig);
}

View File

@ -60,8 +60,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
this.menuMethods = {
selectMessage : (formData, extraArgs, cb) => {
if(MciViewIds.allViews.msgList === formData.submitId) {
this.initialFocusIndex = formData.value.messageIndex ||
formData.value.message; // older deprecated arg name
// 'messageIndex' or older deprecated 'message' member
this.initialFocusIndex = _.get(formData, 'value.messageIndex', formData.value.message);
const modOpts = {
extraArgs : {
@ -108,8 +108,9 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
if(MciViewIds.allViews.msgList != formData.submitId) {
return cb(null);
}
const messageIndex = formData.value.messageIndex ||
formData.value.message; // older, deprecated arg name
// newer 'messageIndex' or older deprecated value
const messageIndex = _.get(formData, 'value.messageIndex', formData.value.message);
return this.promptDeleteMessageConfirm(messageIndex, cb);
},
deleteMessageYes : (formData, extraArgs, cb) => {

View File

@ -64,14 +64,14 @@ function getDefaultConfigPath() {
}
function getConfigPath() {
const baseConfigPath = argv.config ? argv.config : config.getDefaultPath();
const baseConfigPath = argv.config ? argv.config : config.Config.getDefaultPath();
return baseConfigPath + 'config.hjson';
}
function initConfig(cb) {
const configPath = getConfigPath();
config.init(configPath, { keepWsc : true, noWatch : true }, cb);
config.Config.create(configPath, { keepWsc : true, hotReload : false }, cb);
}
function initConfigAndDatabases(cb) {
@ -85,7 +85,7 @@ function initConfigAndDatabases(cb) {
},
function initArchiveUtil(callback) {
// ensure we init ArchiveUtil without events
require('../../core/archive_util').getInstance(true); // true=noWatch
require('../../core/archive_util').getInstance(false); // false=hotReload
return callback(null);
}
],

View File

@ -159,7 +159,7 @@ function askNewConfigQuestions(cb) {
},
function basic(callback) {
getAnswers(QUESTIONS.Basic, answers => {
const defaultConfig = require('../../core/config.js').getDefaultConfig();
const defaultConfig = require('../../core/config_default')();
// start by plopping in values we want directly from config.js
const template = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/config_template.in.hjson'), 'utf8'));
@ -227,26 +227,44 @@ function buildNewConfig() {
if(err) { return;
}
const bn = sanatizeFilename(config.general.boardName)
const boardName = sanatizeFilename(config.general.boardName)
.replace(/[^a-z0-9_-]/ig, '_')
.replace(/_+/g, '_')
.toLowerCase();
const menuFile = `${bn}-menu.hjson`;
copyFileSyncSilent(
paths.join(__dirname, '../../misc/menu_template.in.hjson'),
paths.join(__dirname, '../../config/', menuFile),
fs.constants.COPYFILE_EXCL
const includeFilesIn = [
'message_base.in.hjson',
'private_mail.in.hjson',
'login.in.hjson',
'new_user.in.hjson',
'doors.in.hjson',
'file_base.in.hjson',
];
let includeFiles = [];
includeFilesIn.forEach(incFile => {
const outName = `${boardName}-${incFile.replace('.in', '')}`;
includeFiles.push(outName);
copyFileSyncSilent(
paths.join(__dirname, '../../misc/menu_templates', incFile),
paths.join(__dirname, '../../config/menus', outName),
fs.constants.COPYFILE_EXCL
);
});
// We really only need includes to be replaced
const mainTemplate = fs.readFileSync(paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), 'utf8')
.replace(/%INCLUDE_FILES%/g, includeFiles.join('\n\t\t')); // cheesy, but works!
const menuFile = `${boardName}-main.hjson`;
fs.writeFileSync(
paths.join(__dirname, '../../config/menus', menuFile),
mainTemplate,
'utf8'
);
const promptFile = `${bn}-prompt.hjson`;
copyFileSyncSilent(
paths.join(__dirname, '../../misc/prompt_template.in.hjson'),
paths.join(__dirname, '../../config/', promptFile),
fs.constants.COPYFILE_EXCL
);
config.general.menuFile = menuFile;
config.general.promptFile = promptFile;
config.general.menuFile = paths.join(__dirname, '../../config/menus/', menuFile);
if(writeConfig(config, configPath)) {
console.info('Configuration generated');

View File

@ -9,7 +9,6 @@ module.exports = {
ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId }
ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson)
MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson)
PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson)
// User - includes { user, ...}
NewUser : 'codes.l33t.enigma.system.user_new', // { ... }

View File

@ -5,16 +5,16 @@ const Config = require('./config.js').get;
const art = require('./art.js');
const ansi = require('./ansi_term.js');
const Log = require('./logger.js').log;
const ConfigCache = require('./config_cache.js');
const getFullConfig = require('./config_util.js').getFullConfig;
const asset = require('./asset.js');
const ViewController = require('./view_controller.js').ViewController;
const Errors = require('./enig_error.js').Errors;
const ErrorReasons = require('./enig_error.js').ErrorReasons;
const Events = require('./events.js');
const AnsiPrep = require('./ansi_prep.js');
const UserProps = require('./user_property.js');
const ConfigLoader = require('./config_loader');
const { getConfigPath } = require('./config_util');
// deps
const fs = require('graceful-fs');
const paths = require('path');
@ -26,213 +26,266 @@ exports.getThemeArt = getThemeArt;
exports.getAvailableThemes = getAvailableThemes;
exports.getRandomTheme = getRandomTheme;
exports.setClientTheme = setClientTheme;
exports.initAvailableThemes = initAvailableThemes;
exports.displayPreparedArt = displayPreparedArt;
exports.displayThemeArt = displayThemeArt;
exports.displayThemedPause = displayThemedPause;
exports.displayThemedPrompt = displayThemedPrompt;
exports.displayThemedAsset = displayThemedAsset;
function refreshThemeHelpers(theme) {
//
// Create some handy helpers
//
theme.helpers = {
getPasswordChar : function() {
let pwChar = _.get(
theme,
'customization.defaults.passwordChar',
Config().theme.passwordChar
);
// global instance of ThemeManager; see ThemeManager.create()
let themeManagerInstance;
if(_.isString(pwChar)) {
pwChar = pwChar.substr(0, 1);
} else if(_.isNumber(pwChar)) {
pwChar = String.fromCharCode(pwChar);
exports.ThemeManager = class ThemeManager {
constructor() {
this.availableThemes = new Map();
}
static create(cb) {
themeManagerInstance = new ThemeManager();
themeManagerInstance.init(err => {
if (!err) {
themeManagerInstance.getAvailableThemes().forEach( (themeConfig, themeId) => {
const { name, author, group } = themeConfig.get().info;
Log.info(
{ themeId, themeName : name, author, group },
'Theme loaded'
);
});
}
return pwChar;
},
getDateFormat : function(style = 'short') {
const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY';
return _.get(theme, `customization.defaults.dateFormat.${style}`, format);
},
getTimeFormat : function(style = 'short') {
const format = Config().theme.timeFormat[style] || 'h:mm a';
return _.get(theme, `customization.defaults.timeFormat.${style}`, format);
},
getDateTimeFormat : function(style = 'short') {
const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a';
return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format);
}
};
}
function loadTheme(themeId, cb) {
const path = paths.join(Config().paths.themes, themeId, 'theme.hjson');
const changed = ( { fileName, fileRoot } ) => {
const reCachedPath = paths.join(fileRoot, fileName);
if(reCachedPath === path) {
reloadTheme(themeId);
}
};
const getOpts = {
filePath : path,
forceReCache : true,
callback : changed,
};
ConfigCache.getConfigWithOptions(getOpts, (err, theme) => {
if(err) {
return cb(err);
}
});
}
getAvailableThemes() {
return this.availableThemes;
}
init(cb) {
this.menuConfig = new ConfigLoader({
onReload : err => {
if (!err) {
// menu.hjson/includes have changed; this could affect
// all themes, so they must be reloaded
Events.emit(Events.getSystemEvents().MenusChanged);
this._reloadAllThemes();
}
},
});
this.menuConfig.init(getConfigPath(Config().general.menuFile), err => {
if (err) {
return cb(err);
}
return this._loadThemes(cb);
});
}
_loadThemes(cb) {
const themeDir = Config().paths.themes;
fs.readdir(themeDir, (err, files) => {
if (err) {
return cb(err);
}
async.filter(files, (filename, nextFilename) => {
const fullPath = paths.join(themeDir, filename);
fs.stat(fullPath, (err, stats) => {
if (err) {
return nextFilename(err);
}
return nextFilename(null, stats.isDirectory());
});
},
(err, themeIds) => {
if (err) {
return cb(err);
}
async.each(themeIds, (themeId, nextThemeId) => {
return this._loadTheme(themeId, nextThemeId);
},
err => {
return cb(err);
});
});
});
}
_loadTheme(themeId, cb) {
const themeConfig = new ConfigLoader({
onReload : err => {
if (!err) {
// this particular theme has changed
this._themeLoaded(themeId, themeConfig, err => {
if (!err) {
Events.emit(
Events.getSystemEvents().ThemeChanged,
{ themeId }
);
}
});
}
}
});
const themeConfigPath = paths.join(Config().paths.themes, themeId, 'theme.hjson');
themeConfig.init(themeConfigPath, err => {
if (err) {
return cb(err);
}
this._themeLoaded(themeId, themeConfig);
return cb(null);
});
}
_themeLoaded(themeId, themeConfig) {
const theme = themeConfig.get();
// do some basic validation
// :TODO: schema validation here
if(!_.isObject(theme.info) ||
!_.isString(theme.info.name) ||
!_.isString(theme.info.author))
{
return cb(Errors.Invalid('Invalid or missing "info" section'));
return Log.warn({ themeId }, 'Theme contains invalid or missing "info" section');
}
if(false === _.get(theme, 'info.enabled')) {
return cb(Errors.General('Theme is not enabled', ErrorReasons.ErrNotEnabled));
Log.info({ themeId }, 'Theme is disabled');
return this.availableThemes.delete(themeId);
}
refreshThemeHelpers(theme);
// merge menu.hjson+theme.hjson/etc. to the final usable theme
this._finalizeTheme(themeId, themeConfig);
return cb(null, theme, path);
});
}
// Theme is valid and enabled; update it in available themes
this.availableThemes.set(themeId, themeConfig);
const availableThemes = new Map();
const IMMUTABLE_MCI_PROPERTIES = [
'maxLength', 'argName', 'submit', 'validate'
];
function getMergedTheme(menuConfig, promptConfig, theme) {
assert(_.isObject(menuConfig));
assert(_.isObject(theme));
// :TODO: merge in defaults (customization.defaults{} )
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
//
// Create a *clone* of menuConfig (menu.hjson) then bring in
// promptConfig (prompt.hjson)
//
const mergedTheme = _.cloneDeep(menuConfig);
if(_.isObject(promptConfig.prompts)) {
mergedTheme.prompts = _.cloneDeep(promptConfig.prompts);
Events.emit(
Events.getSystemEvents().ThemeChanged,
{ themeId }
);
}
//
// Add in data we won't be altering directly from the theme
//
mergedTheme.info = theme.info;
mergedTheme.helpers = theme.helpers;
mergedTheme.achievements = _.get(theme, 'customization.achievements');
_finalizeTheme(themeId, themeConfig) {
// These TODOs are left over from the old system - decide what/if to do with them:
// :TODO: merge in defaults (customization.defaults{} )
// :TODO: apply generic stuff, e.g. "VM" (vs "VM1")
//
// merge customizer to disallow immutable MCI properties
//
const mciCustomizer = function(objVal, srcVal, key) {
return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal;
};
// start out with menu.hjson
const mergedTheme = _.cloneDeep(this.menuConfig.get());
function getFormKeys(fromObj) {
// remove all non-numbers
return _.remove(_.keys(fromObj), k => !isNaN(k));
}
const theme = themeConfig.get();
function mergeMciProperties(dest, src) {
Object.keys(src).forEach(mci => {
if(dest[mci]) {
_.mergeWith(dest[mci], src[mci], mciCustomizer);
} else {
// theme contains MCI not in menu; bring in as-is
dest[mci] = src[mci];
}
});
}
// some data brought directly over
mergedTheme.info = Object.assign({}, theme.info, { themeId });
mergedTheme.achievements = _.get(theme, 'customization.achievements');
function applyThemeMciBlock(dest, src, formKey) {
if(_.isObject(src.mci)) {
mergeMciProperties(dest, src.mci);
} else {
if(_.has(src, [ formKey, 'mci' ])) {
mergeMciProperties(dest, src[formKey].mci);
}
}
}
// Create some helpers for this theme
this._setThemeHelpers(mergedTheme);
//
// menu.hjson can have a couple different structures:
// 1) Explicit declaration of expected MCI code(s) under 'form:<id>' before a 'mci' block
// (this allows multiple layout types defined by one menu for example)
//
// 2) Non-explicit declaration: 'mci' directly under 'form:<id>'
//
// theme.hjson has it's own mix:
// 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms)
//
// 2) Non-explicit: 'mci' directly under an entry
//
// Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up
// with menu.hjson in #1.
//
// * When theming an explicit menu.hjson entry (1), we will use a matching explicit
// entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM"
// and fall back to generic if a match is not found.
//
// * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming
// there is a generic 'mci' block.
//
function applyToForm(form, menuTheme, formKey) {
if(_.isObject(form.mci)) {
// non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID
applyThemeMciBlock(form.mci, menuTheme, formKey);
} else {
// remove anything not uppercase
const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase());
// merge customizer to disallow immutable MCI properties
const ImmutableMCIProperties = [
'maxLength', 'argName', 'submit', 'validate'
];
menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) {
let applyFrom;
if(_.has(menuTheme, [ mciKey, 'mci' ])) {
applyFrom = menuTheme[mciKey];
const mciCustomizer = (objVal, srcVal, key) => {
return ImmutableMCIProperties.indexOf(key) > -1 ? objVal : srcVal;
};
const getFormKeys = (obj) => {
// remove all non-numbers
return _.remove(Object.keys(obj), k => !isNaN(k));
};
const mergeMciProperties = (dst, src) => {
Object.keys(src).forEach(mci => {
if (dst[mci]) {
_.mergeWith(dst[mci], src[mci], mciCustomizer);
} else {
applyFrom = menuTheme;
// theme contains a MCI that's not found in menu
dst[mci] = src[mci];
}
applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey);
});
}
}
};
[ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) {
_.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) {
let createdFormSection = false;
const mergedThemeMenu = mergedTheme[sectionName][menuName];
const applyThemeMciBlock = (dst, src, formKey) => {
if(_.isObject(src.mci)) {
mergeMciProperties(dst, src.mci);
} else if (_.has(src, [ formKey, 'mci' ])) {
mergeMciProperties(dst, src[formKey].mci);
}
};
if(_.has(theme, [ 'customization', sectionName, menuName ])) {
const menuTheme = theme.customization[sectionName][menuName];
//
// menu.hjson can have a couple different structures:
// 1) Explicit declaration of expected MCI code(s) under 'form:<id>' before a 'mci' block
// (this allows multiple layout types defined by one menu for example)
//
// 2) Non-explicit declaration: 'mci' directly under 'form:<id>'
//
// theme.hjson has it's own mix:
// 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms)
//
// 2) Non-explicit: 'mci' directly under an entry
//
// Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up
// with menu.hjson in #1.
//
// * When theming an explicit menu.hjson entry (1), we will use a matching explicit
// entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM"
// and fall back to generic if a match is not found.
//
// * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming
// there is a generic 'mci' block.
//
const applyToForm = (form, menuTheme, formKey) => {
if (_.isObject(form.mci)) {
// non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID
applyThemeMciBlock(form.mci, menuTheme, formKey);
} else {
// remove anything not uppercase
const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase());
// config block is direct assign/overwrite
// :TODO: should probably be _.merge()
if(menuTheme.config) {
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
}
menuMciCodeKeys.forEach(mciKey => {
const src = _.has(menuTheme, [ mciKey, 'mci' ]) ?
menuTheme[mciKey] :
menuTheme;
if('menus' === sectionName) {
if(_.isObject(mergedThemeMenu.form)) {
getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) {
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
});
} else {
if(_.isObject(menuTheme.mci)) {
applyThemeMciBlock(form[mciKey].mci, src, formKey);
});
}
};
[ 'menus', 'prompts'].forEach(sectionName => {
if (!_.isObject(mergedTheme[sectionName])) {
return Log.error({sectionName}, 'Merged theme is missing section');
}
Object.keys(mergedTheme[sectionName]).forEach(entryName => {
let createdFormSection = false;
const mergedThemeMenu = mergedTheme[sectionName][entryName];
const menuTheme = _.get(theme, [ 'customization', sectionName, entryName ]);
if (menuTheme) {
if (menuTheme.config) {
// :TODO: should this be _.merge() ?
mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config);
}
if('menus' === sectionName) {
if(_.isObject(mergedThemeMenu.form)) {
getFormKeys(mergedThemeMenu.form).forEach(formKey => {
applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey);
});
} else if(_.isObject(menuTheme.mci)) {
//
// Not specified at menu level means we apply anything from the
// theme to form.0.mci{}
@ -241,158 +294,84 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
mergeMciProperties(mergedThemeMenu.form[0], menuTheme);
createdFormSection = true;
}
} else if('prompts' === sectionName) {
// no 'form' or form keys for prompts -- direct to mci
applyToForm(mergedThemeMenu, menuTheme);
}
} else if('prompts' === sectionName) {
// no 'form' or form keys for prompts -- direct to mci
applyToForm(mergedThemeMenu, menuTheme);
}
}
//
// Finished merging for this menu/prompt
//
// If the following conditions are true, set runtime.autoNext to true:
// * This is a menu
// * There is/was no explicit 'form' section
// * There is no 'prompt' specified
//
if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) &&
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
{
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
}
//
// Finished merging for this menu/prompt
//
// If the following conditions are true, set runtime.autoNext to true:
// * This is a menu
// * There is/was no explicit 'form' section
// * There is no 'prompt' specified
//
if('menus' === sectionName &&
!_.isString(mergedThemeMenu.prompt) &&
(createdFormSection || !_.isObject(mergedThemeMenu.form)))
{
mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } );
}
});
});
});
themeConfig.current = mergedTheme;
}
return mergedTheme;
}
_setThemeHelpers(theme) {
theme.helpers = {
getPasswordChar : function() {
let pwChar = _.get(
theme,
'customization.defaults.passwordChar',
Config().theme.passwordChar
);
function reloadTheme(themeId) {
const config = Config();
async.waterfall(
[
function loadMenuConfig(callback) {
getFullConfig(config.general.menuFile, (err, menuConfig) => {
return callback(err, menuConfig);
});
if(_.isString(pwChar)) {
pwChar = pwChar.substr(0, 1);
} else if(_.isNumber(pwChar)) {
pwChar = String.fromCharCode(pwChar);
}
return pwChar;
},
function loadPromptConfig(menuConfig, callback) {
getFullConfig(config.general.promptFile, (err, promptConfig) => {
return callback(err, menuConfig, promptConfig);
});
getDateFormat : function(style = 'short') {
const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY';
return _.get(theme, `customization.defaults.dateFormat.${style}`, format);
},
function loadIt(menuConfig, promptConfig, callback) {
loadTheme(themeId, (err, theme) => {
if(err) {
if(ErrorReasons.NotEnabled !== err.reasonCode) {
Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme');
return;
}
return callback(err);
}
Object.assign(theme.info, { themeId } );
availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme));
Events.emit(
Events.getSystemEvents().ThemeChanged,
{ themeId }
);
return callback(null, theme);
});
getTimeFormat : function(style = 'short') {
const format = Config().theme.timeFormat[style] || 'h:mm a';
return _.get(theme, `customization.defaults.timeFormat.${style}`, format);
},
getDateTimeFormat : function(style = 'short') {
const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a';
return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format);
}
],
(err, theme) => {
if(err) {
Log.warn( { themeId, error : err.message }, 'Failed to reload theme');
} else {
Log.debug( { info : theme.info }, 'Theme recached' );
}
}
);
}
};
}
function reloadAllThemes()
{
async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId));
}
function initAvailableThemes(cb) {
const config = Config();
async.waterfall(
[
function loadMenuConfig(callback) {
getFullConfig(config.general.menuFile, (err, menuConfig) => {
return callback(err, menuConfig);
});
},
function loadPromptConfig(menuConfig, callback) {
getFullConfig(config.general.promptFile, (err, promptConfig) => {
return callback(err, menuConfig, promptConfig);
});
},
function getThemeDirectories(menuConfig, promptConfig, callback) {
fs.readdir(config.paths.themes, (err, files) => {
if(err) {
return callback(err);
}
return callback(
null,
menuConfig,
promptConfig,
files.filter( f => {
// sync normally not allowed -- initAvailableThemes() is a startup-only method, however
return fs.statSync(paths.join(config.paths.themes, f)).isDirectory();
})
);
});
},
function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) {
async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID
loadTheme(themeId, (err, theme) => {
if(err) {
if(ErrorReasons.NotEnabled !== err.reasonCode) {
Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme');
}
return nextThemeDir(null); // try next
}
Object.assign(theme.info, { themeId } );
availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme));
return nextThemeDir(null);
});
}, err => {
return callback(err);
});
},
function initEvents(callback) {
Events.on(Events.getSystemEvents().MenusChanged, () => {
return reloadAllThemes();
});
Events.on(Events.getSystemEvents().PromptsChanged, () => {
return reloadAllThemes();
});
return callback(null);
}
],
err => {
return cb(err, availableThemes.size);
}
);
}
_reloadAllThemes() {
async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => {
this._loadTheme(themeId, err => {
if (!err) {
Log.info({ themeId }, 'Theme reloaded');
}
return nextThemeId(null); // always proceed
});
});
}
};
function getAvailableThemes() {
return availableThemes;
return themeManagerInstance.getAvailableThemes();
}
function getRandomTheme() {
if(availableThemes.size > 0) {
const themeIds = [ ...availableThemes.keys() ];
const avail = getAvailableThemes();
if(avail.size > 0) {
const themeIds = [ ...avail.keys() ];
return themeIds[Math.floor(Math.random() * themeIds.length)];
}
}
@ -433,10 +412,10 @@ function getThemeArt(options, cb) {
const config = Config();
if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) {
options.themeId = options.client.user.properties[UserProps.ThemeId];
} else {
options.themeId = config.theme.default;
}
options.themeId = options.themeId || config.theme.default;
// :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ...
// :TODO: Some of these options should only be set if not provided!
options.asAnsi = true; // always convert to ANSI

View File

@ -171,7 +171,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
},
function prepareAvailableThemes(callback) {
self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => {
const theme = entry[1];
const theme = entry[1].get();
return {
themeId : theme.info.themeId,
name : theme.info.name,

View File

@ -13,10 +13,10 @@
- Configuration
- [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %})
- [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %})
- [Configuration Files]({{ site.baseurl }}{% link configuration/config-files.md %})
- [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %})
- [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %})
- [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %})
- [Prompts]({{ site.baseurl }}{% link configuration/prompt-hjson.md %})
- [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %})
- [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %})
- [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %})

View File

@ -38,7 +38,7 @@ The `customization` block in is itself broken up into major parts:
|-------------|---------------------------------------------------|
| `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. |
| `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. |
| `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. |
| `prompts` | Similar to `menus`, this section themes `prompts`. |
#### Defaults
| Item | Description |
@ -46,7 +46,7 @@ The `customization` block in is itself broken up into major parts:
| `passwordChar` | Character to display in password fields. Defaults to `*` |
| `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. |
| `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. |
| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. |
| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. |
Example:
```hjson

View File

@ -74,7 +74,7 @@ For `list` commands, the `entryMatch` key must be provided. This key should prov
```
## Archive Formats
Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config.js`).
Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config_default.js`).
### Example Archive Format Configuration
```

View File

@ -2,9 +2,7 @@
layout: page
title: Colour Codes
---
ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used in [`config.hjson`](config-hjson.md),
[`prompt.hjson`](prompt-hjson.md), [`menu.hjson`](menu-hjson.md), and can also be used in places like the oneliner, rumour mod,
full screen editor etc.
ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used throughout your configuration, and can also be used in places like onelinerz, rumourz, full screen editor etc.
## Usage
When ENiGMA½ encounters colour codes in strings, they'll be processed in order and combined where possible.
@ -21,4 +19,5 @@ For example:
:warning: Colour codes |24 to |31 are considered "blinking" or "iCE" colour codes. On terminals that support them they'll
be shown as the correct colours - for terminals that don't, or are that are set to "blinking" mode - they'll blink!
![Regegade style colour codes](../assets/images/colour-codes.png "Colour Codes")
![Renegade style colour codes](../assets/images/colour-codes.png "Colour Codes")

View File

@ -0,0 +1,124 @@
---
layout: page
title: Configuration Files
---
## General Information
ENiGMA½ configuration files such as the [system config](config-hjson.md), [menus](menu-hjson.md) and [themes](../art/themes.md) are formatted in the [HJSON format](hjson.md).
## Hot-Reload
Nearly all of ENiGMA½'s configuration can be hot-reloaded. That is, a live system can have it's configuration modified and it will be loaded in place.
## Common Directives
### Includes
Most configuration files offer an `includes` directive that allows users to break up large configuration files into smaller and organized parts. For example, consider a system with many menus/screens. Instead of a single `menu.hjson`, the SysOp may break this into `message-base.hjson`, `file-base.hjson`, etc.
The `includes` directive may be used the top-level scope of a configuration file:
```hjson
// menu.hjson
{
includes: [
message-base.hjson
file-base.hjson
]
menus: {
someOtherMenu: {
// ...
}
}
}
```
```hjson
// message-base.hjson
{
menus: {
someMessageMenu: {
// ...
}
}
}
```
### References
Often times in a configuration you will find that you're repeating yourself quite a bit. ENiGMA½ provides an `@reference` that can help with this in the form of `@reference:dot.path.to.section`.
Consider `actionKeys` in a menu. Often times you may show a screen and the user presses `Q` or `ESC` to fall back to the previous. Instead of repeating this in many menus, a generic block can be referenced:
```hjson
{
// note that 'recycle' here is arbitrary;
// only 'menus' and 'prompts' is reserved at this level.
recycle: {
prevMenu: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
menus: {
someMenu: {
form: {
0: {
actionKeys: @reference:recycle.prevMenu
}
}
}
}
}
```
:information_source: An unresolved `@reference` will be left intact.
### Environment Variables
Especially in a container environment such as [Docker](/docs/installation/docker.md), environment variable access in configuration files can become very handy. ENiGMA½ provides a flexible way to access variables using the `@environment` directive. The most basic form of `@environment:VAR_NAME` produces a string value. Additionally a `:type` suffix can be supplied to coerece the value to a particular type. Variables pointing to a comma separated list can be turned to arrays using an additional `:array` suffix.
Below is a table of the various forms:
| Form | Variable Value | Produces |
|------|----------------|----------|
| `@environment:SOME_VAR` | "Foo" | `"Foo"` (without quotes) |
| `@environment:SOME_VAR` | "123" | `"123"` (without quotes) |
| `@environment:SOME_VAR:string` | "Bar" | `"Bar"` (without quotes) |
| `@environment:SOME_VAR:string:array` | "Foo,Bar" | `[ 'Foo', 'Bar' ]` |
| `@environment:SOME_VAR:boolean` | "1" | `true` |
| `@environment:SOME_VAR:boolean` | "True" | `true` |
| `@environment:SOME_VAR:boolean` | "false" | `false` |
| `@environment:SOME_VAR:boolean` | "cat" | `false` |
| `@environment:SOME_VAR:boolean:array` | "True,false,TRUE" | `[ true, false, true ]` |
| `@environment:SOME_VAR:number` | "123" | `123` |
| `@environment:SOME_VAR:number:array` | "123,456" | `[ 123, 456 ]` |
| `@environment:SOME_VAR:number` | "kitten" | (invalid) |
| `@environment:SOME_VAR:object` | '{"a":"b"}' | `{ 'a' : 'b' }` |
| `@environment:SOME_VAR:object:array` | '{"a":"b"},{"c":"d"}' | `[ { 'a' : 'b' }, { 'c' : 'd' } ]` |
| `@environment:SOME_VAR:timestamp` | "2020-01-05" | A [moment](https://momentjs.com/) object representing 2020-01-05 |
| `@environment:SOME_VAR:timestamp:array` | "2020-01-05,2016-05-16T01:15:37'" | An array of [moment](https://momentjs.com/) objects representing 2020-01-05 and 2016-05-16T01:15:37 |
:information_source: `bool` may be used as an alias to `boolean`.
:information_source: `timestamp` values can be in any form that [moment can parse](https://momentjs.com/docs/#/parsing/).
:information_source: An unresolved or invalid `@environment` will be left intact.
Consider the following fragment:
```hjson
{
foo: {
bar: @environment:BAR_VAR:number
}
}
```
If the environment has `BAR_VAR=1337`, this would produce:
```hjson
{
foo: {
bar: 1337
}
}
```
## See Also
* [System Configuration](config-hjson.md)
* [Menu Configuration](menu-hjson.md)
* [The HJSON Format](hjson.md)

View File

@ -3,7 +3,7 @@ layout: page
title: System Configuration
---
## System Configuration
The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config.js` may be overridden by simply providing the object members you wish replace.
The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config_default.js` may be overridden by simply providing the object members you wish replace.
See also [HJSON General Information](hjson.md) for more information on the HJSON format.
@ -16,7 +16,7 @@ Your initial configuration skeleton should be created using the `oputil.js` comm
You will be asked a series of questions to create an initial configuration.
### Overriding Defaults
The file `core/config.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows:
The file `core/config_default.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows:
```javascript
general : {
boardName : 'Another Fine ENiGMA½ System'
@ -32,7 +32,7 @@ general: {
(Note the very slightly [HJSON](hjson.md) different syntax. **You can use standard JSON if you wish!**)
While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)!
While not everything that is available in your `config.hjson` file can be found defaulted in `core/config_default.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config_default.js)!
### Configuration Sections
Below is a list of various configuration sections. There are many more, but this should get you started:

View File

@ -10,5 +10,5 @@ Your initial configuration skeleton can be created using the `oputil.js` command
./oputil.js config new
```
You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/<bbsName>-menu.hjson` and `config/<bbsName>-prompt.hjson` files (where `<bbsName>` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) and [Prompt HJSON](prompt-hjson.md) for more information.
You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce menu files under `config/menus/`. See [Menu HJSON](menu-hjson.md) for more information.

View File

@ -2,14 +2,16 @@
layout: page
title: Directory Structure
---
All paths mentioned here are relative to the ENiGMA½ checkout directory.
All paths mentioned here are relative to the ENiGMA½ checkout directory.
| Directory | Description |
|---------------------|-----------------------------------------------------------------------------------------------------------|
| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.md %}).
| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes]({{ site.baseurl }}{% link art/themes.md %}).
| `/config` | config.hjson, [menu.hjson]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) and prompt.hjson storage. Also default path for SSL certs and public/private keys
| `/db` | All ENiGMA½ databases in Sqlite3 format
| `/config` | [config.hjson](config-hjson.md) system configuration.
| `/config/menus` | [menu.hjson](menu-hjson.md)storage.
| `/config/security` | D path for SSL certs and public/private keys.
| `/db` | All ENiGMA½ databases in Sqlite3 format.
| `/docs` | These docs ;-)
| `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %})
| `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %})

View File

@ -28,12 +28,12 @@ For protocols of type `external` the following members may be defined:
* `sendCmd`: Required for protocols that can send (allow user downloads); The command/binary to execute.
* `sendArgs`: Required if using `sendCmd`; An array of arguments. A placeholder of `{fileListPath}` may be used to supply a path to a **file containing** a list of files to send, or `{filePaths}` to supply *1:n* individual file paths to send.
* `recvCmd`: Required for protocols that can receive (allow user uploads); The command/binary to execute.
* `recvArgs`: Required if using `recvCmd` and supporting **batch** uploads; An array of arguments. A placeholder of `{uploadDir}` may be used to supply the system provided upload directory. If `{uploadDir}` is not present, the system expects uploaded files to be placed in CWD which will be set to the upload directory.
* `recvArgs`: Required if using `recvCmd` and supporting **batch** uploads; An array of arguments. A placeholder of `{uploadDir}` may be used to supply the system provided upload directory. If `{uploadDir}` is not present, the system expects uploaded files to be placed in CWD which will be set to the upload directory.
* `recvArgsNonBatch`: Required if using `recvCmd` and supporting non-batch (single file) uploads; A placeholder of `{fileName}` may be supplied to indicate to the protocol what the uploaded file should be named (this will be collected from the user before the upload starts).
* `escapeTelnet`: Optional; If set to `true`, escape all internal Telnet related codes such as IAC's. This option is required for external protocol handlers such as `sz` and `rz` that do not escape themselves.
### Adding Your Own
Take a look a the example below as well as [core/config.js](/core/config.js).
Take a look a the example below as well as [core/config_default.js](/core/config_default.js).
#### Example File Transfer Protocol Configuration
```

View File

@ -3,7 +3,7 @@ layout: page
title: HJSON Config Files
---
## JSON for Humans!
HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), [Prompts](prompt-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans!
HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans!
For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more:
@ -18,7 +18,6 @@ Through the documentation, some terms regarding HJSON and configuration files wi
* `config.hjson`: Refers to `/path/to/enigma-bbs/config/config.hjson`. See [System Configuration](config-hjson.md).
* `menu.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-menu.hjson`. See [Menus](menu-hjson.md).
* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-prompt.hjson`. See [Prompts](prompt-hjson.md).
* Configuration *key*: Elements in HJSON are name-value pairs where the name is the *key*. For example, provided `foo: bar`, `foo` is the key.
* Configuration *section* or *block* (also commonly called an "Object" in code): This is referring to a section in a HJSON file that starts with a *key*. For example:
```hjson

View File

@ -3,7 +3,7 @@ layout: page
title: Menu HSJON
---
## Menu HJSON
The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information.
The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information.
Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to:
@ -13,6 +13,8 @@ Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each en
Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system.
:information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this.
## Common Menu Entry Members
Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars.
@ -21,7 +23,7 @@ Below is a table of **common** menu entry members. These members apply to most e
| `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. |
| `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). |
| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. |
| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. |
| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in the `prompts` section. See **Prompts** for more information. |
| `submit` | Defines a submit handler when using `prompt`.
| `form` | An object defining one or more *forms* available on this menu. |
| `module` | Sets the module name to use for this menu. See **Menu Modules** below. |
@ -183,6 +185,11 @@ In the above entry, you'll notice `form`. This defines a form(s) object. In this
* The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`).
* Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu).
## Prompts
Prompts are found in the `prompts` section of menu files. Prompts allow for quick user input and shorthand form requirements for menus. Additionally, prompts are often used for for multiple menus. Consider a pause prompt or menu command input for example.
TODO: additional prompt docs
## ACS Checks
Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax.

View File

@ -1,8 +0,0 @@
---
layout: page
title: prompt.hjson
---
:zap: This page is to describe general information the `prompt.hjson` file. It
needs fleshing out, please submit a PR if you'd like to help!
See [HJSON General Information](hjson.md) for more information.

View File

@ -9,5 +9,5 @@ The built in `file_transfer_protocol_select` module provides a way to select a l
### Theming
The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) (the protocol list):
* `name`: The name of the protocol. Each entry is +op defined in `config.hjson` with defaults found in `config.js`. Note that the standard `{text}` field also contains this value.
* `name`: The name of the protocol. Each entry is +op defined in `config.hjson` with defaults found in `config_default.js`. Note that the standard `{text}` field also contains this value.

View File

@ -17,7 +17,7 @@ Entries available under `config.loginServers.ssh`:
| `enabled` | :+1: | Set to `true` to enable the SSH server. |
| `port` | :-1: | Override the default port of `8443`. |
| `address` | :-1: | Sets an explicit bind address. |
| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`.
| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config_default.js`.
| `traceConnections` | :-1: | Set to `true` to enable full trace-level information on SSH connections.
### Example Configuration

View File

@ -369,7 +369,7 @@
storageTags: {
//
// Example storage tag: "super_l33t_warez":
// super_l33t_warez: "/path/to/super/l33t/warez"
// super_l33t_warez: /path/to/super/l33t/warez
//
}
@ -377,10 +377,10 @@
//
// Example area with the areaTag of "an_example_area":
// an_example_area: {
// name: "Example File Area"
// desc: "It's just an example, yo!"
// name: Example File Area
// desc: It's just an example, yo!
// storageTags: [
// "super_l33t_warez"
// super_l33t_warez
// ]
// }
//

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
{
menus: {
doorsMainMenu: {
desc: Doors Menu
art: DOORMNU
prompt: menuCommand
config: {
interrupt: realtime
}
submit: [
{
value: { command: "G" }
action: @menu:fullLogoffSequence
}
{
value: { command: "Q" }
action: @systemMethod:prevMenu
}
//
// The system supports many ways of launching doors including
// modules for DoorParty!, BBSLink, etc.
//
// Below are some examples. See the documentation for more info.
//
{
value: { command: "ABRACADABRA" }
action: @menu:doorAbracadabraExample
}
{
value: { command: "TWBBSLINK" }
action: @menu:doorTradeWars2002BBSLinkExample
}
{
value: { command: "DP" }
action: @menu:doorPartyExample
}
{
value: { command: "CN" }
action: @menu:doorCombatNetExample
}
{
value: { command: "EXODUS" }
action: @menu:doorExodusCataclysm
}
]
}
//
// Local Door Example via abracadabra module
//
// This example assumes launch_door.sh (which is passed args)
// launches the door.
//
doorAbracadabraExample: {
desc: Abracadabra Example
module: abracadabra
config: {
name: Example Door
dropFileType: DORINFO
cmd: /home/enigma/DOS/scripts/launch_door.sh
args: [
"{node}",
"{dropFile}",
"{srvPort}",
],
nodeMax: 1
tooManyArt: DOORMANY
io: socket
}
}
//
// BBSLink Example (TradeWars 2000)
//
// Register @ https://bbslink.net/
//
doorTradeWars2002BBSLinkExample: {
desc: Playing TW 2002 (BBSLink)
module: bbs_link
config: {
sysCode: XXXXXXXX
authCode: XXXXXXXX
schemeCode: XXXXXXXX
door: tw
}
}
//
// DoorParty! Example
//
// Register @ http://throwbackbbs.com/
//
doorPartyExample: {
desc: Using DoorParty!
module: door_party
config: {
username: XXXXXXXX
password: XXXXXXXX
bbsTag: XX
}
}
//
// CombatNet Example
//
// Register @ http://combatnet.us/
//
doorCombatNetExample: {
desc: Using CombatNet
module: combatnet
config: {
bbsTag: CBNxxx
password: XXXXXXXXX
}
}
//
// Exodus Example (cataclysm)
// Register @ https://oddnetwork.org/exodus/
//
doorExodusCataclysm: {
desc: Cataclysm
module: exodus
config: {
rejectUnauthorized: false
board: XXX
key: XXXXXXXX
door: cataclysm
}
}
}
}

View File

@ -0,0 +1,918 @@
{
menus: {
fileBaseMainMenu: {
desc: File Base
art: FMENU
prompt: fileMenuCommand
config: {
interrupt: realtime
}
submit: [
{
value: { menuOption: "L" }
action: @menu:fileBaseListEntries
}
{
value: { menuOption: "B" }
action: @menu:fileBaseBrowseByAreaSelect
}
{
value: { menuOption: "F" }
action: @menu:fileBaseFilterEditor
}
{
value: { menuOption: "Q" }
action: @systemMethod:prevMenu
}
{
value: { menuOption: "G" }
action: @menu:fullLogoffSequence
}
{
value: { menuOption: "D" }
action: @menu:fileBaseDownloadManager
}
{
value: { menuOption: "W" }
action: @menu:fileBaseWebDownloadManager
}
{
value: { menuOption: "U" }
action: @menu:fileBaseUploadFiles
}
{
value: { menuOption: "S" }
action: @menu:fileBaseSearch
}
{
value: { menuOption: "P" }
action: @menu:fileBaseSetNewScanDate
}
{
value: { menuOption: "E" }
action: @menu:fileBaseExportListFilter
}
]
}
fileBaseListEntries: {
module: file_area_list
desc: Browsing Files
config: {
art: {
browse: FBRWSE
details: FDETAIL
detailsGeneral: FDETGEN
detailsNfo: FDETNFO
detailsFileList: FDETLST
help: FBHELP
}
}
form: {
0: {
mci: {
MT1: {
mode: preview
}
HM2: {
focus: true
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit"
]
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFile
}
{
value: { navSelect: 1 }
action: @method:nextFile
}
{
value: { navSelect: 2 }
action: @method:viewDetails
}
{
value: { navSelect: 3 }
action: @method:toggleQueue
}
{
value: { navSelect: 4 }
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
value: { navSelect: 5 }
action: @menu:fileBaseFilterEditor
}
{
value: { navSelect: 6 }
action: @method:displayHelp
}
{
value: { navSelect: 7 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "w", "shift + w" ]
action: @method:showWebDownloadLink
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "t", "shift + t" ]
action: @method:toggleQueue
}
{
keys: [ "f", "shift + f" ]
action: @menu:fileBaseFilterEditor
}
{
keys: [ "v", "shift + v" ]
action: @method:viewDetails
}
{
keys: [ "r", "shift + r" ]
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
keys: [ "?" ]
action: @method:displayHelp
}
]
}
1: {
mci: {
HM1: {
focus: true
submit: true
argName: navSelect
items: [
"general", "nfo/readme", "file listing"
]
}
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @method:detailsQuit
}
]
}
2: {
// details - general
mci: {}
}
3: {
// details - nfo/readme
mci: {
MT1: {
mode: preview
}
}
}
4: {
// details - file listing
mci: {
VM1: {
}
}
}
}
}
fileBaseBrowseByAreaSelect: {
desc: Browsing File Areas
module: file_base_area_select
art: FAREASEL
form: {
0: {
mci: {
VM1: {
focus: true
argName: areaTag
}
}
submit: {
*: [
{
value: { areaTag: null }
action: @method:selectArea
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
fileBaseFilterEditor: {
desc: File Filter Editor
module: file_area_filter_edit
art: FFILEDT
form: {
0: {
mci: {
ET1: {
argName: searchTerms
}
ET2: {
maxLength: 64
argName: tags
}
SM3: {
maxLength: 64
argName: areaIndex
}
SM4: {
items: [
"upload date",
"uploaded by",
"downloads",
"rating",
"estimated year",
"size",
]
argName: sortByIndex
}
SM5: {
items: [
"decending",
"ascending"
]
argName: orderByIndex
}
ET6: {
maxLength: 64
argName: name
validate: @systemMethod:validateNonEmpty
}
HM7: {
focus: true
items: [
"prev", "next", "make active", "save", "new", "delete"
]
argName: navSelect
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFilter
}
{
value: { navSelect: 1 }
action: @method:nextFilter
}
{
value: { navSelect: 2 }
action: @method:makeFilterActive
}
{
value: { navSelect: 3 }
action: @method:saveFilter
}
{
value: { navSelect: 4 }
action: @method:newFilter
}
{
value: { navSelect: 5 }
action: @method:deleteFilter
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseDownloadManager: {
desc: Download Manager
module: file_base_download_manager
config: {
art: {
queueManager: FDLMGR
/*
NYI
details: FDLDET
*/
}
emptyQueueMenu: fileBaseDownloadManagerEmptyQueue
}
form: {
0: {
mci: {
VM1: {
argName: queueItem
}
HM2: {
focus: true
items: [ "download all", "quit" ]
argName: navSelect
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:downloadAll
}
{
value: { navSelect: 1 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "a", "shift + a" ]
action: @method:downloadAll
}
{
keys: [ "delete", "r", "shift + r" ]
action: @method:removeItem
}
{
keys: [ "c", "shift + c" ]
action: @method:clearQueue
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseDownloadManagerEmptyQueue: {
desc: Empty Download Queue
art: FEMPTYQ
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}
fileBaseWebDownloadManager: {
desc: Web D/L Manager
module: file_base_web_download_manager
config: {
art: {
queueManager: FWDLMGR
batchList: BATDLINF
}
emptyQueueMenu: fileBaseDownloadManagerEmptyQueue
}
form: {
0: {
mci: {
VM1: {
argName: queueItem
}
HM2: {
focus: true
items: [ "get batch link", "quit", "help" ]
argName: navSelect
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:getBatchLink
}
{
value: { navSelect: 1 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "b", "shift + b" ]
action: @method:getBatchLink
}
{
keys: [ "delete", "r", "shift + r" ]
action: @method:removeItem
}
{
keys: [ "c", "shift + c" ]
action: @method:clearQueue
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseUploadFiles: {
desc: Uploading
module: upload
config: {
interrupt: never
art: {
options: ULOPTS
fileDetails: ULDETAIL
processing: ULCHECK
dupes: ULDUPES
}
}
form: {
// options
0: {
mci: {
SM1: {
argName: areaSelect
focus: true
}
TM2: {
argName: uploadType
items: [ "blind", "supply filename" ]
}
ET3: {
argName: fileName
maxLength: 255
validate: @method:validateNonBlindFileName
}
HM4: {
argName: navSelect
items: [ "continue", "cancel" ]
submit: true
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:optionsNavContinue
}
{
value: { navSelect: 1 }
action: @systemMethod:prevMenu
}
]
}
"actionKeys" : [
{
"keys" : [ "escape" ],
action: @systemMethod:prevMenu
}
]
}
1: {
mci: { }
}
// file details entry
2: {
mci: {
MT1: {
argName: shortDesc
tabSwitchesView: true
focus: true
}
ET2: {
argName: tags
}
ME3: {
argName: estYear
maskPattern: "####"
}
BT4: {
argName: continue
text: continue
submit: true
}
}
submit: {
*: [
{
value: { continue: null }
action: @method:fileDetailsContinue
}
]
}
}
// dupes
3: {
mci: {
VM1: {
/*
Use 'dupeInfoFormat' to custom format:
areaDesc
areaName
areaTag
desc
descLong
fileId
fileName
fileSha256
storageTag
uploadTimestamp
*/
mode: preview
}
}
}
}
}
fileBaseSearch: {
module: file_base_search
desc: Searching Files
art: FSEARCH
form: {
0: {
mci: {
ET1: {
focus: true
argName: searchTerms
}
BT2: {
argName: search
text: search
submit: true
}
ET3: {
maxLength: 64
argName: tags
}
SM4: {
maxLength: 64
argName: areaIndex
}
SM5: {
items: [
"upload date",
"uploaded by",
"downloads",
"rating",
"estimated year",
"size",
"filename",
]
argName: sortByIndex
}
SM6: {
items: [
"decending",
"ascending"
]
argName: orderByIndex
}
BT7: {
argName: advancedSearch
text: advanced search
submit: true
}
}
submit: {
*: [
{
value: { search: null }
action: @method:search
}
{
value: { advancedSearch: null }
action: @method:search
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseSetNewScanDate: {
module: set_newscan_date
desc: File Base
art: SETFNSDATE
config: {
target: file
scanDateFormat: YYYYMMDD
}
form: {
0: {
mci: {
ME1: {
focus: true
submit: true
argName: scanDate
maskPattern: "####/##/##"
}
}
submit: {
*: [
{
value: { scanDate: null }
action: @method:scanDateSubmit
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
fileBaseExportListFilter: {
module: file_base_search
art: FBLISTEXPSEARCH
config: {
fileBaseListEntriesMenu: fileBaseExportList
}
form: {
0: {
mci: {
ET1: {
focus: true
argName: searchTerms
}
BT2: {
argName: search
text: search
submit: true
}
ET3: {
maxLength: 64
argName: tags
}
SM4: {
maxLength: 64
argName: areaIndex
}
SM5: {
items: [
"upload date",
"uploaded by",
"downloads",
"rating",
"estimated year",
"size",
"filename"
]
argName: sortByIndex
}
SM6: {
items: [
"decending",
"ascending"
]
argName: orderByIndex
}
BT7: {
argName: advancedSearch
text: advanced search
submit: true
}
}
submit: {
*: [
{
value: { search: null }
action: @method:search
}
{
value: { advancedSearch: null }
action: @method:search
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseExportList: {
module: file_base_user_list_export
art: FBLISTEXP
config: {
pause: true
templates: {
entry: file_list_entry.asc
}
}
form: {
0: {
mci: {
TL1: { }
TL2: { }
}
}
}
}
fileBaseExportListNoResults: {
desc: Browsing Files
art: FBNORES
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}
// Referenced by various menus
fileBaseGetRatingForSelectedEntry: {
desc: Rating a File
prompt: fileBaseRateEntryPrompt
config: {
cls: true
}
submit: [
// :TODO: handle esc/q
{
// pass data back to caller
value: { rating: null }
action: @systemMethod:prevMenu
}
]
}
// default menu entry used by the 'file_area_list' module
// when there are no search results for the provided criteria
fileBaseListEntriesNoResults: {
desc: Browsing Files
art: FBNORES
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}
// default menu entry used by the 'file_base_download_manager' module
// for protocol selection
fileTransferProtocolSelection: {
desc: Protocol selection
module: file_transfer_protocol_select
art: FPROSEL
form: {
0: {
mci: {
VM1: {
focus: true
argName: protocol
}
}
submit: {
*: [
{
value: { protocol: null }
action: @method:selectProtocol
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
// default menu entry used by the 'upload' module for when
// no areas are available for the user to upload to
fileBaseNoUploadAreasAvail: {
desc: File Base
art: ULNOAREA
config: {
pause: true
menuFlags: [ "noHistory", "popParent" ]
}
}
// default menu entry used by the 'file_transfer_protocol_select' module
// when performing user downloads
sendFilesToUser: {
desc: Downloading
module: file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
direction: send
}
}
// default menu entry used by the 'file_transfer_protocol_select' module
// when performing user uploads
recvFilesFromUser: {
desc: Uploading
module: file_transfer
config: {
// defaults - generally use extraArgs
protocol: zmodem8kSexyz
direction: recv
}
}
}
prompts: {
fileMenuCommand: {
art: FILPMPT
mci: {
TL1: {}
ET2: {
argName: menuOption
width: 20
maxLength: 20
textStyle: upper
focus: true
}
}
}
fileBaseRateEntryPrompt: {
art: RATEFILE
mci: {
SM1: {
argName: rating
items: [ "-----", "*----", "**---", "***--", "****-", "*****" ]
}
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
fileBaseTagEntryPrompt: {
art: TAGFILE
mci: {
ET1: {
argName: tags
}
}
}
}
}

View File

@ -0,0 +1,607 @@
{
menus: {
//
// Send telnet connections to matrix where users can login, apply, etc.
//
telnetConnected: {
art: CONNECT
next: matrix
config: { nextTimeout: 1500 }
}
//
// SSH connections are pre-authenticated via the SSH server itself.
// Jump directly to either the 2FA/OTP auth or the login sequence
// depending on user ACS.
//
sshConnected: {
art: CONNECT
next: [
{
acs: AR2
next: loginTwoFactorAuthOTPLoop
}
{
next: fullLoginSequenceLoginArt
}
]
config: { nextTimeout: 1500 }
}
//
// Another SSH specialization: If the user logs in with a new user
// name (e.g. "new", "apply", ...) they will be directed to the
// application process.
//
sshConnectedNewUser: {
art: CONNECT
next: newUserApplicationPreSsh
config: { nextTimeout: 1500 }
}
// Ye ol' standard matrix
matrix: {
art: matrix
form: {
0: {
VM: {
mci: {
VM1: {
submit: true
focus: true
argName: navSelect
items: [
{
text: login
data: login
}
{
text: apply
data: apply
}
//
// To enable the forgot password option, you'll need to have
// the web server & email configured. Once that is in place,
// uncomment the section below.
//
// See docs for more information
//
/*
{
text: forgot pass
data: forgot
}
*/
{
text: log off
data: logoff
}
]
}
}
submit: {
*: [
{
value: { navSelect: "login" }
action: @menu:login
}
{
value: { navSelect: "apply" }
action: @menu:newUserApplicationPre
}
{
value: { navSelect: "forgot" }
action: @menu:forgotPassword
}
{
value: { navSelect: "logoff" }
action: @menu:logoff
}
]
}
}
}
}
}
login: {
art: USERLOG
next: [
{
//
// Users with 2FA/OTP enabled *must* go through
// an additional OTP authentication step
//
acs: AR2
next: loginTwoFactorAuthOTPLoop
}
{
// ...everyone else can carry on as per usual
next: fullLoginSequenceLoginArt
}
]
config: {
tooNodeMenu: loginAttemptTooNode
inactive: loginAttemptAccountInactive
disabled: loginAttemptAccountDisabled
locked: loginAttemptAccountLocked
}
form: {
0: {
mci: {
ET1: {
maxLength: @config:users.usernameMax
argName: username
focus: true
}
ET2: {
password: true
maxLength: @config:users.passwordMax
argName: password
submit: true
}
}
submit: {
*: [
{
value: { password: null }
action: @systemMethod:login
}
]
}
actionKeys: @reference:common.escToPrev
}
}
}
loginAttemptTooNode: {
art: TOONODE
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
loginAttemptAccountLocked: {
art: ACCOUNTLOCKED
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
loginAttemptAccountDisabled: {
art: ACCOUNTDISABLED
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
loginAttemptAccountInactive: {
art: ACCOUNTINACTIVE
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
forgotPassword: {
desc: Forgot password
prompt: forgotPasswordPrompt
submit: [
{
value: { username: null }
action: @systemMethod:sendForgotPasswordEmail
extraArgs: { next: "forgotPasswordSubmitted" }
}
]
}
forgotPasswordSubmitted: {
desc: Forgot password
art: FORGOTPWSENT
config: {
cls: true
pause: true
}
next: @systemMethod:logoff
}
fullLoginSequenceLoginArt: {
desc: Logging In
art: WELCOME
config: { pause: true }
next: fullLoginSequenceLastCallers
}
fullLoginSequenceLastCallers: {
desc: Last Callers
module: last_callers
art: LASTCALL
config: {
pause: true
font: cp437
}
next: fullLoginSequenceWhosOnline
}
fullLoginSequenceWhosOnline: {
desc: Who's Online
module: whos_online
art: WHOSON
config: { pause: true }
next: fullLoginSequenceOnelinerz
}
fullLoginSequenceOnelinerz: {
desc: Viewing Onelinerz
module: onelinerz
next: [
{
// calls >= 2
acs: NC2
next: fullLoginSequenceNewScanConfirm
}
{
// new users - skip new scan
next: fullLoginSequenceUserStats
}
]
config: {
cls: true
art: {
view: ONELINER
add: ONEADD
}
}
form: {
0: {
mci: {
VM1: {
focus: false
height: 10
}
TM2: {
argName: addOrExit
items: [ "yeah!", "nah" ]
"hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 }
submit: true
focus: true
}
}
submit: {
*: [
{
value: { addOrExit: 0 }
action: @method:viewAddScreen
}
{
value: { addOrExit: null }
action: @systemMethod:nextMenu
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:nextMenu
}
]
},
1: {
mci: {
ET1: {
focus: true
maxLength: 70
argName: oneliner
}
TL2: {
width: 60
}
TM3: {
argName: addOrCancel
items: [ "add", "cancel" ]
"hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 }
submit: true
}
}
submit: {
*: [
{
value: { addOrCancel: 0 }
action: @method:addEntry
}
{
value: { addOrCancel: 1 }
action: @method:cancelAdd
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:cancelAdd
}
]
}
}
}
fullLoginSequenceNewScanConfirm: {
desc: Logging In
prompt: loginGlobalNewScan
submit: [
{
value: { promptValue: 0 }
action: @menu:fullLoginSequenceNewScan
}
{
value: { promptValue: 1 }
action: @menu:fullLoginSequenceUserStats
}
]
}
fullLoginSequenceNewScan: {
desc: Performing New Scan
module: new_scan
art: NEWSCAN
next: fullLoginSequenceSysStats
config: {
messageListMenu: newScanMessageList
}
}
newScanMessageList: {
desc: New Messages
module: msg_list
art: NEWMSGS
config: {
menuViewPost: messageAreaViewPost
}
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: messageIndex
}
TL6: { }
}
submit: {
*: [
{
value: { messageIndex: null }
action: @method:selectMessage
}
]
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "x", "shift + x" ]
action: @method:fullExit
}
{
keys: [ "m", "shift + m" ]
action: @method:markAllRead
}
]
}
}
}
newScanFileBaseList: {
module: file_area_list
desc: New Files
config: {
art: {
browse: FNEWBRWSE
details: FDETAIL
detailsGeneral: FDETGEN
detailsNfo: FDETNFO
detailsFileList: FDETLST
help: FBHELP
}
}
form: {
0: {
mci: {
MT1: {
mode: preview
ansiView: true
}
HM2: {
focus: true
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "help", "quit"
]
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFile
}
{
value: { navSelect: 1 }
action: @method:nextFile
}
{
value: { navSelect: 2 }
action: @method:viewDetails
}
{
value: { navSelect: 3 }
action: @method:toggleQueue
}
{
value: { navSelect: 4 }
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
value: { navSelect: 5 }
action: @method:displayHelp
}
{
value: { navSelect: 6 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "w", "shift + w" ]
action: @method:showWebDownloadLink
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "t", "shift + t" ]
action: @method:toggleQueue
}
{
keys: [ "v", "shift + v" ]
action: @method:viewDetails
}
{
keys: [ "r", "shift + r" ]
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
keys: [ "?" ]
action: @method:displayHelp
}
]
}
1: {
mci: {
HM1: {
focus: true
submit: true
argName: navSelect
items: [
"general", "nfo/readme", "file listing"
]
}
}
actionKeys: @reference:common.quitToPrev
}
2: {
// details - general
mci: { }
}
3: {
// details - nfo/readme
mci: {
MT1: {
mode: preview
}
}
}
4: {
// details - file listing
mci: {
VM1: { }
}
}
}
}
fullLoginSequenceSysStats: {
desc: System Stats
art: SYSSTAT
config: { pause: true }
next: fullLoginSequenceUserStats
}
fullLoginSequenceUserStats: {
desc: User Stats
art: STATUS
config: { pause: true }
next: mainMenu
}
//
// Empty menu to catch us in a 2FA/OTP auth loop
// until the user either authenticates successfully
// or the system boots them.
//
loginTwoFactorAuthOTPLoop: {
next: loginTwoFactorAuthOTP
}
loginTwoFactorAuthOTP: {
art: 2FAOTP
next: fullLoginSequenceLoginArt
form: {
0: {
mci: {
ET1: {
argName: token
focus: true
submit: true
}
}
submit: {
*: [
{
value: { token: null }
action: @systemMethod:login2FA_OTP
}
]
}
actionKeys: [
{
// no turning back at this point...
keys: [ "escape" ]
action: @systemMethod:logoff
}
]
}
}
}
}
prompts: {
loginGlobalNewScan: {
art: GNSPMPT
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,816 @@
{
menus: {
messageBaseMainMenu: {
art: MSGMNU
desc: Message Area
prompt: messageBaseMenuPrompt
config: {
interrupt: realtime
}
submit: [
{
value: { command: "P" }
action: @menu:messageBaseNewPost
}
{
value: { command: "J" }
action: @menu:messageBaseChangeCurrentConference
}
{
value: { command: "C" }
action: @menu:messageBaseChangeCurrentArea
}
{
value: { command: "L" }
action: @menu:messageBaseMessageList
}
{
value: { command: "<" }
action: @systemMethod:prevConf
}
{
value: { command: ">" }
action: @systemMethod:nextConf
}
{
value: { command: "[" }
action: @systemMethod:prevArea
}
{
value: { command: "]" }
action: @systemMethod:nextArea
}
{
value: { command: "D" }
action: @menu:messageBaseSetNewScanDate
}
{
value: { command: "S" }
action: @menu:messageBaseSearch
}
{
value: { command: "M" }
action: @menu:messageBaseMyMessages
}
{
value: { command: "A" }
action: @menu:editAutoSignature
}
{
value: { command: "Q" }
action: @systemMethod:prevMenu
}
{
value: { command: "G" }
action: @menu:fullLogoffSequence
}
]
}
messageBaseNewPost: {
desc: Posting message,
module: msg_area_post_fse
config: {
art: {
header: MSGEHDR
body: MSGBODY
footerEditor: MSGEFTR
footerEditorMenu: MSGEMFT
help: MSGEHLP
}
editorMode: edit
editorType: area
}
form: {
0: {
mci: {
TL1: {
argName: from
}
ET2: {
argName: to
focus: true
text: All
validate: @systemMethod:validateNonEmpty
maxLength: 36
}
ET3: {
argName: subject
maxLength: 72
submit: true
validate: @systemMethod:validateNonEmpty
}
}
submit: {
3: [
{
value: { subject: null }
action: @method:headerSubmit
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
1: {
"mci" : {
MT1: {
width: 79
argName: message
mode: edit
}
}
submit: {
*: [ { "value": "message", "action": "@method:editModeEscPressed" } ]
}
actionKeys: [
{
keys: [ "escape" ]
viewId: 1
}
]
}
2: {
TLTL: {
mci: {
TL1: { width: 5 }
TL2: { width: 4 }
}
}
}
3: {
HM: {
mci: {
HM1: {
"items" : [ "save", "discard", "help" ]
}
}
submit: {
*: [
{
value: { 1: 0 }
action: @method:editModeMenuSave
}
{
value: { 1: 1 }
action: @systemMethod:prevMenu
}
{
value: { 1: 2 }
action: @method:editModeMenuHelp
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:editModeEscPressed
}
{
keys: [ "?" ]
action: @method:editModeMenuHelp
}
]
}
}
}
}
messageBaseChangeCurrentConference: {
art: CCHANGE
module: msg_conf_list
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: conf
}
}
submit: {
*: [
{
value: { conf: null }
action: @method:changeConference
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
messageBaseChangeCurrentArea: {
art: CHANGE
module: msg_area_list
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: area
}
}
submit: {
*: [
{
value: { area: null }
action: @method:changeArea
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
messageBaseMessageList: {
module: msg_list
art: MSGLIST
config: {
menuViewPost: messageAreaViewPost
}
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: messageIndex
}
}
submit: {
*: [
{
value: { message: null }
action: @method:selectMessage
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
messageBaseSetNewScanDate: {
module: set_newscan_date
desc: Message Base
art: SETMNSDATE
config: {
target: message
scanDateFormat: YYYYMMDD
}
form: {
0: {
mci: {
ME1: {
focus: true
submit: true
argName: scanDate
maskPattern: "####/##/##"
}
SM2: {
argName: targetSelection
submit: false
}
}
submit: {
*: [
{
value: { scanDate: null }
action: @method:scanDateSubmit
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
messageBaseSearch: {
desc: Message Search
module: message_base_search
art: MSEARCH
config: {
messageListMenu: messageBaseSearchResultsMessageList
}
form: {
0: {
mci: {
ET1: {
focus: true
argName: searchTerms
}
BT2: {
argName: search
text: search
submit: true
}
SM3: {
argName: confTag
}
SM4: {
argName: areaTag
}
ET5: {
argName: toUserName
maxLength: @config:users.usernameMax
}
ET6: {
argName: fromUserName
maxLength: @config:users.usernameMax
}
BT7: {
argName: advancedSearch
text: advanced search
submit: true
}
}
submit: {
*: [
{
value: { search: null }
action: @method:search
}
{
value: { advancedSearch: null }
action: @method:search
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
messageBaseSearchResultsMessageList: {
desc: Message Search
module: msg_list
art: MSRCHLST
config: {
menuViewPost: messageAreaViewPost
}
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: messageIndex
}
TL6: {
// theme me!
}
}
submit: {
*: [
{
value: { messageIndex: null }
action: @method:selectMessage
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
// The message_base_search module looks for this entry by default
messageSearchNoResults: {
desc: Message Search
art: MSRCNORES
config: {
pause: true
}
}
messageBaseMyMessages: {
desc: Personal Messages
module: my_messages
config: {
messageListMenu: messageBaseMyMessagesList
}
}
messageBaseMyMessagesList: {
desc: Personal Messages
module: msg_list
art: MYMSGLST
config: {
menuViewPost: messageAreaViewPost
}
form: {
0: {
mci: {
VM1: {
focus: true
submit: true
argName: messageIndex
}
}
submit: {
*: [
{
value: { messageIndex: null }
action: @method:selectMessage
}
]
}
actionKeys: @reference:common.quitToPrev
}
}
}
editAutoSignature: {
desc: Auto Sig Editor
module: autosig_edit
art: autosig
form: {
0: {
mci: {
MT1: {
argName: signature
tabSwitchesView: true
}
BT2: {
text: save
argName: save
submit: true
}
}
submit: {
*: [
{
value: { save: null }
action: @method:saveChanges
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
messageAreaViewPost: {
module: msg_area_view_fse
config: {
art: {
header: MSGVHDR
body: MSGBODY
footerView: MSGVFTR
help: MSGVHLP
},
editorMode: view
editorType: area
}
form: {
0: {
mci: {
// :TODO: ensure this block isn't even req. for theme to apply...
}
}
1: {
mci: {
MT1: {
width: 79
mode: preview
}
}
submit: {
*: [
{
value: message
action: @method:editModeEscPressed
}
]
}
actionKeys: [
{
keys: [ "escape" ]
viewId: 1
}
]
}
2: {
TLTL: {
mci: {
TL1: { width: 5 }
TL2: { width: 4 }
}
}
}
4: {
mci: {
HM1: {
// :TODO: (#)Jump/(L)Index (msg list)/Last
items: [ "prev", "next", "reply", "quit", "help" ]
focusItemIndex: 1
}
}
submit: {
*: [
{
value: { 1: 0 }
action: @method:prevMessage
}
{
value: { 1: 1 }
action: @method:nextMessage
}
{
value: { 1: 2 }
action: @method:replyMessage
extraArgs: {
menu: messageAreaReplyPost
}
}
{
value: { 1: 3 }
action: @systemMethod:prevMenu
}
{
value: { 1: 4 }
action: @method:viewModeMenuHelp
}
]
}
actionKeys: [
{
keys: [ "p", "shift + p" ]
action: @method:prevMessage
}
{
keys: [ "n", "shift + n" ]
action: @method:nextMessage
}
{
keys: [ "r", "shift + r" ]
action: @method:replyMessage
extraArgs: {
menu: messageAreaReplyPost
}
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "?" ]
action: @method:viewModeMenuHelp
}
{
keys: [ "down arrow", "up arrow", "page up", "page down" ]
action: @method:movementKeyPressed
}
]
}
}
}
messageAreaReplyPost: {
module: msg_area_post_fse
config: {
art: {
header: MSGEHDR
body: MSGBODY
quote: MSGQUOT
footerEditor: MSGEFTR
footerEditorMenu: MSGEMFT
help: MSGEHLP
}
editorMode: edit
editorType: area
}
form: {
0: {
mci: {
// :TODO: use appropriate system properties for max lengths
TL1: {
argName: from
}
ET2: {
argName: to
focus: true
validate: @systemMethod:validateNonEmpty
maxLength: 36
}
ET3: {
argName: subject
maxLength: 72
submit: true
validate: @systemMethod:validateNonEmpty
}
TL4: {
// :TODO: this is for RE: line (NYI)
//width: 27
//textOverflow: ...
}
}
submit: {
3: [
{
value: { subject: null }
action: @method:headerSubmit
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
1: {
mci: {
MT1: {
width: 79
height: 14
argName: message
mode: edit
}
}
submit: {
*: [ { "value": "message", "action": "@method:editModeEscPressed" } ]
}
actionKeys: [
{
keys: [ "escape" ],
viewId: 1
}
]
}
3: {
mci: {
HM1: {
items: [ "save", "discard", "quote", "help" ]
}
}
submit: {
*: [
{
value: { 1: 0 }
action: @method:editModeMenuSave
}
{
value: { 1: 1 }
action: @systemMethod:prevMenu
}
{
value: { 1: 2 },
action: @method:editModeMenuQuote
}
{
value: { 1: 3 }
action: @method:editModeMenuHelp
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:editModeEscPressed
}
{
keys: [ "s", "shift + s" ]
action: @method:editModeMenuSave
}
{
keys: [ "d", "shift + d" ]
action: @systemMethod:prevMenu
}
{
keys: [ "q", "shift + q" ]
action: @method:editModeMenuQuote
}
{
keys: [ "?" ]
action: @method:editModeMenuHelp
}
]
}
// Quote builder
5: {
mci: {
MT1: {
width: 79
height: 7
}
VM3: {
width: 79
height: 4
argName: quote
}
}
submit: {
*: [
{
value: { quote: null }
action: @method:appendQuoteEntry
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:quoteBuilderEscPressed
}
]
}
}
}
//
// The 'msg_conf_list' module defaults to looking for
// a menu entry of 'changeConfPreArtMenu'. If found,
// this menu will be executed upon changing message
// conferences using the conference tag as an art spec.
//
changeMessageConfPreArt: {
module: show_art
config: {
method: messageConf
key: confTag
pause: true
cls: true
menuFlags: [ "popParent", "noHistory" ]
}
}
//
// The 'msg_area_list' module defaults to looking for
// a menu entry of 'changeMessageAreaPreArt'. If found,
// this menu will be executed upon changing message
// areas using the area tag as an art spec.
//
changeMessageAreaPreArt: {
module: show_art
config: {
method: messageArea
key: areaTag
pause: true
cls: true
menuFlags: [ "popParent", "noHistory" ]
}
}
}
prompts: {
messageBaseMenuPrompt: {
art: MSGPMPT
mci: {
//TL1: {}
ET2: {
argName: command
width: 20
maxLength: 20
submit: true
textStyle: upper
focus: true
}
}
},
// default prompt entry used by the 'msg_lsit' module
deleteMessageFromListPrompt: {
art: MSGDELPMPT
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
}
}

View File

@ -0,0 +1,362 @@
{
menus: {
// A quick preamble - defaults to warning about broken terminals
newUserApplicationPre: {
art: NEWUSER1
next: newUserApplication
desc: Applying
config: {
pause: true
cls: true
menuFlags: [ "noHistory" ]
}
}
newUserApplication: {
module: nua
art: NUA
next: [
{
// Initial SysOp does not send feedback to themselves
acs: ID1
next: fullLoginSequenceLoginArt
}
{
// ...everyone else does
next: newUserFeedbackToSysOpPreamble
}
]
form: {
0: {
mci: {
ET1: {
focus: true
argName: username
maxLength: @config:users.usernameMax
validate: @systemMethod:validateUserNameAvail
}
ET2: {
argName: realName
maxLength: @config:users.realNameMax
validate: @systemMethod:validateNonEmpty
}
MET3: {
argName: birthdate
maskPattern: "####/##/##"
validate: @systemMethod:validateBirthdate
}
ME4: {
argName: sex
maskPattern: A
textStyle: upper
validate: @systemMethod:validateNonEmpty
}
ET5: {
argName: location
maxLength: @config:users.locationMax
validate: @systemMethod:validateNonEmpty
}
ET6: {
argName: affils
maxLength: @config:users.affilsMax
}
ET7: {
argName: email
maxLength: @config:users.emailMax
validate: @systemMethod:validateEmailAvail
}
ET8: {
argName: web
maxLength: @config:users.webMax
}
ET9: {
argName: password
password: true
maxLength: @config:users.passwordMax
validate: @systemMethod:validatePasswordSpec
}
ET10: {
argName: passwordConfirm
password: true
maxLength: @config:users.passwordMax
validate: @method:validatePassConfirmMatch
}
TM12: {
argName: submission
items: [ "apply", "cancel" ]
submit: true
}
}
submit: {
*: [
{
value: { "submission" : 0 }
action: @method:submitApplication
extraArgs: {
inactive: userNeedsActivated
error: newUserCreateError
}
}
{
value: { "submission" : 1 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: @reference:common.escToPrev
}
}
}
// A quick preamble - defaults to warning about broken terminals (SSH version)
newUserApplicationPreSsh: {
art: NEWUSER1
next: newUserApplicationSsh
desc: Applying
config: {
pause: true
cls: true
menuFlags: [ "noHistory" ]
}
}
//
// SSH specialization of NUA
// Canceling this form logs off vs falling back to matrix
//
newUserApplicationSsh: {
module: nua
art: NUA
fallback: logoff
next: newUserFeedbackToSysOpPreamble
form: {
0: {
mci: {
ET1: {
focus: true
argName: username
maxLength: @config:users.usernameMax
validate: @systemMethod:validateUserNameAvail
}
ET2: {
argName: realName
maxLength: @config:users.realNameMax
validate: @systemMethod:validateNonEmpty
}
MET3: {
argName: birthdate
maskPattern: "####/##/##"
validate: @systemMethod:validateBirthdate
}
ME4: {
argName: sex
maskPattern: A
textStyle: upper
validate: @systemMethod:validateNonEmpty
}
ET5: {
argName: location
maxLength: @config:users.locationMax
validate: @systemMethod:validateNonEmpty
}
ET6: {
argName: affils
maxLength: @config:users.affilsMax
}
ET7: {
argName: email
maxLength: @config:users.emailMax
validate: @systemMethod:validateEmailAvail
}
ET8: {
argName: web
maxLength: @config:users.webMax
}
ET9: {
argName: password
password: true
maxLength: @config:users.passwordMax
validate: @systemMethod:validatePasswordSpec
}
ET10: {
argName: passwordConfirm
password: true
maxLength: @config:users.passwordMax
validate: @method:validatePassConfirmMatch
}
TM12: {
argName: submission
items: [ "apply", "cancel" ]
submit: true
}
}
submit: {
*: [
{
value: { "submission" : 0 }
action: @method:submitApplication
extraArgs: {
inactive: userNeedsActivated
error: newUserCreateError
}
}
{
value: { "submission" : 1 }
action: @systemMethod:logoff
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:logoff
}
]
}
}
}
newUserFeedbackToSysOpPreamble: {
art: LETTER
config: { pause: true }
next: newUserFeedbackToSysOp
}
newUserFeedbackToSysOp: {
desc: Feedback to SysOp
module: msg_area_post_fse
next: [
{
acs: AS2
next: fullLoginSequenceLoginArt
}
{
next: newUserInactiveDone
}
]
config: {
art: {
header: MSGEHDR
body: MSGBODY
footerEditor: MSGEFTR
footerEditorMenu: MSGEMFT
help: MSGEHLP
},
editorMode: edit
editorType: email
messageAreaTag: private_mail
toUserId: 1 /* always to +op */
}
form: {
0: {
mci: {
TL1: {
argName: from
}
ET2: {
argName: to
focus: true
text: @sysStat:sysop_username
maxLength: 36
// :TODO: readOnly: true
}
ET3: {
argName: subject
maxLength: 72
submit: true
text: New user feedback
validate: @systemMethod:validateMessageSubject
}
}
submit: {
3: [
{
value: { subject: null }
action: @method:headerSubmit
}
]
}
}
1: {
mci: {
MT1: {
width: 79
argName: message
mode: edit
}
}
submit: {
*: [
{
value: message
action: @method:editModeEscPressed
}
]
}
actionKeys: [
{
keys: [ "escape" ]
viewId: 1
}
]
},
2: {
TLTL: {
mci: {
TL1: {
width: 5
}
TL2: {
width: 4
}
}
}
}
3: {
HM: {
mci: {
HM1: {
// :TODO: clear
items: [ "save", "help" ]
}
}
submit: {
*: [
{
value: { 1: 0 }
action: @method:editModeMenuSave
}
{
value: { 1: 1 }
action: @method:editModeMenuHelp
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:editModeEscPressed
}
{
keys: [ "?" ]
action: @method:editModeMenuHelp
}
]
}
}
}
}
newUserInactiveDone: {
desc: Finished with NUA
art: DONE
config: { pause: true }
next: @menu:logoff
}
}
}

View File

@ -0,0 +1,197 @@
{
menus: {
privateMailMenu: {
art: MAILMNU
desc: Private Mail
prompt: menuCommand
config: {
interrupt: realtime
}
submit: [
{
value: { command: "C" }
action: @menu:privateMailMenuCreateMessage
}
{
value: { command: "I" }
action: @menu:privateMailMenuInbox
}
{
value: { command: "Q" }
action: @systemMethod:prevMenu
}
{
value: { command: "G" }
action: @menu:fullLogoffSequence
}
]
}
privateMailMenuCreateMessage: {
desc: Mailing Someone
module: msg_area_post_fse
config: {
art: {
header: MSGEHDR
body: MSGBODY
footerEditor: MSGEFTR
footerEditorMenu: MSGEMFT
help: MSGEHLP
},
editorMode: edit
editorType: email
messageAreaTag: private_mail
}
form: {
0: {
mci: {
TL1: {
argName: from
}
ET2: {
argName: to
focus: true
validate: @systemMethod:validateGeneralMailAddressedTo
maxLength: 36
}
ET3: {
argName: subject
maxLength: 72
submit: true
validate: @systemMethod:validateMessageSubject
}
}
submit: {
3: [
{
value: { subject: null }
action: @method:headerSubmit
}
]
}
actionKeys: @reference: common.escToPrev
}
1: {
mci: {
MT1: {
width: 79
argName: message
mode: edit
}
}
submit: {
*: [ { value: "message", action: "@method:editModeEscPressed" } ]
}
actionKeys: [
{
keys: [ "escape" ]
viewId: 1
}
]
},
2: {
TLTL: {
mci: {
TL1: {
width: 5
}
TL2: {
width: 4
}
}
}
}
3: {
HM: {
mci: {
HM1: {
// :TODO: clear
items: [ "save", "discard", "help" ]
}
}
submit: {
*: [
{
value: { 1: 0 }
action: @method:editModeMenuSave
}
{
value: { 1: 1 }
action: @systemMethod:prevMenu
}
{
value: { 1: 2 }
action: @method:editModeMenuHelp
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @method:editModeEscPressed
}
{
keys: [ "?" ]
action: @method:editModeMenuHelp
}
]
}
}
}
}
privateMailMenuInbox: {
module: msg_list
art: PRVMSGLIST
config: {
menuViewPost: messageAreaViewPost
messageAreaTag: private_mail
}
form: {
0: { // main list
mci: {
VM1: {
focus: true
submit: true
argName: messageIndex
}
}
submit: {
*: [
{
value: { messageIndex: null }
action: @method:selectMessage
}
]
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "delete", "d", "shift + d" ]
action: @method:deleteSelected
}
]
}
1: { // delete prompt form
submit: {
*: [
{
value: { promptValue: 0 }
action: @method:deleteMessageYes
}
{
value: { promptValue: 1 }
action: @method:deleteMessageNo
}
]
}
}
}
}
}
}

View File

@ -1,294 +0,0 @@
{
/*
./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- -
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____
/____ _____| __________ ___|__| ____| \ / _____ \
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
/__ _\
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
-------------------------------------------------------------------------------
This configuration is in HJSON (http://hjson.org/) format. Strict to-spec
JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON.
See http://hjson.org/ for more information and syntax.
If you haven't yet, copy the conents of this file to something like
sick_board_prompt.hjson. Point to it via config.hjson using the
'general.promptFile' key:
general: { promptFile: "sick_board_prompt.hjson" }
*/
// :TODO: this entire file needs cleaned up a LOT
// :TODO: Convert all of this to HJSON
prompts: {
userCredentials: {
"art" : "usercred",
"mci" : {
"ET1" : {
"argName" : "username",
"maxLength" : "@config:users.usernameMax"
},
"ET2" : {
"submit" : true,
"argName" : "password",
"password" : true,
"maxLength" : "@config:users.passwordMax"
}
}
},
"userLoginCredentials" : {
"art" : "USRCRED",
"mci" : {
"ET1" : {
"argName" : "username",
"maxLength" : "@config:users.usernameMax"
},
"ET2" : {
"submit" : true,
"argName" : "password",
"password" : true,
"maxLength" : "@config:users.passwordMax"
}
}
},
logoffConfirmation: {
art: LOGPMPT
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
loginSequenceFlavorSelect: {
art: LOGINSEL
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
focusItemIndex: 1
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
loginGlobalNewScan: {
art: GNSPMPT
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
menuCommand: {
art: MNUPRMT
mci: {
TL1: {
// theme me!
}
ET2: {
argName: command
width: 20
maxLength: 20
submit: true
textStyle: upper
focus: true
}
}
},
messageMenuCommand: {
art: MSGPMPT
mci: {
TL1: {
// theme me!
}
ET2: {
argName: command
width: 20
maxLength: 20
submit: true
textStyle: upper
focus: true
}
}
},
deleteMessageFromListPrompt: {
art: MSGDELPMPT
mci: {
TM1: {
argName: promptValue
items: [ "yes", "no" ]
focus: true
hotKeys: { Y: 0, N: 1 }
hotKeySubmit: true
}
}
}
"newAreaPostPrompt" : {
"art" : "message_area_new_post",
"mci" : {
"ET1" : {
"argName" : "to",
"width" : 20
},
"ET2" : {
"argName" : "subject",
"width" : 20
}
}
},
forgotPasswordPrompt: {
art: FORGOTPW
mci: {
ET1: {
argName: username
maxLength: @config:users.usernameMax
width: 32
focus: true
}
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
///////////////////////////////////////////////////////////////////////
// File Base Related
///////////////////////////////////////////////////////////////////////
fileMenuCommand: {
art: FILPMPT
mci: {
TL1: {}
ET2: {
argName: menuOption
width: 20
maxLength: 20
textStyle: upper
focus: true
}
}
}
fileBaseRateEntryPrompt: {
art: RATEFILE
mci: {
SM1: {
argName: rating
items: [ "-----", "*----", "**---", "***--", "****-", "*****" ]
}
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
fileBaseTagEntryPrompt: {
art: TAGFILE
mci: {
ET1: {
argName: tags
}
}
}
///////////////////////////////////////////////////////////////////////
// Standard / Required
//
// Prompts in this section are considered "standard" and are required
// to be present
//
///////////////////////////////////////////////////////////////////////
pause: {
//
// Any menu 'pause' will use this prompt
//
art: pause
config: {
trailingLF: no
}
/*
"mci" : {
// :TODO: Need special pause for a key MCI
// e.g. %PA -> themed prompt
}
...or maybe pause should just be special:
{
...
"pause" true
// uses theme pause which can be art/inline/etc.
}
... better, a special prompt
GetKeyView
* echoKey : false
*/
}
/*,
"standard" : {
// any menu 'pause' will display this, pause for a key, then erase and move on
"pause" : {
"art" : "pause"
// :TODO: support mci mappings
}
},
"custom" : {
}*/
/*
see notes in menu_module.js also
...how to allow for this to come from the theme first???
same as custom vc drawing/etc.? ...
{
"theme" : {
"inlineArt" : {
"something" : "%MC and |01Pipe codes here"
}
}
}
"pause" : {
"art" : "@inline:simplePrompt",
// support pipe codes & MCI
"simplePrompt" : "--------/ Pause /----------------",
"mci" : {
}
}
*/
}
}

View File

@ -26,6 +26,7 @@
"binary-parser": "^1.6.2",
"buffers": "github:NuSkooler/node-buffers",
"bunyan": "^1.8.12",
"deepdash": "^5.1.1",
"exiftool": "^0.0.3",
"fs-extra": "9.0.1",
"glob": "7.1.6",
@ -60,7 +61,9 @@
"xxhash": "^0.3.0",
"yazl": "^2.5.1"
},
"devDependencies": {},
"devDependencies": {
"eslint": "^7.2.0"
},
"engines": {
"node": ">=12"
}

545
yarn.lock
View File

@ -2,6 +2,27 @@
# yarn lockfile v1
"@babel/code-frame@^7.0.0":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff"
integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==
dependencies:
"@babel/highlight" "^7.10.1"
"@babel/helper-validator-identifier@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5"
integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==
"@babel/highlight@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0"
integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==
dependencies:
"@babel/helper-validator-identifier" "^7.10.1"
chalk "^2.0.0"
js-tokens "^4.0.0"
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@ -20,6 +41,26 @@ abbrev@1:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
acorn-jsx@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
acorn@^7.2.0:
version "7.3.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd"
integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==
ajv@^6.10.0, ajv@^6.10.2:
version "6.12.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-escapes@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228"
@ -47,6 +88,13 @@ ansi-regex@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@ -76,6 +124,13 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -120,6 +175,11 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
astral-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
async@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
@ -230,6 +290,11 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
@ -237,6 +302,15 @@ capture-exit@^2.0.0:
dependencies:
rsvp "^4.8.4"
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
@ -245,6 +319,14 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -295,6 +377,13 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@ -302,6 +391,11 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
@ -343,6 +437,15 @@ cross-spawn@^6.0.0:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -357,6 +460,13 @@ debug@^4.0.0:
dependencies:
ms "^2.1.1"
debug@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -372,6 +482,19 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deep-is@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepdash@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.1.1.tgz#dcf68b9e15085b5df18bdb4011723790e0c40430"
integrity sha512-esz3pjQJaeYO4z74seqCMrOYUsAAdrhO3KJuEnGEaxTGbSy8VGOWn7jTU2J3nR5WDyNpS5/hse3m/hdM1/8ZWA==
dependencies:
lodash "^4.17.15"
lodash-es "^4.17.15"
define-property@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
@ -426,6 +549,13 @@ detect-libc@^1.0.2:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
dtrace-provider@~0.8:
version "0.8.7"
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04"
@ -433,6 +563,11 @@ dtrace-provider@~0.8:
dependencies:
nan "^2.10.0"
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@ -450,6 +585,111 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
eslint-scope@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5"
integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd"
integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==
dependencies:
eslint-visitor-keys "^1.1.0"
eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa"
integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==
eslint@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.2.0.tgz#d41b2e47804b30dbabb093a967fb283d560082e6"
integrity sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^5.1.0"
eslint-utils "^2.0.0"
eslint-visitor-keys "^1.2.0"
espree "^7.1.0"
esquery "^1.2.0"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^12.1.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^7.0.0"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.14"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^5.2.3"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c"
integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==
dependencies:
acorn "^7.2.0"
acorn-jsx "^5.2.0"
eslint-visitor-keys "^1.2.0"
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
dependencies:
estraverse "^4.1.0"
estraverse@^4.1.0, estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
exec-sh@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b"
@ -524,6 +764,21 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-levenshtein@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fb-watchman@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@ -538,6 +793,13 @@ figures@^3.0.0:
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
dependencies:
flat-cache "^2.0.1"
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@ -548,6 +810,20 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
dependencies:
flatted "^2.0.0"
rimraf "2.6.3"
write "1.0.3"
flatted@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -590,6 +866,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -616,7 +897,14 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
glob@7.1.6:
glob-parent@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
dependencies:
is-glob "^4.0.1"
glob@7.1.6, glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@ -651,6 +939,13 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^12.1.0:
version "12.4.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
dependencies:
type-fest "^0.8.1"
globby@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
@ -677,6 +972,11 @@ graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
@ -749,6 +1049,24 @@ ignore-walk@^3.0.1:
dependencies:
minimatch "^3.0.4"
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
import-fresh@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -776,7 +1094,7 @@ ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inquirer@^7.1.0:
inquirer@^7.0.0, inquirer@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
@ -858,6 +1176,11 @@ is-extendable@^1.0.1:
dependencies:
is-plain-object "^2.0.4"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
@ -875,6 +1198,13 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.0, is-glob@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
dependencies:
is-extglob "^2.1.1"
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -940,6 +1270,29 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
jsonfile@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179"
@ -978,7 +1331,20 @@ later@1.2.0:
resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f"
integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8=
lodash@^4.17.15:
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lodash-es@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash@^4.17.14, lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -1158,6 +1524,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
ncp@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
@ -1318,6 +1689,18 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
dependencies:
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.3"
os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@ -1353,6 +1736,13 @@ p-map@^1.1.1:
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@ -1373,6 +1763,11 @@ path-key@^2.0.0, path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -1400,11 +1795,21 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@ -1413,6 +1818,11 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qrcode-generator@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
@ -1458,6 +1868,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexpp@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -1473,6 +1888,11 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -1491,6 +1911,13 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rimraf@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
dependencies:
glob "^7.1.3"
rimraf@^2.2.8, rimraf@^2.6.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
@ -1586,6 +2013,11 @@ semver@^5.5.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
semver@^7.2.1:
version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
serialize-error@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a"
@ -1623,16 +2055,37 @@ shebang-command@^1.2.0:
dependencies:
shebang-regex "^1.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
dependencies:
ansi-styles "^3.2.0"
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -1698,6 +2151,11 @@ split2@^3.0.0:
dependencies:
readable-stream "^3.0.0"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sqlite3-trans@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sqlite3-trans/-/sqlite3-trans-1.2.2.tgz#faf268cc8d04dfd1a4854d64a70a229bdb50609f"
@ -1759,6 +2217,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff"
@ -1796,7 +2263,7 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^5.2.0:
strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@ -1815,11 +2282,23 @@ strip-eof@^1.0.0:
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
strip-json-comments@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
@ -1827,6 +2306,16 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
dependencies:
ajv "^6.10.2"
lodash "^4.17.14"
slice-ansi "^2.1.0"
string-width "^3.0.0"
tar@^4:
version "4.4.6"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b"
@ -1855,6 +2344,11 @@ temptmp@^1.1.0:
dependencies:
del "^3.0.0"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
thirty-two@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
@ -1927,11 +2421,23 @@ tweetnacl@^0.14.3:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
prelude-ls "^1.2.1"
type-fest@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2"
integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==
type-fest@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
union-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@ -1955,6 +2461,13 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
dependencies:
punycode "^2.1.0"
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
@ -1985,6 +2498,11 @@ uuid@8.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
v8-compile-cache@^2.0.3:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
@ -1999,6 +2517,13 @@ which@^1.2.9:
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
@ -2006,11 +2531,23 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
dependencies:
mkdirp "^0.5.1"
ws@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"