diff --git a/package.json b/package.json index 086592718..e19b90fd6 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "codeclimate": "UID=$(id -u) DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker/docker-compose.codeclimate.yml --compatibility up", "snyk-protect": "snyk protect", "prepare": "yarn run snyk-protect", - "cli": "yarn babel-node --extensions \".ts\" src/algorithms/cli.ts" + "cli": "yarn babel-node --extensions \".ts\" src/cli/cli.ts" }, "dependencies": { "@devexpress/dx-core": "2.7.0", @@ -97,6 +97,7 @@ "lodash": "4.17.19", "mathjs": "7.1.0", "moment": "2.27.0", + "neodoc": "2.0.2", "next": "9.4.4", "next-compose-plugins": "2.2.0", "numbro": "2.3.1", diff --git a/src/algorithms/cli.ts b/src/algorithms/cli.ts deleted file mode 100644 index ae3e48246..000000000 --- a/src/algorithms/cli.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable no-console, unicorn/no-process-exit,@typescript-eslint/restrict-template-expressions,@typescript-eslint/no-floating-promises */ -import fs from 'fs' -import yargs from 'yargs' - -import { DEFAULT_SEVERITY_DISTRIBUTION } from '../constants' - -import { run } from './run' -import { getAgeDistributionData } from '../io/defaults/getAgeDistributionData' -import { getSeverityDistributionData } from '../io/defaults/getSeverityDistributionData' - -import type { ScenarioFlat, ScenarioData, SeverityDistributionData, AgeDistributionData } from './types/Param.types' -import { toInternal } from './types/convert' - -const handleRejection: NodeJS.UnhandledRejectionListener = (err) => { - console.error(err) - process.exit(1) -} - -process.on('unhandledRejection', handleRejection) - -/** - * Read a file in JSON format. - * - * @param inputFilename The path to the file. - */ -function readJsonFromFile(inputFilename: string) { - console.log(`Reading data from file ${inputFilename}`) - const inputData = fs.readFileSync(inputFilename, 'utf8') - return JSON.parse(inputData) as T -} - -/** - * Get severity distribution data. If a file is specified on the command - * line, give priority to its contents, else load a default distribution. - * - * @param inputFilename The path to the file. - */ -function getSeverity(inputFilename: string | undefined) { - if (inputFilename) { - const data = readJsonFromFile(inputFilename) - return data.data - } - - return getSeverityDistributionData(DEFAULT_SEVERITY_DISTRIBUTION).data -} - -/** - * Get age distribution data. If a file is specified on the command - * line, give priority to its contents, else load the distribution - * name as specified in the scenario parameters. - * - * @param inputFilename The path to the file. - * @param name The age distribution name to use if no file is - * specified. - */ -function getAge(inputFilename: string | undefined, name: string) { - if (inputFilename) { - const data = readJsonFromFile(inputFilename) - return data.data - } - - return getAgeDistributionData(name).data -} - -async function main() { - // Command line argument processing. - const { argv } = yargs - .options({ - scenario: { type: 'string', demandOption: true, describe: 'Path to scenario parameters JSON file.' }, - age: { type: 'string', describe: 'Path to age distribution JSON file.' }, - severity: { type: 'string', describe: 'Path to severity JSON file.' }, - out: { type: 'string', demandOption: true, describe: 'Path to output file.' }, - }) - .help() - .version(false) - .alias('h', 'help') - - // Read the scenario data. - const scenarioData = readJsonFromFile(argv.scenario) - const scenario = toInternal(scenarioData.data) - const params: ScenarioFlat = { - ...scenario.population, - ...scenario.epidemiological, - ...scenario.simulation, - ...scenario.mitigation, - } - - // Load severity and age data. - const severity = getSeverity(argv.severity) - const ageDistribution = getAge(argv.age, params.ageDistributionName) - - // Run the model. - try { - const outputFile = argv.out - console.log('Running the model') - const result = await run({ params, severity, ageDistribution }) - console.log('Run complete') - console.log(result) - console.log(`Writing output to ${outputFile}`) - fs.writeFileSync(outputFile, JSON.stringify(result)) - } catch (error) { - console.error(`Run failed: ${error}`) - process.exit(1) - } -} - -main() diff --git a/src/cli/cli.ts b/src/cli/cli.ts new file mode 100644 index 000000000..88cea9d34 --- /dev/null +++ b/src/cli/cli.ts @@ -0,0 +1,322 @@ +/* eslint-disable unicorn/no-process-exit,@typescript-eslint/restrict-template-expressions,@typescript-eslint/no-floating-promises */ +import fs from 'fs-extra' +import neodoc from 'neodoc' +import { DEFAULT_SEVERITY_DISTRIBUTION } from '../constants' + +import { run } from '../algorithms/run' +import { appendDash } from '../helpers/appendDash' +import { + ScenarioFlat, + ScenarioData, + SeverityDistributionData, + SeverityDistributionDatum, + SeverityDistributionArray, + AgeDistributionData, + AgeDistributionDatum, + AgeDistributionArray, + Convert, + MitigationInterval, + ScenarioParameters, +} from '../algorithms/types/Param.types' + +import { deserialize } from '../io/serialization/deserialize' +import { DeserializationError } from '../io/serialization/errors' + +import { toInternal } from '../algorithms/types/convert' + +const handleRejection: NodeJS.UnhandledRejectionListener = (err) => { + console.error(err) + process.exit(1) +} + +process.on('unhandledRejection', handleRejection) + +/** + * Run the model //TODO: these docs + * + * @param params: ScenarioFlat it's got some properties to it + * @param severity Severity array + * @param ageDistribution Age distribution array + */ +export async function runModel( + params: ScenarioFlat, + severity: SeverityDistributionDatum[], + ageDistribution: AgeDistributionDatum[], +) { + return run({ params, severity, ageDistribution }) +} + +/** + * Read a file in JSON format. + * + * @param inputFilename The path to the file. + */ +function readJsonFromFile(inputFilename: string) { + console.info(`Reading data from file ${inputFilename}`) + return fs.readJsonSync(inputFilename, { encoding: 'utf8' }) as T +} + +/** + * Get severity distribution data. If a file is specified on the command + * line, give priority to its contents, else load a default distribution. + * + * @param inputFilename The path to the file. + */ +function getSeverity(inputFilename: string | undefined): SeverityDistributionDatum[] { + if (inputFilename) { + const data: SeverityDistributionData = readJsonFromFile(inputFilename) + return data.data + } + + const dataRaw: SeverityDistributionData = readJsonFromFile( + './src/assets/data/severityDistributions.json', + ) + const severityDistributionFound: + | SeverityDistributionData + | undefined = ((dataRaw as unknown) as SeverityDistributionArray).all.find( + (s) => s.name === DEFAULT_SEVERITY_DISTRIBUTION, + ) + if (!severityDistributionFound) { + throw new Error(`Error: scenario not found`) + } + + const severityDistribution = Convert.toSeverityDistributionData(JSON.stringify(severityDistributionFound)) + + severityDistribution.data.sort((a, b) => { + if (a.ageGroup > b.ageGroup) { + return +1 + } + + if (a.ageGroup < b.ageGroup) { + return -1 + } + + return 0 + }) + + return severityDistribution.data +} + +/** + * Get age distribution data. If a file is specified on the command + * line, give priority to its contents, else load the distribution + * name as specified in the scenario parameters. + * + * @param inputFilename The path to the file. + * @param name The age distribution name to use if no file is + * specified. + */ +function getAge(inputFilename: string | undefined, name: string): AgeDistributionDatum[] { + if (inputFilename) { + const data = readJsonFromFile(inputFilename) + return data.data + } + const dataRaw: AgeDistributionData = readJsonFromFile('./src/assets/data/ageDistribution.json') + const ageDistributionFound: AgeDistributionData | undefined = ((dataRaw as unknown) as AgeDistributionArray).all.find( + (cad) => cad.name === name, + ) + if (!ageDistributionFound) { + throw new Error(`Error: country age distribution "${name}" not found in JSON`) + } + + const ageDistribution = Convert.toAgeDistributionData(JSON.stringify(ageDistributionFound)) + + // eslint-disable-next-line sonarjs/no-identical-functions + ageDistribution.data.sort((a, b) => { + if (a.ageGroup > b.ageGroup) { + return +1 + } + + if (a.ageGroup < b.ageGroup) { + return -1 + } + + return 0 + }) + + return ageDistribution.data +} + +async function main() { + // Command line argument processing. + const argv = neodoc.run( + ` + usage: cli [options] + cli mitigation + ( + )... + [options] + options: + Path to scenario parameters JSON file + Path to output file + --age= + Path to age distribution JSON file + --ageDistribution= + Name of country for age distribution + --severity= + Path to severity JSON file + --hospitalStayDays= + Average number of days a severe case stays in regular hospital beds + --icuStayDays= + Average number of days a critical case stays in the Intensive Care Unit (ICU) + --infectiousPeriodDays= + Average number of days a person is infectious + --latencyDays= + Time from infection to onset of symptoms (here onset of infectiousness) + --overflowSeverity= + A multiplicative factor to death rate to patients that require but do not have access to an Intensive Care Unit (ICU) bed relative to those who do + --peakMonth= + Time of the year with peak transmission (month as a number) + --r0Low= + Average number of secondary infections per case (lower bound) + --r0High= + Average number of secondary infections per case (upper bound) + --ageDistributionName= + Name of age distribution data to use + --caseCountsName= + Name of case count data to use + --hospitalBeds= + Number of hospital beds available + --icuBeds= + Number of available beds in Intensive Care Units (ICUs) + --importsPerDay= + Number of cases imported from the outside per day on average + --initialNumberOfCases= + Number of cases present at the start of simulation + --populationServed= + Number of people served by the healthcare system + --numberStochasticRuns= + Number of runs, to account for the uncertainty of parameters. + --mitTimeBegin= + Start of mitigation time period (date in form yyyy-mm-dd) + --mitTimeEnd= + End of mitigation time period (date in form yyyy-mm-dd) + --transmissionReductionLow= + Intervention efficacy as a range of plausible multiplicative reductions of the base growth rate (low bound) + --transmissionReductionHigh= + Intervention efficacy as a range of plausible multiplicative reductions of the base growth rate (high bound) + --simulationRangeBegin= + Beginning of simulation time range (date in form yyyy-mm-dd) + --simulationRangeEnd= + End of simulation time range (date in form yyyy-mm-dd) + --name= + Scenario name + --color= + Colorhex + `, + { smartOptions: true }, + ) + // Read the scenario data. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const scenarioData: ScenarioData = readJsonFromFile(argv['']!) + const scenario = scenarioData.data + + Object.keys(scenario.epidemiological).forEach((key) => { + if (argv[`--${key}`]) { + scenario.epidemiological[key] = argv[`--${key}`] + } + }) + if (argv['--r0Low']) { + scenario.epidemiological.r0.begin = +argv['--r0Low'] + } + if (argv['--r0High']) { + scenario.epidemiological.r0.end = +argv['--r0High'] + } + + Object.keys(scenario.population).forEach((key) => { + if (argv[`--${key}`]) { + scenario.population[key] = argv[`--${key}`] + } + }) + if (argv['--numberStochasticRuns']) { + scenario.simulation.numberStochasticRuns = +argv['--numberStochasticRuns'] + } + + if (argv.mitigation) { + const mitigationIntervals: MitigationInterval[] = [] + for (let i = 0; i < argv[''].length; ++i) { + mitigationIntervals[i] = { + color: scenario.mitigation.mitigationIntervals[0].color, + name: `Intervention ${i + 1}`, + timeRange: { + begin: argv[''][i] + ? argv[''][i] + : scenario.mitigation.mitigationIntervals[0].timeRange.begin, + end: argv[''][i] + ? argv[''][i] + : scenario.mitigation.mitigationIntervals[0].timeRange.end, + }, + transmissionReduction: { + begin: argv[''][i] + ? argv[''][i] + : scenario.mitigation.mitigationIntervals[0].transmissionReduction.begin, + end: argv[''][i] + ? argv[''][i] + : scenario.mitigation.mitigationIntervals[0].transmissionReduction.end, + }, + } + } + scenario.mitigation.mitigationIntervals = mitigationIntervals + } + + const params: ScenarioFlat = { + ...scenario.population, + ...scenario.epidemiological, + ...scenario.simulation, + ...scenario.mitigation, + } + const ageDistributionName: string = argv['--ageDistribution'] ? argv['--ageDistribution'] : params.ageDistributionName + + // Load severity and age data. + const severity = getSeverity(argv['--severity']) + const ageDistribution = getAge(argv['--age'], ageDistributionName) + + scenario.population.ageDistributionName = ageDistributionName + + const scenarioDataToSerialize: ScenarioData = { + name: 'Afghanistan', + data: scenario, + } + const ageDistributionDataToSerialize: AgeDistributionData = { + name: ageDistributionName, + data: ageDistribution, + } + const severityDataToSerialize: SeverityDistributionData = { + name: 'China CDC', + data: severity, + } + const scenarioParamsToSerialize = { + schemaVer: '2.1.0', + scenarioData: scenarioDataToSerialize, + ageDistributionData: ageDistributionDataToSerialize, + severityDistributionData: severityDataToSerialize, + } + try { + deserialize(JSON.stringify(scenarioParamsToSerialize)) + } catch (error) { + if (error instanceof DeserializationError) { + const { errors } = error + console.error(`when deserializing: validation failed:\n${errors.map(appendDash).join('\n')}`) + process.exit(1) + } else { + console.error(`when deserializing: unknown error occured`) + console.log(error) + process.exit(1) + } + } + // Run the model. + try { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const outputFile: string = argv['']! + console.info('Running the model') + const result = await runModel(params, severity, ageDistribution) + console.info('Run complete') + console.info(`Writing output to ${outputFile}`) + fs.writeFileSync(outputFile, JSON.stringify(result)) + } catch (error) { + console.error(`Run failed: ${error}`) + process.exit(1) + } +} + +main() diff --git a/src/cli/input-file.json b/src/cli/input-file.json new file mode 100644 index 000000000..2623e122e --- /dev/null +++ b/src/cli/input-file.json @@ -0,0 +1,50 @@ +{ + "data": { + "epidemiological": { + "hospitalStayDays": 3.0, + "icuStayDays": 14.0, + "infectiousPeriodDays": 3.0, + "latencyDays": 3.0, + "overflowSeverity": 2.0, + "peakMonth": 0, + "r0": { + "begin": 2.12, + "end": 2.59 + }, + "seasonalForcing": 0.0 + }, + "mitigation": { + "mitigationIntervals": [ + { + "color": "#cccccc", + "name": "Intervention 1", + "timeRange": { + "begin": "2020-04-02", + "end": "2020-09-01" + }, + "transmissionReduction": { + "begin": 34.4, + "end": 41.6 + } + } + ] + }, + "population": { + "ageDistributionName": "Afghanistan", + "caseCountsName": "Afghanistan", + "hospitalBeds": 17470, + "icuBeds": 524, + "importsPerDay": 0.1, + "initialNumberOfCases": 58, + "populationServed": 37172386 + }, + "simulation": { + "numberStochasticRuns": 15, + "simulationTimeRange": { + "begin": "2020-03-03", + "end": "2020-08-31" + } + } + }, + "name": "Afghanistan" +} diff --git a/src/io/serialization/v2.0.0/serialize.ts b/src/io/serialization/v2.0.0/serialize.ts index 96f7b273f..a7e06201e 100644 --- a/src/io/serialization/v2.0.0/serialize.ts +++ b/src/io/serialization/v2.0.0/serialize.ts @@ -4,7 +4,7 @@ import { trim } from 'lodash' import Ajv from 'ajv' import ajvLocalizers from 'ajv-i18n' -import validateShareable, { errors } from '../../../.generated/2.0.0/validateShareable' +import validateShareable from '../../../.generated/2.0.0/validateShareable' import { Convert } from '../../../.generated/2.0.0/types' import type { ScenarioParameters, Shareable } from '../../../algorithms/types/Param.types' @@ -25,11 +25,11 @@ function validateSchema(shareableDangerous: Record) { if (!validateShareable(shareableDangerous)) { const locale = 'en' // TODO: use current locale const localize = ajvLocalizers[locale] ?? ajvLocalizers.en - localize(errors) + localize(validateShareable.errors) const ajv = Ajv({ allErrors: true }) const separator = '<<>>' - const errorString = ajv.errorsText(errors, { dataVar: '', separator }) + const errorString = ajv.errorsText(validateShareable.errors, { dataVar: '', separator }) if (typeof errorString === 'string') { const errorStrings = errorString.split(separator).map(trim) if (errorStrings.length > 0) { diff --git a/src/io/serialization/v2.1.0/serialize.ts b/src/io/serialization/v2.1.0/serialize.ts index 47720f4d9..a5da65b77 100644 --- a/src/io/serialization/v2.1.0/serialize.ts +++ b/src/io/serialization/v2.1.0/serialize.ts @@ -3,7 +3,7 @@ import { trim, isEqual } from 'lodash' import Ajv from 'ajv' import ajvLocalizers from 'ajv-i18n' -import validateShareable, { errors } from '../../../.generated/latest/validateShareable' +import validateShareable from '../../../.generated/latest/validateShareable' import type { ScenarioParameters, Shareable } from '../../../algorithms/types/Param.types' import { Convert } from '../../../algorithms/types/Param.types' @@ -26,7 +26,7 @@ function serialize(scenarioParameters: ScenarioParameters): string { const serialized = Convert.shareableToJson(shareable) if (process.env.NODE_ENV !== 'production' && !validateShareable(JSON.parse(serialized))) { - throw errors + throw validateShareable.errors } return serialized @@ -36,11 +36,11 @@ function validateSchema(shareableDangerous: Record) { if (!validateShareable(shareableDangerous)) { const locale = 'en' // TODO: use current locale const localize = ajvLocalizers[locale] ?? ajvLocalizers.en - localize(errors) + localize(validateShareable.errors) const ajv = Ajv({ allErrors: true }) const separator = '<<>>' - const errorString = ajv.errorsText(errors, { dataVar: '', separator }) + const errorString = ajv.errorsText(validateShareable.errors, { dataVar: '', separator }) if (typeof errorString === 'string') { const errorStrings = errorString.split(separator).map(trim) if (errorStrings.length > 0) { diff --git a/src/types/neodoc.d.ts b/src/types/neodoc.d.ts new file mode 100644 index 000000000..e4a5c2de2 --- /dev/null +++ b/src/types/neodoc.d.ts @@ -0,0 +1,72 @@ +declare module 'neodoc' { + import { Required } from 'utility-types' + + export declare type NeodocSpec = Record + + export declare interface NeodocOptions { + // Do not exit upon error or when parsing --help or --version. Instead throw and error / return the value. + dontExit?: boolean + + // Override process.env + env?: Record + + // Override process.argv + argv?: Record + + // Parse until the first command or argument, then collect the rest into an array, given the help + // indicates another, repeatable, positional argument, e.g. : [options] [...] + optionsFirst?: boolean + + // Enable parsing groups that "look like" options as options. For example: [-f ARG...] means [-f=ARG...] + smartOptions?: boolean + + // Stop parsing at the given options, i.e. [ -n ]. It's value will be the rest of argv. + stopAt?: string + + // Require flags be present in the input. In neodoc, flags are optional by default and can be omitted. This option + // forces the user to pass flags explicitly, failing the parse otherwise. + requireFlags?: boolean + + // Relax placement rules. Positionals and commands are no longer solid anchors. The order amongs them, however, + // remains fixed. This implies that options can appear anywhere. + laxPlacement?: boolean + + // An array of flags that trigger the special version behavior: Print the program version and exit with code 0. + versionFlags?: string[] + + // The version to print for the special version behavior. Defaults to finding the version of the nearest + // package.json file, relative to the executing main module. Note that disk IO is only performed if + // opts.versionFlags is non-empty and opts.version is not set. + version?: string + + // An array of flags that trigger the special help behavior: Print the full program help text and exit with code 0. + helpFlags?: string[] + + // Allow options to be repeated even if the spec does not explicitly allow this. This "loosens" up the parser to + // accept more input and makes for a more intuitive command line. Please note: repeatability is still subject to + // chunking (use laxPlacement to relax this further). + repeatableOptions?: boolean + + transforms?: { + // an array of functions to be called prior to "solving" the input. This function takes the spec as it's only + // parameter. At this point, the spec is mostly untouched by neodoc with the exception of smart-options which + // runs as a fixed transform prior to user-provided callbacks if smart-options is true. Transforms that need to + // be aware of option stacks and [...-options] references should run here as this information is lost + // during the solving transforms. + presolve?(spec: NeodocSpec): NeodocSpec + + // an array of functions to be called after "solving" the input, just prior to passing the spec to the arg-parser. + // This function takes the spec as it's only parameter. At this point, the spec has been fully solved, expanded + // and canonicalised. + postsolve?(spec: NeodocSpec): NeodocSpec + } + + // Collect unknown options under a special key ? instead of failing. Useful to send an unknown subset of + // options to another program. + allowUnknown: string + } + + declare function run(doc: string, NeodocOptions): Record + + export default { run } +} diff --git a/tsconfig.cli.json b/tsconfig.cli.json new file mode 100644 index 000000000..2ea7701a6 --- /dev/null +++ b/tsconfig.cli.json @@ -0,0 +1,61 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "*": ["src/types/*", "src/generated/*"], + "src/*": ["./src/*"] + }, + "target": "esnext", + "module": "esnext", + "lib": ["esnext", "dom", "webworker"], + "jsx": "preserve", + "moduleResolution": "node", + "allowJs": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "preserveConstEnums": true, + "removeComments": false, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true + }, + "include": ["src/cli/**/*"], + "exclude": [ + ".build", + ".cache", + ".idea", + ".ignore", + ".reports", + "3rdparty", + "cypress", + "data", + "docker", + "docs", + "node_modules", + "public", + "src/.generated", + "tsconfig.json", + "src/**/*.spec.*", + "src/**/*.test.*", + "src/**/__tests__/**/*", + "src/algorithms/__test_data__/**/*", + // FIXME: errors in these files have to be resolved eventually + // begin + "src/algorithms/model.ts", // FIXME + "src/algorithms/results.ts", // FIXME + "src/components/Main/Results/AgeBarChart.tsx", // FIXME + "src/components/Main/Results/DeterministicLinePlot.tsx", // FIXME + "src/components/Main/Results/Utils.ts" // FIXME + // end + ] +} diff --git a/tsconfig.json b/tsconfig.json index 8f9be2dcc..84a2e2ec7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -56,6 +56,14 @@ "node_modules", "public", "src/.generated", - "tsconfig.json" + "tsconfig.json", + // FIXME: errors in these files have to be resolved eventually + // begin + "src/algorithms/model.ts", // FIXME + "src/algorithms/results.ts", // FIXME + "src/components/Main/Results/AgeBarChart.tsx", // FIXME + "src/components/Main/Results/DeterministicLinePlot.tsx", // FIXME + "src/components/Main/Results/Utils.ts" // FIXME + // end ] } diff --git a/yarn.lock b/yarn.lock index 046ab47f0..92ff79a6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12089,6 +12089,13 @@ neo-async@^2.5.0, neo-async@^2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +neodoc@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/neodoc/-/neodoc-2.0.2.tgz#ad00b30b9758379dcd3cf752a0659bacbab2c4fb" + integrity sha512-NAppJ0YecKWdhSXFYCHbo6RutiX8vOt/Jo3l46mUg6pQlpJNaqc5cGxdrW2jITQm5JIYySbFVPDl3RrREXNyPw== + dependencies: + ansi-regex "^2.0.0" + netmask@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"