From 3a4992633ee62d5edfbb484d9c6bcb3cf158489d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 31 Jul 2023 14:34:36 +0200 Subject: Migrate server to ESM Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports) --- server/tools/README.md | 3 - server/tools/package.json | 11 - server/tools/peertube-auth.ts | 171 -------------- server/tools/peertube-get-access-token.ts | 34 --- server/tools/peertube-import-videos.ts | 351 ---------------------------- server/tools/peertube-plugins.ts | 165 ------------- server/tools/peertube-redundancy.ts | 172 -------------- server/tools/peertube-upload.ts | 77 ------ server/tools/peertube.ts | 72 ------ server/tools/shared/cli.ts | 262 --------------------- server/tools/shared/index.ts | 1 - server/tools/tsconfig.json | 12 - server/tools/yarn.lock | 373 ------------------------------ 13 files changed, 1704 deletions(-) delete mode 100644 server/tools/README.md delete mode 100644 server/tools/package.json delete mode 100644 server/tools/peertube-auth.ts delete mode 100644 server/tools/peertube-get-access-token.ts delete mode 100644 server/tools/peertube-import-videos.ts delete mode 100644 server/tools/peertube-plugins.ts delete mode 100644 server/tools/peertube-redundancy.ts delete mode 100644 server/tools/peertube-upload.ts delete mode 100644 server/tools/peertube.ts delete mode 100644 server/tools/shared/cli.ts delete mode 100644 server/tools/shared/index.ts delete mode 100644 server/tools/tsconfig.json delete mode 100644 server/tools/yarn.lock (limited to 'server/tools') diff --git a/server/tools/README.md b/server/tools/README.md deleted file mode 100644 index d7ecd4004..000000000 --- a/server/tools/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# PeerTube CLI - -See https://docs.joinpeertube.org/maintain/tools#remote-tools diff --git a/server/tools/package.json b/server/tools/package.json deleted file mode 100644 index b20f38244..000000000 --- a/server/tools/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@peertube/cli", - "version": "1.0.0", - "private": true, - "dependencies": { - "application-config": "^2.0.0", - "cli-table3": "^0.6.0", - "netrc-parser": "^3.1.6" - }, - "devDependencies": {} -} diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts deleted file mode 100644 index c853469c2..000000000 --- a/server/tools/peertube-auth.ts +++ /dev/null @@ -1,171 +0,0 @@ -import CliTable3 from 'cli-table3' -import { OptionValues, program } from 'commander' -import { isUserUsernameValid } from '../helpers/custom-validators/users' -import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared' - -import prompt = require('prompt') - -async function delInstance (url: string) { - const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) - - const index = settings.remotes.indexOf(url) - settings.remotes.splice(index) - - if (settings.default === index) settings.default = -1 - - await writeSettings(settings) - - delete netrc.machines[url] - - await netrc.save() -} - -async function setInstance (url: string, username: string, password: string, isDefault: boolean) { - const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) - - if (settings.remotes.includes(url) === false) { - settings.remotes.push(url) - } - - if (isDefault || settings.remotes.length === 1) { - settings.default = settings.remotes.length - 1 - } - - await writeSettings(settings) - - netrc.machines[url] = { login: username, password } - await netrc.save() -} - -function isURLaPeerTubeInstance (url: string) { - return url.startsWith('http://') || url.startsWith('https://') -} - -function stripExtraneousFromPeerTubeUrl (url: string) { - // Get everything before the 3rd /. - const urlLength = url.includes('/', 8) - ? url.indexOf('/', 8) - : url.length - - return url.substring(0, urlLength) -} - -program - .name('auth') - .usage('[command] [options]') - -program - .command('add') - .description('remember your accounts on remote instances for easier use') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('--default', 'add the entry as the new default') - .action((options: OptionValues) => { - /* eslint-disable no-import-assign */ - prompt.override = options - prompt.start() - prompt.get({ - properties: { - url: { - description: 'instance url', - conform: (value) => isURLaPeerTubeInstance(value), - message: 'It should be an URL (https://peertube.example.com)', - required: true - }, - username: { - conform: (value) => isUserUsernameValid(value), - message: 'Name must be only letters, spaces, or dashes', - required: true - }, - password: { - hidden: true, - replace: '*', - required: true - } - } - }, async (_, result) => { - - // Check credentials - try { - // Strip out everything after the domain:port. - // See https://github.com/Chocobozzz/PeerTube/issues/3520 - result.url = stripExtraneousFromPeerTubeUrl(result.url) - - const server = buildServer(result.url) - await assignToken(server, result.username, result.password) - } catch (err) { - console.error(err.message) - process.exit(-1) - } - - await setInstance(result.url, result.username, result.password, options.default) - - process.exit(0) - }) - }) - -program - .command('del ') - .description('unregisters a remote instance') - .action(async url => { - await delInstance(url) - - process.exit(0) - }) - -program - .command('list') - .description('lists registered remote instances') - .action(async () => { - const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) - - const table = new CliTable3({ - head: [ 'instance', 'login' ], - colWidths: [ 30, 30 ] - }) as any - - settings.remotes.forEach(element => { - if (!netrc.machines[element]) return - - table.push([ - element, - netrc.machines[element].login - ]) - }) - - console.log(table.toString()) - - process.exit(0) - }) - -program - .command('set-default ') - .description('set an existing entry as default') - .action(async url => { - const settings = await getSettings() - const instanceExists = settings.remotes.includes(url) - - if (instanceExists) { - settings.default = settings.remotes.indexOf(url) - await writeSettings(settings) - - process.exit(0) - } else { - console.log(' is not a registered instance.') - process.exit(-1) - } - }) - -program.addHelpText('after', '\n\n Examples:\n\n' + - ' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + - ' $ peertube auth add -u https://peertube.cpy.re -U root\n' + - ' $ peertube auth list\n' + - ' $ peertube auth del https://peertube.cpy.re\n' -) - -if (!process.argv.slice(2).length) { - program.outputHelp() -} - -program.parse(process.argv) diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts deleted file mode 100644 index 71a4826e8..000000000 --- a/server/tools/peertube-get-access-token.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { program } from 'commander' -import { assignToken, buildServer } from './shared' - -program - .option('-u, --url ', 'Server url') - .option('-n, --username ', 'Username') - .option('-p, --password ', 'Password') - .parse(process.argv) - -const options = program.opts() - -if ( - !options.url || - !options.username || - !options.password -) { - if (!options.url) console.error('--url field is required.') - if (!options.username) console.error('--username field is required.') - if (!options.password) console.error('--password field is required.') - - process.exit(-1) -} - -const server = buildServer(options.url) - -assignToken(server, options.username, options.password) - .then(() => { - console.log(server.accessToken) - process.exit(0) - }) - .catch(err => { - console.error(err) - process.exit(-1) - }) diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts deleted file mode 100644 index bbdaa09c0..000000000 --- a/server/tools/peertube-import-videos.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { program } from 'commander' -import { accessSync, constants } from 'fs' -import { remove } from 'fs-extra' -import { join } from 'path' -import { YoutubeDLCLI, YoutubeDLInfo, YoutubeDLInfoBuilder } from '@server/helpers/youtube-dl' -import { wait } from '@shared/core-utils' -import { sha256 } from '@shared/extra-utils' -import { doRequestAndSaveToFile } from '../helpers/requests' -import { - assignToken, - buildCommonVideoOptions, - buildServer, - buildVideoAttributesFromCommander, - getLogger, - getServerCredentials -} from './shared' - -import prompt = require('prompt') - -const processOptions = { - maxBuffer: Infinity -} - -let command = program - .name('import-videos') - -command = buildCommonVideoOptions(command) - -command - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('--target-url ', 'Video target URL') - .option('--since ', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate) - .option('--until ', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate) - .option('--first ', 'Process first n elements of returned playlist') - .option('--last ', 'Process last n elements of returned playlist') - .option('--wait-interval ', 'Duration between two video imports (in seconds)', convertIntoMs) - .option('-T, --tmpdir ', 'Working directory', __dirname) - .usage('[global options] [ -- youtube-dl options]') - .parse(process.argv) - -const options = command.opts() - -const log = getLogger(options.verbose) - -getServerCredentials(command) - .then(({ url, username, password }) => { - if (!options.targetUrl) { - exitError('--target-url field is required.') - } - - try { - accessSync(options.tmpdir, constants.R_OK | constants.W_OK) - } catch (e) { - exitError('--tmpdir %s: directory does not exist or is not accessible', options.tmpdir) - } - - url = normalizeTargetUrl(url) - options.targetUrl = normalizeTargetUrl(options.targetUrl) - - run(url, username, password) - .catch(err => exitError(err)) - }) - .catch(err => console.error(err)) - -async function run (url: string, username: string, password: string) { - if (!password) password = await promptPassword() - - const youtubeDLBinary = await YoutubeDLCLI.safeGet() - - let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args) - - if (!Array.isArray(info)) info = [ info ] - - // Try to fix youtube channels upload - const uploadsObject = info.find(i => !i.ie_key && !i.duration && i.title === 'Uploads') - - if (uploadsObject) { - console.log('Fixing URL to %s.', uploadsObject.url) - - info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args) - } - - let infoArray: any[] - - infoArray = [].concat(info) - if (options.first) { - infoArray = infoArray.slice(0, options.first) - } else if (options.last) { - infoArray = infoArray.slice(-options.last) - } - - log.info('Will download and upload %d videos.\n', infoArray.length) - - let skipInterval = true - for (const [ index, info ] of infoArray.entries()) { - try { - if (index > 0 && options.waitInterval && !skipInterval) { - log.info('Wait for %d seconds before continuing.', options.waitInterval / 1000) - await wait(options.waitInterval) - } - - skipInterval = await processVideo({ - cwd: options.tmpdir, - url, - username, - password, - youtubeInfo: info - }) - } catch (err) { - console.error('Cannot process video.', { info, url, err }) - } - } - - log.info('Video/s for user %s imported: %s', username, options.targetUrl) - process.exit(0) -} - -async function processVideo (parameters: { - cwd: string - url: string - username: string - password: string - youtubeInfo: any -}) { - const { youtubeInfo, cwd, url, username, password } = parameters - - log.debug('Fetching object.', youtubeInfo) - - const videoInfo = await fetchObject(youtubeInfo) - log.debug('Fetched object.', videoInfo) - - if ( - options.since && - videoInfo.originallyPublishedAtWithoutTime && - videoInfo.originallyPublishedAtWithoutTime.getTime() < options.since.getTime() - ) { - log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.since)) - return true - } - - if ( - options.until && - videoInfo.originallyPublishedAtWithoutTime && - videoInfo.originallyPublishedAtWithoutTime.getTime() > options.until.getTime() - ) { - log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.until)) - return true - } - - const server = buildServer(url) - const { data } = await server.search.advancedVideoSearch({ - search: { - search: videoInfo.name, - sort: '-match', - searchTarget: 'local' - } - }) - - log.info('############################################################\n') - - if (data.find(v => v.name === videoInfo.name)) { - log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.name) - return true - } - - const path = join(cwd, sha256(videoInfo.url) + '.mp4') - - log.info('Downloading video "%s"...', videoInfo.name) - - try { - const youtubeDLBinary = await YoutubeDLCLI.safeGet() - const output = await youtubeDLBinary.download({ - url: videoInfo.url, - format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), - output: path, - additionalYoutubeDLArgs: command.args, - processOptions - }) - - log.info(output.join('\n')) - await uploadVideoOnPeerTube({ - cwd, - url, - username, - password, - videoInfo, - videoPath: path - }) - } catch (err) { - log.error(err.message) - } - - return false -} - -async function uploadVideoOnPeerTube (parameters: { - videoInfo: YoutubeDLInfo - videoPath: string - cwd: string - url: string - username: string - password: string -}) { - const { videoInfo, videoPath, cwd, url, username, password } = parameters - - const server = buildServer(url) - await assignToken(server, username, password) - - let thumbnailfile: string - if (videoInfo.thumbnailUrl) { - thumbnailfile = join(cwd, sha256(videoInfo.thumbnailUrl) + '.jpg') - - await doRequestAndSaveToFile(videoInfo.thumbnailUrl, thumbnailfile) - } - - const baseAttributes = await buildVideoAttributesFromCommander(server, program, videoInfo) - - const attributes = { - ...baseAttributes, - - originallyPublishedAtWithoutTime: videoInfo.originallyPublishedAtWithoutTime - ? videoInfo.originallyPublishedAtWithoutTime.toISOString() - : null, - - thumbnailfile, - previewfile: thumbnailfile, - fixture: videoPath - } - - log.info('\nUploading on PeerTube video "%s".', attributes.name) - - try { - await server.videos.upload({ attributes }) - } catch (err) { - if (err.message.indexOf('401') !== -1) { - log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.') - - server.accessToken = await server.login.getAccessToken(username, password) - - await server.videos.upload({ attributes }) - } else { - exitError(err.message) - } - } - - await remove(videoPath) - if (thumbnailfile) await remove(thumbnailfile) - - log.info('Uploaded video "%s"!\n', attributes.name) -} - -/* ---------------------------------------------------------- */ - -async function fetchObject (info: any) { - const url = buildUrl(info) - - const youtubeDLCLI = await YoutubeDLCLI.safeGet() - const result = await youtubeDLCLI.getInfo({ - url, - format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), - processOptions - }) - - const builder = new YoutubeDLInfoBuilder(result) - - const videoInfo = builder.getInfo() - - return { ...videoInfo, url } -} - -function buildUrl (info: any) { - const webpageUrl = info.webpage_url as string - if (webpageUrl?.match(/^https?:\/\//)) return webpageUrl - - const url = info.url as string - if (url?.match(/^https?:\/\//)) return url - - // It seems youtube-dl does not return the video url - return 'https://www.youtube.com/watch?v=' + info.id -} - -function normalizeTargetUrl (url: string) { - let normalizedUrl = url.replace(/\/+$/, '') - - if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { - normalizedUrl = 'https://' + normalizedUrl - } - - return normalizedUrl -} - -async function promptPassword () { - return new Promise((res, rej) => { - prompt.start() - const schema = { - properties: { - password: { - hidden: true, - required: true - } - } - } - prompt.get(schema, function (err, result) { - if (err) { - return rej(err) - } - return res(result.password) - }) - }) -} - -function parseDate (dateAsStr: string): Date { - if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) { - exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`) - } - const date = new Date(dateAsStr) - date.setHours(0, 0, 0) - if (isNaN(date.getTime())) { - exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`) - } - return date -} - -function formatDate (date: Date): string { - return date.toISOString().split('T')[0] -} - -function convertIntoMs (secondsAsStr: string): number { - const seconds = parseInt(secondsAsStr, 10) - if (seconds <= 0) { - exitError(`Invalid duration passed: ${seconds}. Expected duration to be strictly positive and in seconds`) - } - return Math.round(seconds * 1000) -} - -function exitError (message: string, ...meta: any[]) { - // use console.error instead of log.error here - console.error(message, ...meta) - process.exit(-1) -} - -function getYoutubeDLInfo (youtubeDLCLI: YoutubeDLCLI, url: string, args: string[]) { - return youtubeDLCLI.getInfo({ - url, - format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), - additionalYoutubeDLArgs: [ '-j', '--flat-playlist', '--playlist-reverse', ...args ], - processOptions - }) -} diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts deleted file mode 100644 index 0660c855f..000000000 --- a/server/tools/peertube-plugins.ts +++ /dev/null @@ -1,165 +0,0 @@ -import CliTable3 from 'cli-table3' -import { Command, OptionValues, program } from 'commander' -import { isAbsolute } from 'path' -import { PluginType } from '../../shared/models' -import { assignToken, buildServer, getServerCredentials } from './shared' - -program - .name('plugins') - .usage('[command] [options]') - -program - .command('list') - .description('List installed plugins') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-t, --only-themes', 'List themes only') - .option('-P, --only-plugins', 'List plugins only') - .action((options, command) => pluginsListCLI(command, options)) - -program - .command('install') - .description('Install a plugin or a theme') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-P --path ', 'Install from a path') - .option('-n, --npm-name ', 'Install from npm') - .option('--plugin-version ', 'Specify the plugin version to install (only available when installing from npm)') - .action((options, command) => installPluginCLI(command, options)) - -program - .command('update') - .description('Update a plugin or a theme') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-P --path ', 'Update from a path') - .option('-n, --npm-name ', 'Update from npm') - .action((options, command) => updatePluginCLI(command, options)) - -program - .command('uninstall') - .description('Uninstall a plugin or a theme') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-n, --npm-name ', 'NPM plugin/theme name') - .action((options, command) => uninstallPluginCLI(command, options)) - -if (!process.argv.slice(2).length) { - program.outputHelp() -} - -program.parse(process.argv) - -// ---------------------------------------------------------------------------- - -async function pluginsListCLI (command: Command, options: OptionValues) { - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - let pluginType: PluginType - if (options.onlyThemes) pluginType = PluginType.THEME - if (options.onlyPlugins) pluginType = PluginType.PLUGIN - - const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType }) - - const table = new CliTable3({ - head: [ 'name', 'version', 'homepage' ], - colWidths: [ 50, 20, 50 ] - }) as any - - for (const plugin of data) { - const npmName = plugin.type === PluginType.PLUGIN - ? 'peertube-plugin-' + plugin.name - : 'peertube-theme-' + plugin.name - - table.push([ - npmName, - plugin.version, - plugin.homepage - ]) - } - - console.log(table.toString()) - process.exit(0) -} - -async function installPluginCLI (command: Command, options: OptionValues) { - if (!options.path && !options.npmName) { - console.error('You need to specify the npm name or the path of the plugin you want to install.\n') - program.outputHelp() - process.exit(-1) - } - - if (options.path && !isAbsolute(options.path)) { - console.error('Path should be absolute.') - process.exit(-1) - } - - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - try { - await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion }) - } catch (err) { - console.error('Cannot install plugin.', err) - process.exit(-1) - } - - console.log('Plugin installed.') - process.exit(0) -} - -async function updatePluginCLI (command: Command, options: OptionValues) { - if (!options.path && !options.npmName) { - console.error('You need to specify the npm name or the path of the plugin you want to update.\n') - program.outputHelp() - process.exit(-1) - } - - if (options.path && !isAbsolute(options.path)) { - console.error('Path should be absolute.') - process.exit(-1) - } - - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - try { - await server.plugins.update({ npmName: options.npmName, path: options.path }) - } catch (err) { - console.error('Cannot update plugin.', err) - process.exit(-1) - } - - console.log('Plugin updated.') - process.exit(0) -} - -async function uninstallPluginCLI (command: Command, options: OptionValues) { - if (!options.npmName) { - console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') - program.outputHelp() - process.exit(-1) - } - - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - try { - await server.plugins.uninstall({ npmName: options.npmName }) - } catch (err) { - console.error('Cannot uninstall plugin.', err) - process.exit(-1) - } - - console.log('Plugin uninstalled.') - process.exit(0) -} diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts deleted file mode 100644 index c24eb5233..000000000 --- a/server/tools/peertube-redundancy.ts +++ /dev/null @@ -1,172 +0,0 @@ -import CliTable3 from 'cli-table3' -import { Command, program } from 'commander' -import { URL } from 'url' -import validator from 'validator' -import { forceNumber, uniqify } from '@shared/core-utils' -import { HttpStatusCode, VideoRedundanciesTarget } from '@shared/models' -import { assignToken, buildServer, getServerCredentials } from './shared' - -import bytes = require('bytes') -program - .name('redundancy') - .usage('[command] [options]') - -program - .command('list-remote-redundancies') - .description('List remote redundancies on your videos') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .action(() => listRedundanciesCLI('my-videos')) - -program - .command('list-my-redundancies') - .description('List your redundancies of remote videos') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .action(() => listRedundanciesCLI('remote-videos')) - -program - .command('add') - .description('Duplicate a video in your redundancy system') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-v, --video ', 'Video id to duplicate') - .action((options, command) => addRedundancyCLI(options, command)) - -program - .command('remove') - .description('Remove a video from your redundancies') - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-v, --video ', 'Video id to remove from redundancies') - .action((options, command) => removeRedundancyCLI(options, command)) - -if (!process.argv.slice(2).length) { - program.outputHelp() -} - -program.parse(process.argv) - -// ---------------------------------------------------------------------------- - -async function listRedundanciesCLI (target: VideoRedundanciesTarget) { - const { url, username, password } = await getServerCredentials(program) - const server = buildServer(url) - await assignToken(server, username, password) - - const { data } = await server.redundancy.listVideos({ start: 0, count: 100, sort: 'name', target }) - - const table = new CliTable3({ - head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ] - }) as any - - for (const redundancy of data) { - const webVideoFiles = redundancy.redundancies.files - const streamingPlaylists = redundancy.redundancies.streamingPlaylists - - let totalSize = '' - if (target === 'remote-videos') { - const tmp = webVideoFiles.concat(streamingPlaylists) - .reduce((a, b) => a + b.size, 0) - - totalSize = bytes(tmp) - } - - const instances = uniqify( - webVideoFiles.concat(streamingPlaylists) - .map(r => r.fileUrl) - .map(u => new URL(u).host) - ) - - table.push([ - redundancy.id.toString(), - redundancy.name, - redundancy.url, - webVideoFiles.length, - streamingPlaylists.length, - instances.join('\n'), - totalSize - ]) - } - - console.log(table.toString()) - process.exit(0) -} - -async function addRedundancyCLI (options: { video: number }, command: Command) { - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - if (!options.video || validator.isInt('' + options.video) === false) { - console.error('You need to specify the video id to duplicate and it should be a number.\n') - command.outputHelp() - process.exit(-1) - } - - try { - await server.redundancy.addVideo({ videoId: options.video }) - - console.log('Video will be duplicated by your instance!') - - process.exit(0) - } catch (err) { - if (err.message.includes(HttpStatusCode.CONFLICT_409)) { - console.error('This video is already duplicated by your instance.') - } else if (err.message.includes(HttpStatusCode.NOT_FOUND_404)) { - console.error('This video id does not exist.') - } else { - console.error(err) - } - - process.exit(-1) - } -} - -async function removeRedundancyCLI (options: { video: number }, command: Command) { - const { url, username, password } = await getServerCredentials(command) - const server = buildServer(url) - await assignToken(server, username, password) - - if (!options.video || validator.isInt('' + options.video) === false) { - console.error('You need to specify the video id to remove from your redundancies.\n') - command.outputHelp() - process.exit(-1) - } - - const videoId = forceNumber(options.video) - - const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' }) - let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id) - - if (!videoRedundancy) { - const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' }) - videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id) - } - - if (!videoRedundancy) { - console.error('Video redundancy not found.') - process.exit(-1) - } - - try { - const ids = videoRedundancy.redundancies.files - .concat(videoRedundancy.redundancies.streamingPlaylists) - .map(r => r.id) - - for (const id of ids) { - await server.redundancy.removeVideo({ redundancyId: id }) - } - - console.log('Video redundancy removed!') - - process.exit(0) - } catch (err) { - console.error(err) - process.exit(-1) - } -} diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts deleted file mode 100644 index 87da55005..000000000 --- a/server/tools/peertube-upload.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { program } from 'commander' -import { access, constants } from 'fs-extra' -import { isAbsolute } from 'path' -import { assignToken, buildCommonVideoOptions, buildServer, buildVideoAttributesFromCommander, getServerCredentials } from './shared' - -let command = program - .name('upload') - -command = buildCommonVideoOptions(command) - -command - .option('-u, --url ', 'Server url') - .option('-U, --username ', 'Username') - .option('-p, --password ', 'Password') - .option('-b, --thumbnail ', 'Thumbnail path') - .option('-v, --preview ', 'Preview path') - .option('-f, --file ', 'Video absolute file path') - .parse(process.argv) - -const options = command.opts() - -getServerCredentials(command) - .then(({ url, username, password }) => { - if (!options.videoName || !options.file) { - if (!options.videoName) console.error('--video-name is required.') - if (!options.file) console.error('--file is required.') - - process.exit(-1) - } - - if (isAbsolute(options.file) === false) { - console.error('File path should be absolute.') - process.exit(-1) - } - - run(url, username, password).catch(err => { - console.error(err) - process.exit(-1) - }) - }) - .catch(err => console.error(err)) - -async function run (url: string, username: string, password: string) { - const server = buildServer(url) - await assignToken(server, username, password) - - await access(options.file, constants.F_OK) - - console.log('Uploading %s video...', options.videoName) - - const baseAttributes = await buildVideoAttributesFromCommander(server, program) - - const attributes = { - ...baseAttributes, - - fixture: options.file, - thumbnailfile: options.thumbnail, - previewfile: options.preview - } - - try { - await server.videos.upload({ attributes }) - console.log(`Video ${options.videoName} uploaded.`) - process.exit(0) - } catch (err) { - const message = err.message || '' - if (message.includes('413')) { - console.error('Aborted: user quota is exceeded or video file is too big for this PeerTube instance.') - } else { - console.error(require('util').inspect(err)) - } - - process.exit(-1) - } -} - -// ---------------------------------------------------------------------------- diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts deleted file mode 100644 index b79917b4f..000000000 --- a/server/tools/peertube.ts +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env node - -import { CommandOptions, program } from 'commander' -import { getSettings, version } from './shared' - -program - .version(version, '-v, --version') - .usage('[command] [options]') - -/* Subcommands automatically loaded in the directory and beginning by peertube-* */ -program - .command('auth [action]', 'register your accounts on remote instances to use them with other commands') - .command('upload', 'upload a video').alias('up') - .command('import-videos', 'import a video from a streaming platform').alias('import') - .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') - .command('plugins [action]', 'manage instance plugins/themes').alias('p') - .command('redundancy [action]', 'manage instance redundancies').alias('r') - -/* Not Yet Implemented */ -program - .command( - 'diagnostic [action]', - 'like couple therapy, but for your instance', - { noHelp: true } as CommandOptions - ).alias('d') - .command('admin', - 'manage an instance where you have elevated rights', - { noHelp: true } as CommandOptions - ).alias('a') - -// help on no command -if (!process.argv.slice(2).length) { - const logo = '░P░e░e░r░T░u░b░e░' - console.log(` - ___/),.._ ` + logo + ` -/' ,. ."'._ -( "' '-.__"-._ ,- -\\'='='), "\\ -._-"-. -"/ - / ""/"\\,_\\,__"" _" /,- - / / -" _/"/ - / | ._\\\\ |\\ |_.".-" / - / | __\\)|)|),/|_." _,." - / \\_." " ") | ).-""---''-- - ( "/.""7__-""'' - | " ."._--._ - \\ \\ (_ __ "" ".,_ - \\.,. \\ "" -"".-" - ".,_, (",_-,,,-".- - "'-,\\_ __,-" - ",)" ") - /"\\-" - ,"\\/ - _,.__/"\\/_ (the CLI for red chocobos) - / \\) "./, ". - --/---"---" "-) )---- by Chocobozzz et al.\n`) -} - -getSettings() - .then(settings => { - const state = (settings.default === undefined || settings.default === -1) - ? 'no instance selected, commands will require explicit arguments' - : 'instance ' + settings.remotes[settings.default] + ' selected' - - program - .addHelpText('after', '\n\n State: ' + state + '\n\n' + - ' Examples:\n\n' + - ' $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + - ' $ peertube up \n' - ) - .parse(process.argv) - }) - .catch(err => console.error(err)) diff --git a/server/tools/shared/cli.ts b/server/tools/shared/cli.ts deleted file mode 100644 index e010ab320..000000000 --- a/server/tools/shared/cli.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { Command } from 'commander' -import { Netrc } from 'netrc-parser' -import { join } from 'path' -import { createLogger, format, transports } from 'winston' -import { getAppNumber, isTestInstance } from '@server/helpers/core-utils' -import { loadLanguages } from '@server/initializers/constants' -import { root } from '@shared/core-utils' -import { UserRole, VideoPrivacy } from '@shared/models' -import { PeerTubeServer } from '@shared/server-commands' - -let configName = 'PeerTube/CLI' -if (isTestInstance()) configName += `-${getAppNumber()}` - -const config = require('application-config')(configName) - -const version = require(join(root(), 'package.json')).version - -async function getAdminTokenOrDie (server: PeerTubeServer, username: string, password: string) { - const token = await server.login.getAccessToken(username, password) - const me = await server.users.getMyInfo({ token }) - - if (me.role.id !== UserRole.ADMINISTRATOR) { - console.error('You must be an administrator.') - process.exit(-1) - } - - return token -} - -interface Settings { - remotes: any[] - default: number -} - -async function getSettings (): Promise { - const defaultSettings = { - remotes: [], - default: -1 - } - - const data = await config.read() - - return Object.keys(data).length === 0 - ? defaultSettings - : data -} - -async function getNetrc () { - const Netrc = require('netrc-parser').Netrc - - const netrc = isTestInstance() - ? new Netrc(join(root(), 'test' + getAppNumber(), 'netrc')) - : new Netrc() - - await netrc.load() - - return netrc -} - -function writeSettings (settings: Settings) { - return config.write(settings) -} - -function deleteSettings () { - return config.trash() -} - -function getRemoteObjectOrDie ( - program: Command, - settings: Settings, - netrc: Netrc -): { url: string, username: string, password: string } { - const options = program.opts() - - function exitIfNoOptions (optionNames: string[], errorPrefix: string = '') { - let exit = false - - for (const key of optionNames) { - if (!options[key]) { - if (exit === false && errorPrefix) console.error(errorPrefix) - - console.error(`--${key} field is required`) - exit = true - } - } - - if (exit) process.exit(-1) - } - - // If username or password are specified, both are mandatory - if (options.username || options.password) { - exitIfNoOptions([ 'username', 'password' ]) - } - - // If no available machines, url, username and password args are mandatory - if (Object.keys(netrc.machines).length === 0) { - exitIfNoOptions([ 'url', 'username', 'password' ], 'No account found in netrc') - } - - if (settings.remotes.length === 0 || settings.default === -1) { - exitIfNoOptions([ 'url' ], 'No default instance found') - } - - let url: string = options.url - let username: string = options.username - let password: string = options.password - - if (!url && settings.default !== -1) url = settings.remotes[settings.default] - - const machine = netrc.machines[url] - if ((!username || !password) && !machine) { - console.error('Cannot find existing configuration for %s.', url) - process.exit(-1) - } - - if (!username && machine) username = machine.login - if (!password && machine) password = machine.password - - return { url, username, password } -} - -function buildCommonVideoOptions (command: Command) { - function list (val) { - return val.split(',') - } - - return command - .option('-n, --video-name ', 'Video name') - .option('-c, --category ', 'Category number') - .option('-l, --licence ', 'Licence number') - .option('-L, --language ', 'Language ISO 639 code (fr or en...)') - .option('-t, --tags ', 'Video tags', list) - .option('-N, --nsfw', 'Video is Not Safe For Work') - .option('-d, --video-description ', 'Video description') - .option('-P, --privacy ', 'Privacy') - .option('-C, --channel-name ', 'Channel name') - .option('--no-comments-enabled', 'Disable video comments') - .option('-s, --support ', 'Video support text') - .option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video') - .option('--no-download-enabled', 'Disable video download') - .option('-v, --verbose ', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') -} - -async function buildVideoAttributesFromCommander (server: PeerTubeServer, command: Command, defaultAttributes: any = {}) { - const options = command.opts() - - const defaultBooleanAttributes = { - nsfw: false, - commentsEnabled: true, - downloadEnabled: true, - waitTranscoding: true - } - - const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {} - - for (const key of Object.keys(defaultBooleanAttributes)) { - if (options[key] !== undefined) { - booleanAttributes[key] = options[key] - } else if (defaultAttributes[key] !== undefined) { - booleanAttributes[key] = defaultAttributes[key] - } else { - booleanAttributes[key] = defaultBooleanAttributes[key] - } - } - - const videoAttributes = { - name: options.videoName || defaultAttributes.name, - category: options.category || defaultAttributes.category || undefined, - licence: options.licence || defaultAttributes.licence || undefined, - language: options.language || defaultAttributes.language || undefined, - privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC, - support: options.support || defaultAttributes.support || undefined, - description: options.videoDescription || defaultAttributes.description || undefined, - tags: options.tags || defaultAttributes.tags || undefined - } - - Object.assign(videoAttributes, booleanAttributes) - - if (options.channelName) { - const videoChannel = await server.channels.get({ channelName: options.channelName }) - - Object.assign(videoAttributes, { channelId: videoChannel.id }) - - if (!videoAttributes.support && videoChannel.support) { - Object.assign(videoAttributes, { support: videoChannel.support }) - } - } - - return videoAttributes -} - -function getServerCredentials (program: Command) { - return Promise.all([ getSettings(), getNetrc() ]) - .then(([ settings, netrc ]) => { - return getRemoteObjectOrDie(program, settings, netrc) - }) -} - -function buildServer (url: string) { - loadLanguages() - return new PeerTubeServer({ url }) -} - -async function assignToken (server: PeerTubeServer, username: string, password: string) { - const bodyClient = await server.login.getClient() - const client = { id: bodyClient.client_id, secret: bodyClient.client_secret } - - const body = await server.login.login({ client, user: { username, password } }) - - server.accessToken = body.access_token -} - -function getLogger (logLevel = 'info') { - const logLevels = { - 0: 0, - error: 0, - 1: 1, - warn: 1, - 2: 2, - info: 2, - 3: 3, - verbose: 3, - 4: 4, - debug: 4 - } - - const logger = createLogger({ - levels: logLevels, - format: format.combine( - format.splat(), - format.simple() - ), - transports: [ - new (transports.Console)({ - level: logLevel - }) - ] - }) - - return logger -} - -// --------------------------------------------------------------------------- - -export { - version, - getLogger, - getSettings, - getNetrc, - getRemoteObjectOrDie, - writeSettings, - deleteSettings, - - getServerCredentials, - - buildCommonVideoOptions, - buildVideoAttributesFromCommander, - - getAdminTokenOrDie, - buildServer, - assignToken -} diff --git a/server/tools/shared/index.ts b/server/tools/shared/index.ts deleted file mode 100644 index 8a3f31e2f..000000000 --- a/server/tools/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './cli' diff --git a/server/tools/tsconfig.json b/server/tools/tsconfig.json deleted file mode 100644 index 39f8e74e4..000000000 --- a/server/tools/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/server/tools" - }, - "include": [ ".", "../typings" ], - "references": [ - { "path": "../" } - ], - "files": [], - "exclude": [ ] // Overwrite exclude property -} diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock deleted file mode 100644 index 025ef208d..000000000 --- a/server/tools/yarn.lock +++ /dev/null @@ -1,373 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== - dependencies: - "@babel/highlight" "^7.16.7" - -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== - -"@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -application-config-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.0.tgz#193c5f0a86541a4c66fba1e2dc38583362ea5e8f" - integrity sha1-GTxfCoZUGkxm+6Hi3DhYM2LqXo8= - -application-config@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/application-config/-/application-config-2.0.0.tgz#15b4d54d61c0c082f9802227e3e85de876b47747" - integrity sha512-NC5/0guSZK3/UgUDfCk/riByXzqz0owL1L3r63JPSBzYk5QALrp3bLxbsR7qeSfvYfFmAhnp3dbqYsW3U9MpZQ== - dependencies: - application-config-path "^0.1.0" - load-json-file "^6.2.0" - write-json-file "^4.2.0" - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -cli-table3@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" - integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== - dependencies: - string-width "^4.2.0" - optionalDependencies: - colors "1.4.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -colors@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -detect-indent@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -graceful-fs@^4.1.15: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -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= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -load-json-file@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" - integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== - dependencies: - graceful-fs "^4.1.15" - parse-json "^5.0.0" - strip-bom "^4.0.0" - type-fest "^0.6.0" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -netrc-parser@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72" - integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw== - dependencies: - debug "^3.1.0" - execa "^0.10.0" - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sort-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" - integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== - dependencies: - is-plain-obj "^2.0.0" - -string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -which@^1.2.9: - 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" - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write-json-file@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" - integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== - dependencies: - detect-indent "^6.0.0" - graceful-fs "^4.1.15" - is-plain-obj "^2.0.0" - make-dir "^3.0.0" - sort-keys "^4.0.0" - write-file-atomic "^3.0.0" -- cgit v1.2.3