From 60d7e0b561aafd78b8260f7c49aeb3bafcd87918 Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Fri, 6 Nov 2020 23:51:23 +0100 Subject: [PATCH] rewrite with commander --- .gitignore | 6 +- package.json | 8 +- src/autorestic.ts | 63 ---------- src/backend.ts | 82 ++++++------- src/backup.ts | 144 +++++++++++----------- src/config.ts | 9 +- src/cron.ts | 6 +- src/forget.ts | 94 +++++++-------- src/handlers.ts | 244 -------------------------------------- src/handlers/backup.ts | 13 ++ src/handlers/check.ts | 9 ++ src/handlers/cron.ts | 7 ++ src/handlers/exec.ts | 14 +++ src/handlers/forget.ts | 13 ++ src/handlers/info.ts | 18 +++ src/handlers/install.ts | 50 ++++++++ src/handlers/restore.ts | 9 ++ src/handlers/uninstall.ts | 13 ++ src/handlers/upgrade.ts | 37 ++++++ src/index.ts | 104 ++++++++++++++++ src/info.ts | 32 ++--- src/lock.ts | 10 +- src/restore.ts | 101 +++++++--------- src/types.ts | 113 +++++++++--------- src/utils.ts | 35 +++++- tsconfig.json | 1 + yarn.lock | 233 +++++++++++++++++------------------- 27 files changed, 702 insertions(+), 766 deletions(-) delete mode 100644 src/autorestic.ts delete mode 100644 src/handlers.ts create mode 100644 src/handlers/backup.ts create mode 100644 src/handlers/check.ts create mode 100644 src/handlers/cron.ts create mode 100644 src/handlers/exec.ts create mode 100644 src/handlers/forget.ts create mode 100644 src/handlers/info.ts create mode 100644 src/handlers/install.ts create mode 100644 src/handlers/restore.ts create mode 100644 src/handlers/uninstall.ts create mode 100644 src/handlers/upgrade.ts create mode 100644 src/index.ts diff --git a/.gitignore b/.gitignore index 8047093..8e35ddb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,9 @@ data restore docker Dockerfile +build # Config .autorestic.yml .autorestic.lock -.docker.yml - -# Docs -.codedoc \ No newline at end of file +.docker.yml \ No newline at end of file diff --git a/package.json b/package.json index e468ae9..39c928d 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,9 @@ "private": true, "scripts": { "build": "tsc", - "build:watch": "tsc -w", - "dev": "tsnd --no-notify --respawn ./src/autorestic.ts", - "move": "mv bin/autorestic-linux bin/autorestic_linux_x64 && mv bin/autorestic-macos bin/autorestic_macos_x64", - "bin": "yarn run build && pkg lib/autorestic.js --targets latest-macos-x64,latest-linux-x64 --out-path bin && yarn run move", + "dev": "tsc -w", + "move": "mv bin/index-linux bin/autorestic_linux_x64 && mv bin/index-macos bin/autorestic_macos_x64", + "bin": "yarn run build && pkg lib/index.js --targets latest-macos-x64,latest-linux-x64 --out-path bin && yarn run move", "docs:build": "codedoc install && codedoc build", "docs:dev": "codedoc serve" }, @@ -25,7 +24,6 @@ "commander": "^6.2.0", "cron-parser": "2.x.x", "js-yaml": "3.x.x", - "minimist": "1.x.x", "uhrwerk": "1.x.x" } } diff --git a/src/autorestic.ts b/src/autorestic.ts deleted file mode 100644 index 7220539..0000000 --- a/src/autorestic.ts +++ /dev/null @@ -1,63 +0,0 @@ -import 'colors' -import minimist from 'minimist' - -import { init } from './config' -import handlers, { error, help } from './handlers' -import { Config } from './types' -import { readLock, writeLock, unlock } from './lock' - -process.on('uncaughtException', (err) => { - console.log(err.message) - unlock() - process.exit(1) -}) - -export const { _: commands, ...flags } = minimist(process.argv.slice(2), { - alias: { - c: 'config', - v: 'version', - h: 'help', - a: 'all', - l: 'location', - b: 'backend', - d: 'dry-run', - }, - boolean: ['a', 'd'], - string: ['l', 'b'], -}) - -export const VERSION = '0.20' -export const INSTALL_DIR = '/usr/local/bin' -export const VERBOSE = flags.verbose - -export let config: Config - -async function main() { - config = init() - - // Don't let 2 instances run on the same config - const lock = readLock() - if (lock.running) { - console.log('An instance of autorestic is already running for this config file'.red) - return - } - writeLock({ - ...lock, - running: true, - }) - - // For dev - // return await handlers['cron']([], { ...flags, all: true }) - - if (commands.length < 1 || commands[0] === 'help') return help() - - const command: string = commands[0] - const args: string[] = commands.slice(1) - - const fn = handlers[command] || error - await fn(args, flags) -} - -main() - .catch((e: Error) => console.error(e.message)) - .finally(unlock) diff --git a/src/backend.ts b/src/backend.ts index 566f89e..7b75334 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -1,74 +1,66 @@ import { Writer } from 'clitastic' -import { config, VERBOSE } from './autorestic' +import { config, VERBOSE } from './' import { Backend, Backends, Locations } from './types' import { exec, pathRelativeToConfigFile, filterObjectByKey } from './utils' - - const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/ export const getPathFromBackend = (backend: Backend): string => { - switch (backend.type) { - case 'local': - return pathRelativeToConfigFile(backend.path) - case 'b2': - case 'azure': - case 'gs': - case 's3': - case 'sftp': - case 'rest': - return `${backend.type}:${backend.path}` - default: - throw new Error(`Unknown backend type.`) - } + switch (backend.type) { + case 'local': + return pathRelativeToConfigFile(backend.path) + case 'b2': + case 'azure': + case 'gs': + case 's3': + case 'sftp': + case 'rest': + return `${backend.type}:${backend.path}` + default: + throw new Error(`Unknown backend type.`) + } } export const getEnvFromBackend = (backend: Backend) => { - const { type, path, key, ...rest } = backend - return { - RESTIC_PASSWORD: key, - RESTIC_REPOSITORY: getPathFromBackend(backend), - ...rest, - } + const { type, path, key, ...rest } = backend + return { + RESTIC_PASSWORD: key, + RESTIC_REPOSITORY: getPathFromBackend(backend), + ...rest, + } } export const getBackendsFromLocations = (locations: Locations): string[] => { - const backends = new Set() - for (const to of Object.values(locations).map(location => location.to)) - Array.isArray(to) ? to.forEach(t => backends.add(t)) : backends.add(to) - return Array.from(backends) + const backends = new Set() + for (const to of Object.values(locations).map((location) => location.to)) Array.isArray(to) ? to.forEach((t) => backends.add(t)) : backends.add(to) + return Array.from(backends) } export const checkAndConfigureBackend = (name: string, backend: Backend) => { - const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳') - try { - const env = getEnvFromBackend(backend) + const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳') + try { + const env = getEnvFromBackend(backend) - const { out, err } = exec('restic', ['init'], { env }) + const { out, err } = exec('restic', ['init'], { env }) - if (err.length > 0 && !ALREADY_EXISTS.test(err)) - throw new Error(`Could not load the backend "${name}": ${err}`) + if (err.length > 0 && !ALREADY_EXISTS.test(err)) throw new Error(`Could not load the backend "${name}": ${err}`) - if (VERBOSE && out.length > 0) console.log(out) + if (VERBOSE && out.length > 0) console.log(out) - writer.done(name.blue + ' : ' + 'Done ✓'.green) - } catch (e) { - writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red) - } + writer.done(name.blue + ' : ' + 'Done ✓'.green) + } catch (e) { + writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red) + } } export const checkAndConfigureBackends = (backends?: Backends) => { - if (!backends) - backends = config.backends + if (!backends) backends = config.backends - console.log('\nConfiguring Backends'.grey.underline) - for (const [name, backend] of Object.entries(backends)) - checkAndConfigureBackend(name, backend) + console.log('\nConfiguring Backends'.grey.underline) + for (const [name, backend] of Object.entries(backends)) checkAndConfigureBackend(name, backend) } export const checkAndConfigureBackendsForLocations = (locations: Locations) => { - checkAndConfigureBackends( - filterObjectByKey(config.backends, getBackendsFromLocations(locations)), - ) + checkAndConfigureBackends(filterObjectByKey(config.backends, getBackendsFromLocations(locations))) } diff --git a/src/backup.ts b/src/backup.ts index a29880e..47631f2 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -1,111 +1,103 @@ import { Writer } from 'clitastic' import { mkdirSync } from 'fs' -import { config, VERBOSE } from './autorestic' +import { config, VERBOSE } from './' import { getEnvFromBackend } from './backend' import { LocationFromPrefixes } from './config' import { Locations, Location, Backend } from './types' import { - exec, - pathRelativeToConfigFile, - getFlagsFromLocation, - makeArrayIfIsNot, - execPlain, - MeasureDuration, - fill, - decodeLocationFromPrefix, - checkIfDockerVolumeExistsOrFail, - getPathFromVolume, + exec, + pathRelativeToConfigFile, + getFlagsFromLocation, + makeArrayIfIsNot, + execPlain, + MeasureDuration, + fill, + decodeLocationFromPrefix, + checkIfDockerVolumeExistsOrFail, + getPathFromVolume, } from './utils' - - export const backupFromFilesystem = (from: string, location: Location, backend: Backend, tags?: string[]) => { - const path = pathRelativeToConfigFile(from) + const path = pathRelativeToConfigFile(from) - const { out, err, status } = exec( - 'restic', - ['backup', '.', ...getFlagsFromLocation(location, 'backup')], - { env: getEnvFromBackend(backend), cwd: path }, - ) + const { out, err, status } = exec('restic', ['backup', '.', ...getFlagsFromLocation(location, 'backup')], { + env: getEnvFromBackend(backend), + cwd: path, + }) - if (VERBOSE) console.log(out, err) - if (status != 0 || err.length > 0) - throw new Error(err) + if (VERBOSE) console.log(out, err) + if (status != 0 || err.length > 0) throw new Error(err) } export const backupFromVolume = (volume: string, location: Location, backend: Backend) => { - const tmp = getPathFromVolume(volume) - try { - mkdirSync(tmp) - checkIfDockerVolumeExistsOrFail(volume) + const tmp = getPathFromVolume(volume) + try { + mkdirSync(tmp) + checkIfDockerVolumeExistsOrFail(volume) - // For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost. - // execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`) - execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`) + // For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost. + // execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`) + execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`) - backupFromFilesystem(tmp, location, backend) - } catch (e) { - throw e - } finally { - execPlain(`rm -rf ${tmp}`) - } + backupFromFilesystem(tmp, location, backend) + } catch (e) { + throw e + } finally { + execPlain(`rm -rf ${tmp}`) + } } export const backupSingle = (name: string, to: string, location: Location) => { - const delta = new MeasureDuration() - const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳') + const delta = new MeasureDuration() + const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳') - try { - const backend = config.backends[to] - const [type, value] = decodeLocationFromPrefix(location.from) + try { + const backend = config.backends[to] + const [type, value] = decodeLocationFromPrefix(location.from) - switch (type) { + switch (type) { + case LocationFromPrefixes.Filesystem: + backupFromFilesystem(value, location, backend) + break - case LocationFromPrefixes.Filesystem: - backupFromFilesystem(value, location, backend) - break + case LocationFromPrefixes.DockerVolume: + backupFromVolume(value, location, backend) + break + } - case LocationFromPrefixes.DockerVolume: - backupFromVolume(value, location, backend) - break - - } - - writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`) - } catch (e) { - writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`) - } + writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`) + } catch (e) { + writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`) + } } export const backupLocation = (name: string, location: Location) => { - const display = name.yellow + ' ▶ ' - const filler = fill(name.length + 3) - let first = true + const display = name.yellow + ' ▶ ' + const filler = fill(name.length + 3) + let first = true - if (location.hooks && location.hooks.before) - for (const command of makeArrayIfIsNot(location.hooks.before)) { - const cmd = execPlain(command, {}) - console.log(cmd.out, cmd.err) - } + if (location.hooks && location.hooks.before) + for (const command of makeArrayIfIsNot(location.hooks.before)) { + const cmd = execPlain(command, {}) + console.log(cmd.out, cmd.err) + } - for (const t of makeArrayIfIsNot(location.to)) { - backupSingle(first ? display : filler, t, location) - if (first) first = false - } + for (const t of makeArrayIfIsNot(location.to)) { + backupSingle(first ? display : filler, t, location) + if (first) first = false + } - if (location.hooks && location.hooks.after) - for (const command of makeArrayIfIsNot(location.hooks.after)) { - const cmd = execPlain(command) - console.log(cmd.out, cmd.err) - } + if (location.hooks && location.hooks.after) + for (const command of makeArrayIfIsNot(location.hooks.after)) { + const cmd = execPlain(command) + console.log(cmd.out, cmd.err) + } } export const backupAll = (locations?: Locations) => { - if (!locations) - locations = config.locations + if (!locations) locations = config.locations - console.log('\nBacking Up'.underline.grey) - for (const [name, location] of Object.entries(locations)) - backupLocation(name, location) + console.log('\nBacking Up'.underline.grey) + for (const [name, location] of Object.entries(locations)) backupLocation(name, location) } diff --git a/src/config.ts b/src/config.ts index 84c0e34..67b5568 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,6 @@ import { homedir } from 'os' import yaml from 'js-yaml' import CronParser from 'cron-parser' -import { flags } from './autorestic' import { Backend, Config } from './types' import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils' @@ -56,9 +55,9 @@ export const normalizeAndCheckLocations = (config: Config) => { } } -const findConfigFile = (): string => { +const findConfigFile = (custom: string): string => { const config = '.autorestic.yml' - const paths = [resolve(flags.config || ''), resolve('./' + config), homedir() + '/' + config] + const paths = [resolve(custom || ''), resolve('./' + config), homedir() + '/' + config] for (const path of paths) { try { const file = statSync(path) @@ -70,8 +69,8 @@ const findConfigFile = (): string => { export let CONFIG_FILE: string = '' -export const init = (): Config => { - const file = findConfigFile() +export const init = (custom: string): Config => { + const file = findConfigFile(custom) CONFIG_FILE = file const parsed = yaml.safeLoad(readFileSync(CONFIG_FILE).toString()) diff --git a/src/cron.ts b/src/cron.ts index 5f52da4..fe1bd74 100644 --- a/src/cron.ts +++ b/src/cron.ts @@ -1,12 +1,11 @@ import CronParser from 'cron-parser' -import { config } from './autorestic' +import { config } from './' import { checkAndConfigureBackendsForLocations } from './backend' import { Location } from './types' import { backupLocation } from './backup' import { readLock, writeLock } from './lock' - const runCronForLocation = (name: string, location: Location) => { const lock = readLock() const parsed = CronParser.parseExpression(location.cron || '') @@ -26,8 +25,7 @@ export const runCron = () => { checkAndConfigureBackendsForLocations(Object.fromEntries(locationsWithCron)) console.log('\nRunning cron jobs'.underline.gray) - for (const [name, location] of locationsWithCron) - runCronForLocation(name, location) + for (const [name, location] of locationsWithCron) runCronForLocation(name, location) console.log('\nFinished!'.underline + ' 🎉') } diff --git a/src/forget.ts b/src/forget.ts index b4d47cc..7638481 100644 --- a/src/forget.ts +++ b/src/forget.ts @@ -1,75 +1,61 @@ import { Writer } from 'clitastic' -import { config, VERBOSE } from './autorestic' +import { config, VERBOSE } from './' import { getEnvFromBackend } from './backend' import { LocationFromPrefixes } from './config' import { Locations, Location, Flags } from './types' -import { - exec, - pathRelativeToConfigFile, - getFlagsFromLocation, - makeArrayIfIsNot, - fill, decodeLocationFromPrefix, getPathFromVolume, -} from './utils' - - +import { exec, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, fill, decodeLocationFromPrefix, getPathFromVolume } from './utils' export const forgetSingle = (name: string, to: string, location: Location, dryRun: boolean) => { - const base = name + to.blue + ' : ' - const writer = new Writer(base + 'Removing old snapshots… ⏳') + const base = name + to.blue + ' : ' + const writer = new Writer(base + 'Removing old snapshots… ⏳') - const backend = config.backends[to] - const flags = getFlagsFromLocation(location, 'forget') + const backend = config.backends[to] + const flags = getFlagsFromLocation(location, 'forget') - const [type, value] = decodeLocationFromPrefix(location.from) - let path: string - switch (type) { - case LocationFromPrefixes.Filesystem: - path = pathRelativeToConfigFile(value) - break - case LocationFromPrefixes.DockerVolume: - path = getPathFromVolume(value) - break - } + const [type, value] = decodeLocationFromPrefix(location.from) + let path: string + switch (type) { + case LocationFromPrefixes.Filesystem: + path = pathRelativeToConfigFile(value) + break + case LocationFromPrefixes.DockerVolume: + path = getPathFromVolume(value) + break + } - if (flags.length == 0) { - writer.done(base + 'Skipping, no policy declared') - return - } - if (dryRun) flags.push('--dry-run') + if (flags.length == 0) { + writer.done(base + 'Skipping, no policy declared') + return + } + if (dryRun) flags.push('--dry-run') - writer.replaceLn(base + 'Forgetting old snapshots… ⏳') - const cmd = exec( - 'restic', - ['forget', '--path', path, '--prune', ...flags], - { env: getEnvFromBackend(backend) }, - ) + writer.replaceLn(base + 'Forgetting old snapshots… ⏳') + const cmd = exec('restic', ['forget', '--path', path, '--prune', ...flags], { env: getEnvFromBackend(backend) }) - if (VERBOSE) console.log(cmd.out, cmd.err) - writer.done(base + 'Done ✓'.green) + if (VERBOSE) console.log(cmd.out, cmd.err) + writer.done(base + 'Done ✓'.green) } export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => { - const display = name.yellow + ' ▶ ' - const filler = fill(name.length + 3) - let first = true + const display = name.yellow + ' ▶ ' + const filler = fill(name.length + 3) + let first = true - for (const t of makeArrayIfIsNot(backup.to)) { - const nameOrBlankSpaces: string = first ? display : filler - forgetSingle(nameOrBlankSpaces, t, backup, dryRun) - if (first) first = false - } + for (const t of makeArrayIfIsNot(backup.to)) { + const nameOrBlankSpaces: string = first ? display : filler + forgetSingle(nameOrBlankSpaces, t, backup, dryRun) + if (first) first = false + } } -export const forgetAll = (backups?: Locations, flags?: Flags) => { - if (!backups) { - backups = config.locations - } +export const forgetAll = (backups?: Locations, dryRun = false) => { + if (!backups) { + backups = config.locations + } - console.log('\nRemoving old snapshots according to policy'.underline.grey) - const dryRun = flags ? flags['dry-run'] : false - if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow) + console.log('\nRemoving old snapshots according to policy'.underline.grey) + if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow) - for (const [name, backup] of Object.entries(backups)) - forgetLocation(name, backup, dryRun) + for (const [name, backup] of Object.entries(backups)) forgetLocation(name, backup, dryRun) } diff --git a/src/handlers.ts b/src/handlers.ts deleted file mode 100644 index 51f057b..0000000 --- a/src/handlers.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { chmodSync, renameSync, unlinkSync } from 'fs' -import { tmpdir } from 'os' -import { join, resolve } from 'path' - -import axios from 'axios' -import { Writer } from 'clitastic' - -import { config, INSTALL_DIR, VERSION } from './autorestic' -import { checkAndConfigureBackends, getEnvFromBackend, checkAndConfigureBackendsForLocations } from './backend' -import { backupAll } from './backup' -import { runCron } from './cron' -import { forgetAll } from './forget' -import showAll from './info' -import { restoreSingle } from './restore' -import { Backends, Flags, Locations } from './types' -import { - checkIfCommandIsAvailable, - checkIfResticIsAvailable, - downloadFile, - exec, - filterObjectByKey, - makeArrayIfIsNot, -} from './utils' - - - -export type Handlers = { - [command: string]: (args: string[], flags: Flags) => void -} - -const parseBackend = (flags: Flags): Backends => { - if (!flags.all && !flags.backend) - throw new Error( - 'No backends specified.'.red + - '\n--all [-a]\t\t\t\tCheck all.' + - '\n--backend [-b] myBackend\t\tSpecify one or more backend', - ) - if (flags.all) return config.backends - else { - const backends = makeArrayIfIsNot(flags.backend) - for (const backend of backends) - if (!config.backends[backend]) - throw new Error('Invalid backend: '.red + backend) - return filterObjectByKey(config.backends, backends) - } -} - -const parseLocations = (flags: Flags): Locations => { - if (!flags.all && !flags.location) - throw new Error( - 'No locations specified.'.red + - '\n--all [-a]\t\t\t\tBackup all.' + - '\n--location [-l] site1\t\t\tSpecify one or more locations', - ) - - if (flags.all) { - return config.locations - } else { - const locations = makeArrayIfIsNot(flags.location) - for (const location of locations) - if (!config.locations[location]) - throw new Error('Invalid location: '.red + location) - return filterObjectByKey(config.locations, locations) - } -} - -const handlers: Handlers = { - check(args, flags) { - checkIfResticIsAvailable() - const backends = parseBackend(flags) - checkAndConfigureBackends(backends) - }, - backup(args, flags) { - checkIfResticIsAvailable() - const locations: Locations = parseLocations(flags) - checkAndConfigureBackendsForLocations(locations) - backupAll(locations) - - console.log('\nFinished!'.underline + ' 🎉') - }, - cron(args, flags) { - checkIfResticIsAvailable() - runCron() - }, - restore(args, flags) { - checkIfResticIsAvailable() - - const locations = parseLocations(flags) - const keys = Object.keys(locations) - if (keys.length < 1) throw new Error(`You need to specify the location to restore with --location`.red) - if (keys.length > 2) throw new Error(`Only one location is supported at a time when restoring`.red) - - restoreSingle(keys[0], flags.from, flags.to) - }, - forget(args, flags) { - checkIfResticIsAvailable() - const locations: Locations = parseLocations(flags) - checkAndConfigureBackendsForLocations(locations) - forgetAll(locations, flags) - - console.log('\nFinished!'.underline + ' 🎉') - }, - exec(args, flags) { - checkIfResticIsAvailable() - const backends = parseBackend(flags) - for (const [name, backend] of Object.entries(backends)) { - console.log(`\n${name}:\n`.grey.underline) - const env = getEnvFromBackend(backend) - - const { out, err } = exec('restic', args, { env }) - console.log(out, err) - } - }, - info() { - showAll() - }, - async install() { - try { - checkIfResticIsAvailable() - console.log('Restic is already installed') - return - } catch { - } - - const w = new Writer('Checking latest version... ⏳') - checkIfCommandIsAvailable('bzip2') - const { data: json } = await axios({ - method: 'get', - url: 'https://api.github.com/repos/restic/restic/releases/latest', - responseType: 'json', - }) - - const archMap: { [a: string]: string } = { - x32: '386', - x64: 'amd64', - } - - w.replaceLn('Downloading binary... 🌎') - const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2` - const dl = json.assets.find((asset: any) => asset.name === name) - if (!dl) - return console.log( - 'Cannot get the right binary.'.red, - 'Please see https://bit.ly/2Y1Rzai', - ) - - const tmp = join(tmpdir(), name) - const extracted = tmp.slice(0, -4) //without the .bz2 - - await downloadFile(dl.browser_download_url, tmp) - - w.replaceLn('Decompressing binary... 📦') - exec('bzip2', ['-dk', tmp]) - unlinkSync(tmp) - - w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`) - chmodSync(extracted, 0o755) - renameSync(extracted, INSTALL_DIR + '/restic') - - w.done( - `\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉', - ) - }, - uninstall() { - for (const bin of ['restic', 'autorestic']) - try { - unlinkSync(INSTALL_DIR + '/' + bin) - console.log(`Finished! ${bin} was uninstalled`) - } catch (e) { - console.log(`${bin} is already uninstalled`.red) - } - }, - async update() { - checkIfResticIsAvailable() - const w = new Writer('Checking for latest restic version... ⏳') - exec('restic', ['self-update']) - - w.replaceLn('Checking for latest autorestic version... ⏳') - const { data: json } = await axios({ - method: 'get', - url: - 'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest', - responseType: 'json', - }) - - if (json.tag_name != VERSION) { - const platformMap: { [key: string]: string } = { - darwin: 'macos', - } - - const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}` - const dl = json.assets.find((asset: any) => asset.name === name) - - const to = INSTALL_DIR + '/autorestic' - w.replaceLn('Downloading binary... 🌎') - await downloadFile(dl.browser_download_url, to) - - chmodSync(to, 0o755) - } - - w.done('All up to date! 🚀') - }, - version() { - console.log('version'.grey, VERSION) - }, -} - -export const help = () => { - console.log( - '\nAutorestic'.blue + - ` - ${VERSION} - Easy Restic CLI Utility` + - '\n' + - '\nOptions:'.yellow + - `\n -c, --config Specify config file. Default: .autorestic.yml` + - '\n' + - '\nCommands:'.yellow + - '\n info Show all locations and backends' + - '\n check [-b, --backend] [-a, --all] Check backends' + - '\n backup [-l, --location] [-a, --all] Backup all or specified locations' + - '\n forget [-l, --location] [-a, --all] [--dry-run] Forget old snapshots according to declared policies' + - '\n restore [-l, --location] [--from backend] [--to ] Restore all or specified locations' + - '\n' + - '\n exec [-b, --backend] [-a, --all] -- [native options] Execute native restic command' + - '\n' + - '\n install install restic' + - '\n uninstall uninstall restic' + - '\n update update restic' + - '\n help Show help' + - '\n' + - '\nExamples: '.yellow + - 'https://git.io/Jf0x6' + - '\n', - ) -} - -export const error = () => { - help() - console.log( - `Invalid Command:`.red.underline, - `${process.argv.slice(2).join(' ')}`, - ) -} - -export default handlers diff --git a/src/handlers/backup.ts b/src/handlers/backup.ts new file mode 100644 index 0000000..11e68d1 --- /dev/null +++ b/src/handlers/backup.ts @@ -0,0 +1,13 @@ +import { checkAndConfigureBackendsForLocations } from '../backend' +import { backupAll } from '../backup' +import { Flags, Locations } from '../types' +import { checkIfResticIsAvailable, parseLocations } from '../utils' + +export default function backup({ location, all }: Flags) { + checkIfResticIsAvailable() + const locations: Locations = parseLocations(location, all) + checkAndConfigureBackendsForLocations(locations) + backupAll(locations) + + console.log('\nFinished!'.underline + ' 🎉') +} diff --git a/src/handlers/check.ts b/src/handlers/check.ts new file mode 100644 index 0000000..df1d862 --- /dev/null +++ b/src/handlers/check.ts @@ -0,0 +1,9 @@ +import { checkAndConfigureBackends } from '../backend' +import { Flags } from '../types' +import { checkIfResticIsAvailable, parseBackend } from '../utils' + +export default function check({ backend, all }: Flags) { + checkIfResticIsAvailable() + const backends = parseBackend(backend, all) + checkAndConfigureBackends(backends) +} diff --git a/src/handlers/cron.ts b/src/handlers/cron.ts new file mode 100644 index 0000000..1d1512b --- /dev/null +++ b/src/handlers/cron.ts @@ -0,0 +1,7 @@ +import { runCron } from '../cron' +import { checkIfResticIsAvailable } from '../utils' + +export function cron() { + checkIfResticIsAvailable() + runCron() +} diff --git a/src/handlers/exec.ts b/src/handlers/exec.ts new file mode 100644 index 0000000..35b0045 --- /dev/null +++ b/src/handlers/exec.ts @@ -0,0 +1,14 @@ +import { getEnvFromBackend } from '../backend' +import { Flags } from '../types' +import { checkIfResticIsAvailable, exec as execCLI, parseBackend } from '../utils' + +export default function exec({ backend, all }: Flags, args: string[]) { + checkIfResticIsAvailable() + const backends = parseBackend(backend, all) + for (const [name, backend] of Object.entries(backends)) { + console.log(`\n${name}:\n`.grey.underline) + const env = getEnvFromBackend(backend) + const { out, err } = execCLI('restic', args, { env }) + console.log(out, err) + } +} diff --git a/src/handlers/forget.ts b/src/handlers/forget.ts new file mode 100644 index 0000000..b98dbb2 --- /dev/null +++ b/src/handlers/forget.ts @@ -0,0 +1,13 @@ +import { checkAndConfigureBackendsForLocations } from '../backend' +import { forgetAll } from '../forget' +import { Flags, Locations } from '../types' +import { checkIfResticIsAvailable, parseLocations } from '../utils' + +export default function forget({ location, all, dryRun }: Flags) { + checkIfResticIsAvailable() + const locations: Locations = parseLocations(location, all) + checkAndConfigureBackendsForLocations(locations) + forgetAll(locations, dryRun) + + console.log('\nFinished!'.underline + ' 🎉') +} diff --git a/src/handlers/info.ts b/src/handlers/info.ts new file mode 100644 index 0000000..8cd8831 --- /dev/null +++ b/src/handlers/info.ts @@ -0,0 +1,18 @@ +import { config } from '../' +import { fill, treeToString } from '../utils' + +const showAll = () => { + console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline) + for (const [key, data] of Object.entries(config.locations)) { + console.log(`\n${key.blue.underline}:`) + console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:'])) + } + + console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline) + for (const [key, data] of Object.entries(config.backends)) { + console.log(`\n${key.blue.underline}:`) + console.log(treeToString(data, ['type:', 'path:', 'key:'])) + } +} + +export default showAll diff --git a/src/handlers/install.ts b/src/handlers/install.ts new file mode 100644 index 0000000..0d7e7cb --- /dev/null +++ b/src/handlers/install.ts @@ -0,0 +1,50 @@ +import { join } from 'path' +import { chmodSync, renameSync, unlinkSync } from 'fs' +import { tmpdir } from 'os' + +import axios from 'axios' +import { Writer } from 'clitastic' + +import { INSTALL_DIR } from '..' +import { checkIfCommandIsAvailable, checkIfResticIsAvailable, downloadFile, exec } from '../utils' + +export default async function install() { + try { + checkIfResticIsAvailable() + console.log('Restic is already installed') + return + } catch {} + + const w = new Writer('Checking latest version... ⏳') + checkIfCommandIsAvailable('bzip2') + const { data: json } = await axios({ + method: 'get', + url: 'https://api.github.com/repos/restic/restic/releases/latest', + responseType: 'json', + }) + + const archMap: { [a: string]: string } = { + x32: '386', + x64: 'amd64', + } + + w.replaceLn('Downloading binary... 🌎') + const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2` + const dl = json.assets.find((asset: any) => asset.name === name) + if (!dl) return console.log('Cannot get the right binary.'.red, 'Please see https://bit.ly/2Y1Rzai') + + const tmp = join(tmpdir(), name) + const extracted = tmp.slice(0, -4) //without the .bz2 + + await downloadFile(dl.browser_download_url, tmp) + + w.replaceLn('Decompressing binary... 📦') + exec('bzip2', ['-dk', tmp]) + unlinkSync(tmp) + + w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`) + chmodSync(extracted, 0o755) + renameSync(extracted, INSTALL_DIR + '/restic') + + w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉') +} diff --git a/src/handlers/restore.ts b/src/handlers/restore.ts new file mode 100644 index 0000000..72707b9 --- /dev/null +++ b/src/handlers/restore.ts @@ -0,0 +1,9 @@ +import { restoreSingle } from '../restore' +import { Flags } from '../types' +import { checkIfResticIsAvailable, checkIfValidLocation } from '../utils' + +export default function restore({ location, to, from }: Flags) { + checkIfResticIsAvailable() + checkIfValidLocation(location) + restoreSingle(location, from, to) +} diff --git a/src/handlers/uninstall.ts b/src/handlers/uninstall.ts new file mode 100644 index 0000000..184ad34 --- /dev/null +++ b/src/handlers/uninstall.ts @@ -0,0 +1,13 @@ +import { unlinkSync } from 'fs' + +import { INSTALL_DIR } from '..' + +export function uninstall() { + for (const bin of ['restic', 'autorestic']) + try { + unlinkSync(INSTALL_DIR + '/' + bin) + console.log(`Finished! ${bin} was uninstalled`) + } catch (e) { + console.log(`${bin} is already uninstalled`.red) + } +} diff --git a/src/handlers/upgrade.ts b/src/handlers/upgrade.ts new file mode 100644 index 0000000..3c6880b --- /dev/null +++ b/src/handlers/upgrade.ts @@ -0,0 +1,37 @@ +import { chmodSync } from 'fs' + +import axios from 'axios' +import { Writer } from 'clitastic' + +import { INSTALL_DIR, VERSION } from '..' +import { checkIfResticIsAvailable, downloadFile, exec } from '../utils' + +export async function upgrade() { + checkIfResticIsAvailable() + const w = new Writer('Checking for latest restic version... ⏳') + exec('restic', ['self-update']) + + w.replaceLn('Checking for latest autorestic version... ⏳') + const { data: json } = await axios({ + method: 'get', + url: 'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest', + responseType: 'json', + }) + + if (json.tag_name != VERSION) { + const platformMap: { [key: string]: string } = { + darwin: 'macos', + } + + const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}` + const dl = json.assets.find((asset: any) => asset.name === name) + + const to = INSTALL_DIR + '/autorestic' + w.replaceLn('Downloading binary... 🌎') + await downloadFile(dl.browser_download_url, to) + + chmodSync(to, 0o755) + } + + w.done('All up to date! 🚀') +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e15f349 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,104 @@ +import 'colors' +import { program } from 'commander' + +import { unlock, readLock, writeLock } from './lock' +import { Config } from './types' +import { init } from './config' + +import info from './handlers/info' +import check from './handlers/check' +import backup from './handlers/backup' +import restore from './handlers/restore' +import forget from './handlers/forget' +import { cron } from './handlers/cron' +import exec from './handlers/exec' +import install from './handlers/install' +import { uninstall } from './handlers/uninstall' +import { upgrade } from './handlers/upgrade' + +export const VERSION = '0.20' +export const INSTALL_DIR = '/usr/local/bin' + +process.on('uncaughtException', (err) => { + console.log(err.message) + unlock() + process.exit(1) +}) + +let queue: Function = () => {} +const enqueue = (fn: Function) => (cmd: any) => { + queue = () => fn(cmd.opts()) +} + +program.storeOptionsAsProperties() +program.name('autorestic').version(VERSION) + +program.option('-c, --config ', 'Config file').option('-v, --verbose', 'Verbosity', false) + +program.command('info').action(enqueue(info)) + +program + .command('check') + .description('Checks and initializes backend as needed') + .option('-b, --backend ') + .option('-a, --all') + .action(enqueue(check)) + +program.command('backup').description('Performs a backup').option('-b, --backend ').option('-a, --all').action(enqueue(backup)) + +program + .command('restore') + .description('Restores data to a specified folder from a location') + .requiredOption('-l, --location ') + .option('--from ') + .requiredOption('--to ', 'Path to save the restored data to') + .action(enqueue(restore)) + +program + .command('forget') + .description('This will prune and remove data according to your policies') + .option('-l, --location ') + .option('-a, --all') + .option('--dry-run') + .action(enqueue(forget)) + +program + .command('cron') + .description('Intended to be triggered by an automated system like systemd or crontab.') + .option('-a, --all') + .action(enqueue(cron)) + +program + .command('exec') + .description('Run any native restic command on desired backends') + .option('-b, --backend ') + .option('-a, --all') + .action(({ args, all, backend }) => { + queue = () => exec({ all, backend }, args) + }) + +program.command('install').description('Installs both restic and autorestic to /usr/local/bin').action(enqueue(install)) + +program.command('uninstall').description('Uninstalls autorestic from the system').action(enqueue(uninstall)) + +program.command('upgrade').alias('update').description('Checks and installs new autorestic versions').action(enqueue(upgrade)) + +const { verbose, config: configFile } = program.parse(process.argv) + +export const VERBOSE = verbose +export let config: Config = init(configFile) + +try { + const lock = readLock() + if (lock.running) throw new Error('An instance of autorestic is already running for this config file'.red) + + writeLock({ + ...lock, + running: true, + }) + queue() +} catch (e) { + console.error(e.message) +} finally { + unlock() +} diff --git a/src/info.ts b/src/info.ts index 968927b..dc46faf 100644 --- a/src/info.ts +++ b/src/info.ts @@ -1,26 +1,18 @@ -import { config } from './autorestic' +import { config } from './' import { fill, treeToString } from './utils' - - const showAll = () => { - console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline) - for (const [key, data] of Object.entries(config.locations)) { - console.log(`\n${key.blue.underline}:`) - console.log(treeToString( - data, - ['to:', 'from:', 'hooks:', 'options:', 'cron:'], - )) - } + console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline) + for (const [key, data] of Object.entries(config.locations)) { + console.log(`\n${key.blue.underline}:`) + console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:'])) + } - console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline) - for (const [key, data] of Object.entries(config.backends)) { - console.log(`\n${key.blue.underline}:`) - console.log(treeToString( - data, - ['type:', 'path:', 'key:'], - )) - } + console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline) + for (const [key, data] of Object.entries(config.backends)) { + console.log(`\n${key.blue.underline}:`) + console.log(treeToString(data, ['type:', 'path:', 'key:'])) + } } -export default showAll \ No newline at end of file +export default showAll diff --git a/src/lock.ts b/src/lock.ts index 962d463..55ded75 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -1,7 +1,7 @@ import fs from 'fs' -import { pathRelativeToConfigFile } from "./utils" -import { Lockfile } from "./types" +import { pathRelativeToConfigFile } from './utils' +import { Lockfile } from './types' export const getLockFileName = () => { const LOCK_FILE = '.autorestic.lock' @@ -12,11 +12,11 @@ export const readLock = (): Lockfile => { const name = getLockFileName() let lock = { running: false, - crons: {} + crons: {}, } try { lock = JSON.parse(fs.readFileSync(name, { encoding: 'utf-8' })) - } catch { } + } catch {} return lock } export const writeLock = (lock: Lockfile) => { @@ -29,4 +29,4 @@ export const unlock = () => { ...readLock(), running: false, }) -} \ No newline at end of file +} diff --git a/src/restore.ts b/src/restore.ts index a776764..1759e85 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -1,78 +1,63 @@ import { Writer } from 'clitastic' import { resolve } from 'path' -import { config } from './autorestic' +import { config } from './' import { getEnvFromBackend } from './backend' import { LocationFromPrefixes } from './config' import { Backend } from './types' -import { - checkIfDockerVolumeExistsOrFail, - decodeLocationFromPrefix, - exec, - execPlain, - getPathFromVolume, -} from './utils' - - +import { checkIfDockerVolumeExistsOrFail, decodeLocationFromPrefix, exec, execPlain, getPathFromVolume } from './utils' export const restoreToFilesystem = (from: string, to: string, backend: Backend) => { - exec( - 'restic', - ['restore', 'latest', '--path', resolve(from), '--target', to], - { env: getEnvFromBackend(backend) }, - ) + exec('restic', ['restore', 'latest', '--path', resolve(from), '--target', to], { env: getEnvFromBackend(backend) }) } export const restoreToVolume = (volume: string, backend: Backend) => { - const tmp = getPathFromVolume(volume) - try { - restoreToFilesystem(tmp, tmp, backend) - try { - checkIfDockerVolumeExistsOrFail(volume) - } catch { - execPlain(`docker volume create ${volume}`) - } + const tmp = getPathFromVolume(volume) + try { + restoreToFilesystem(tmp, tmp, backend) + try { + checkIfDockerVolumeExistsOrFail(volume) + } catch { + execPlain(`docker volume create ${volume}`) + } - // For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost. - // execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /backup /data`) - execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar xf /backup/archive.tar -C /data`) - } finally { - execPlain(`rm -rf ${tmp}`) - } + // For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost. + // execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /backup /data`) + execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar xf /backup/archive.tar -C /data`) + } finally { + execPlain(`rm -rf ${tmp}`) + } } export const restoreSingle = (locationName: string, from: string, to?: string) => { - const location = config.locations[locationName] + const location = config.locations[locationName] - const baseText = locationName.green + '\t\t' - const w = new Writer(baseText + `Restoring...`) + const baseText = locationName.green + '\t\t' + const w = new Writer(baseText + `Restoring...`) - let backendName: string = Array.isArray(location.to) ? location.to[0] : location.to - if (from) { - if (!location.to.includes(from)) { - w.done(baseText + `Backend ${from} is not a valid location for ${locationName}`.red) - return - } - backendName = from - w.replaceLn(baseText + `Restoring from ${backendName.blue}...`) - } else if (Array.isArray(location.to) && location.to.length > 1) { - w.replaceLn(baseText + `Restoring from ${backendName.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`) - } - const backend = config.backends[backendName] + let backendName: string = Array.isArray(location.to) ? location.to[0] : location.to + if (from) { + if (!location.to.includes(from)) { + w.done(baseText + `Backend ${from} is not a valid location for ${locationName}`.red) + return + } + backendName = from + w.replaceLn(baseText + `Restoring from ${backendName.blue}...`) + } else if (Array.isArray(location.to) && location.to.length > 1) { + w.replaceLn(baseText + `Restoring from ${backendName.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`) + } + const backend = config.backends[backendName] - const [type, value] = decodeLocationFromPrefix(location.from) - switch (type) { + const [type, value] = decodeLocationFromPrefix(location.from) + switch (type) { + case LocationFromPrefixes.Filesystem: + if (!to) throw new Error(`You need to specify the restore path with --to`.red) + restoreToFilesystem(value, to, backend) + break - case LocationFromPrefixes.Filesystem: - if (!to) throw new Error(`You need to specify the restore path with --to`.red) - restoreToFilesystem(value, to, backend) - break - - case LocationFromPrefixes.DockerVolume: - restoreToVolume(value, backend) - break - - } - w.done(locationName.green + '\t\tDone 🎉') + case LocationFromPrefixes.DockerVolume: + restoreToVolume(value, backend) + break + } + w.done(locationName.green + '\t\tDone 🎉') } - diff --git a/src/types.ts b/src/types.ts index 1380d22..b40c439 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,84 +3,77 @@ export type StringOrArray = string | string[] // BACKENDS type BackendLocal = { - type: 'local' - key: string - path: string + type: 'local' + key: string + path: string } type BackendSFTP = { - type: 'sftp' - key: string - path: string - password?: string + type: 'sftp' + key: string + path: string + password?: string } type BackendREST = { - type: 'rest' - key: string - path: string - user?: string - password?: string + type: 'rest' + key: string + path: string + user?: string + password?: string } type BackendS3 = { - type: 's3' - key: string - path: string - aws_access_key_id: string - aws_secret_access_key: string + type: 's3' + key: string + path: string + aws_access_key_id: string + aws_secret_access_key: string } type BackendB2 = { - type: 'b2' - key: string - path: string - b2_account_id: string - b2_account_key: string + type: 'b2' + key: string + path: string + b2_account_id: string + b2_account_key: string } type BackendAzure = { - type: 'azure' - key: string - path: string - azure_account_name: string - azure_account_key: string + type: 'azure' + key: string + path: string + azure_account_name: string + azure_account_key: string } type BackendGS = { - type: 'gs' - key: string - path: string - google_project_id: string - google_application_credentials: string + type: 'gs' + key: string + path: string + google_project_id: string + google_application_credentials: string } -export type Backend = - | BackendAzure - | BackendB2 - | BackendGS - | BackendLocal - | BackendREST - | BackendS3 - | BackendSFTP +export type Backend = BackendAzure | BackendB2 | BackendGS | BackendLocal | BackendREST | BackendS3 | BackendSFTP export type Backends = { [name: string]: Backend } // LOCATIONS export type Location = { - from: string - to: StringOrArray - cron?: string - hooks?: { - before?: StringOrArray - after?: StringOrArray - } - options?: { - [key: string]: { - [key: string]: StringOrArray - } - } + from: string + to: StringOrArray + cron?: string + hooks?: { + before?: StringOrArray + after?: StringOrArray + } + options?: { + [key: string]: { + [key: string]: StringOrArray + } + } } export type Locations = { [name: string]: Location } @@ -88,17 +81,17 @@ export type Locations = { [name: string]: Location } // OTHER export type Config = { - locations: Locations - backends: Backends + locations: Locations + backends: Backends } export type Lockfile = { - running: boolean - crons: { - [name: string]: { - lastRun: number - } - } + running: boolean + crons: { + [name: string]: { + lastRun: number + } + } } export type Flags = { [arg: string]: any } diff --git a/src/utils.ts b/src/utils.ts index 7b33d3d..fb42d01 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,15 @@ -import axios from 'axios' import { spawnSync, SpawnSyncOptions } from 'child_process' import { createHash, randomBytes } from 'crypto' import { createWriteStream, renameSync, unlinkSync } from 'fs' import { homedir, tmpdir } from 'os' import { dirname, isAbsolute, join, resolve } from 'path' + +import axios from 'axios' import { Duration, Humanizer } from 'uhrwerk' import { CONFIG_FILE, LocationFromPrefixes } from './config' -import { Location } from './types' +import { Backends, Location, Locations } from './types' +import { config } from '.' export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => { const { stdout, stderr, status } = spawnSync(command, args, { @@ -106,6 +108,35 @@ export const getFlagsFromLocation = (location: Location, command?: string): stri return flags } +export function parseBackend(backends: string[] = [], all: boolean = false): Backends { + if (all) return config.backends + if (backends.length) { + for (const backend of backends) if (!config.backends[backend]) throw new Error('Invalid backend: '.red + backend) + return filterObjectByKey(config.backends, backends) + } else { + throw new Error( + 'No backends specified.'.red + '\n-a, --all, -a\t\t\tSelect all.' + '\n-b, --backend \t\tSpecify one or more backend' + ) + } +} + +export function checkIfValidLocation(location: string) { + if (!config.locations[location]) throw new Error('Invalid location: '.red + location) +} + +export function parseLocations(locations: string[] = [], all: boolean = false): Locations { + if (all) { + return config.locations + } + if (locations.length) { + for (const location of locations) checkIfValidLocation(location) + return filterObjectByKey(config.locations, locations) + } + throw new Error( + 'No locations specified.'.red + '\n-a, --all\t\t\tSelect all.' + '\n-l, --location \t\t\tSpecify one or more location' + ) +} + export const makeArrayIfIsNot = (maybeArray: T | T[]): T[] => (Array.isArray(maybeArray) ? maybeArray : [maybeArray]) export const fill = (length: number, filler = ' '): string => new Array(length).fill(filler).join('') diff --git a/tsconfig.json b/tsconfig.json index 9ad2430..7230fda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "esnext", "module": "commonjs", "outDir": "./lib", + "rootDir": "./src", "strict": true, "esModuleInterop": true } diff --git a/yarn.lock b/yarn.lock index 5e88375..76ae2fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,21 +3,21 @@ "@babel/parser@^7.9.4": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" - integrity sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA== + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" + integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== "@babel/runtime@^7.9.2": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" - integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" "@codedoc/cli@0.2.x": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@codedoc/cli/-/cli-0.2.1.tgz#03daa1b1e0cedff25c0b5dc29ab8c823b2dd28ef" - integrity sha512-kdxbcKaxM3AUEoD70trMrUFitdbJ8mTS7u5ojJfzZ2uQtTIjO1D0XJLydsPyPXqLQf/TyOZJPEm03Q3d9Smt0A== + version "0.2.6" + resolved "https://registry.yarnpkg.com/@codedoc/cli/-/cli-0.2.6.tgz#5e6e981f8eafefcab2b8c81ee6c14815baf2732c" + integrity sha512-zn92PvMamXCteZudz1i606qa4bPXcXveNgb6oQTAiW7Lq+bRMZHUyKvA3Q3jVwlQtV7SidJg3jA4RwkMllktog== dependencies: chalk "^4.0.0" shelljs "^0.8.3" @@ -46,11 +46,6 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/js-yaml@3.x.x": version "3.12.5" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" @@ -62,9 +57,9 @@ integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= "@types/node@14.x.x": - version "14.0.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" - integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== + version "14.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" + integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw== "@types/strip-bom@^3.0.0": version "3.0.0" @@ -76,10 +71,10 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== -ajv@^6.5.5: - 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== +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -87,11 +82,10 @@ ajv@^6.5.5: uri-js "^4.2.2" ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" anymatch@~3.1.1: @@ -147,9 +141,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" - integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axios@0.19.x: version "0.19.2" @@ -235,9 +229,9 @@ chalk@^4.0.0: supports-color "^7.1.0" chokidar@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" - integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== + version "3.4.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" + integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -245,7 +239,7 @@ chokidar@^3.4.0: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.4.0" + readdirp "~3.5.0" optionalDependencies: fsevents "~2.1.2" @@ -280,6 +274,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -291,9 +290,9 @@ core-util-is@1.0.2, core-util-is@~1.0.0: integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cron-parser@2.x.x: - version "2.15.0" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.15.0.tgz#04803cd51d8efcfcc6f83ac08e60f3f8c40c7ec5" - integrity sha512-rMFkrQw8+oG5OuwjiXesup4KeIlEG/IU82YtG4xyAHbO5jhKmYaHPp/ZNhq9+7TjSJ65E3zV3kQPUbmXSff2/g== + version "2.17.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.17.0.tgz#5707421a7e0a73ee74675d1c032a2f14123f2cf8" + integrity sha512-oTmzVEwlurRe51HqTm4afshVr8Rkxy9kFiWxh5e6SmrY2o9NDYU4S6SduanBZYXLgkLy0skA98y7/tztW/DmjQ== dependencies: is-nan "^1.3.0" moment-timezone "^0.5.31" @@ -458,9 +457,9 @@ fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.9.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" + integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== dependencies: reusify "^1.0.4" @@ -527,6 +526,11 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -575,22 +579,17 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: - ajv "^6.5.5" + ajv "^6.12.3" har-schema "^2.0.0" has-flag@^4.0.0: @@ -598,6 +597,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -662,6 +668,13 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-core-module@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + dependencies: + has "^1.0.3" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -701,21 +714,11 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -851,7 +854,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@1.x.x, minimist@^1.1.3, minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -876,9 +879,9 @@ moment-timezone@^0.5.31: moment ">= 2.9.0" "moment@>= 2.9.0": - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== ms@2.0.0: version "2.0.0" @@ -893,17 +896,6 @@ multistream@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.5" -node-notifier@^5.4.0: - version "5.4.3" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" - integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== - dependencies: - growly "^1.3.0" - is-wsl "^1.1.0" - semver "^5.5.0" - shellwords "^0.1.1" - which "^1.3.0" - normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -1028,7 +1020,7 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pkg-fetch@^2.6.7: +pkg-fetch@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.9.tgz#c18c5fa9604c57a3df3d9630afb64e176bc8732d" integrity sha512-EnVR8LRILXBvaNP+wJOSY02c3+qDDfyEyR+aqAHLhcc9PBnbxFT9UZ1+If49goPQzQPn26TzF//fc6KXZ0aXEg== @@ -1046,9 +1038,9 @@ pkg-fetch@^2.6.7: unique-temp-dir "^1.0.0" pkg@4.4.x: - version "4.4.8" - resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.8.tgz#145fb81f31eebfb90d2010dd2c4b663ca0db4009" - integrity sha512-Fqqv0iaX48U3CFZxd6Dq6JKe7BrAWbgRAqMJkz/m8W3H5cqJ6suvsUWe5AJPRlN/AhbBYXBJ0XG9QlYPTXcVFA== + version "4.4.9" + resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.9.tgz#be04f8d03795772b7c4394724ae7252d7c2a4519" + integrity sha512-FK4GqHtcCY2PPPVaKViU0NyRzpo6gCS7tPKN5b7AkElqjAOCH1bsRKgohEnxThr6DWfTGByGqba2YHGR/BqbmA== dependencies: "@babel/parser" "^7.9.4" "@babel/runtime" "^7.9.2" @@ -1059,7 +1051,7 @@ pkg@4.4.x: into-stream "^5.1.1" minimist "^1.2.5" multistream "^2.1.1" - pkg-fetch "^2.6.7" + pkg-fetch "^2.6.9" progress "^2.0.3" resolve "^1.15.1" stream-meter "^1.0.4" @@ -1124,10 +1116,10 @@ readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.1.4: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== dependencies: picomatch "^2.2.1" @@ -1147,9 +1139,9 @@ redent@^1.0.0: strip-indent "^1.0.1" regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== repeating@^2.0.0: version "2.0.1" @@ -1192,10 +1184,11 @@ request@^2.88.0: uuid "^3.3.2" resolve@^1.0.0, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.15.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + version "1.18.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" + integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== dependencies: + is-core-module "^2.0.0" path-parse "^1.0.6" reusify@^1.0.4: @@ -1211,9 +1204,9 @@ rimraf@^2.6.1: glob "^7.1.3" run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + version "1.1.10" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" + integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" @@ -1230,7 +1223,7 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -"semver@2 || 3 || 4 || 5", semver@^5.5.0: +"semver@2 || 3 || 4 || 5": version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -1249,11 +1242,6 @@ shelljs@^0.8.3: interpret "^1.0.0" rechoir "^0.6.2" -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - signal-exit@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -1299,9 +1287,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== sprintf-js@~1.0.2: version "1.0.3" @@ -1362,9 +1350,9 @@ strip-json-comments@^2.0.0: integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" @@ -1399,24 +1387,23 @@ trim-newlines@^1.0.0: integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= ts-node-dev@^1.0.0-pre.40, ts-node-dev@^1.0.0-pre.44: - version "1.0.0-pre.49" - resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.49.tgz#28836f73fe9513f339ccb1c0bebdb2e2e25c52f0" - integrity sha512-iJd4QPPOaCAByl/WuEdmDX8xDR2GmoWYu6aKvGudGUcfP1sJRjpaHb7oFDuvBspQF1xhxUdbjfHuvEtZPwKZFQ== + version "1.0.0" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0.tgz#24a2270d225c29ce269de2a31f88b1b259fc84cb" + integrity sha512-leA/3TgGtnVU77fGngBwVZztqyDRXirytR7dMtMWZS5b2hGpLl+VDnB0F/gf3A+HEPSzS/KwxgXFP7/LtgX4MQ== dependencies: chokidar "^3.4.0" dateformat "~1.0.4-1.2.3" dynamic-dedupe "^0.3.0" minimist "^1.2.5" mkdirp "^1.0.4" - node-notifier "^5.4.0" resolve "^1.0.0" rimraf "^2.6.1" source-map-support "^0.5.12" tree-kill "^1.2.2" - ts-node "^8.10.2" + ts-node "^9.0.0" tsconfig "^7.0.0" -ts-node@^8.10.2, ts-node@^8.8.2: +ts-node@^8.8.2: version "8.10.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== @@ -1427,6 +1414,17 @@ ts-node@^8.10.2, ts-node@^8.8.2: source-map-support "^0.5.17" yn "3.1.1" +ts-node@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + tsconfig@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" @@ -1457,9 +1455,9 @@ type-check@~0.3.2: prelude-ls "~1.1.2" typescript@3.9.x, typescript@^3.8.3: - version "3.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uhrwerk@1.x.x: version "1.0.2" @@ -1486,9 +1484,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 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== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" @@ -1519,13 +1517,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -which@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"