mirror of https://github.com/Chocobozzz/PeerTube
Add redundancy CLI
parent
b764380ac2
commit
26fcf2efeb
|
@ -6,15 +6,15 @@ import {
|
||||||
addVideoChannel,
|
addVideoChannel,
|
||||||
buildAbsoluteFixturePath,
|
buildAbsoluteFixturePath,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createUser,
|
createUser, doubleFollow,
|
||||||
execCLI,
|
execCLI,
|
||||||
flushAndRunServer,
|
flushAndRunServer,
|
||||||
getEnvCli,
|
getEnvCli, getLocalIdByUUID,
|
||||||
getVideo,
|
getVideo,
|
||||||
getVideosList,
|
getVideosList,
|
||||||
getVideosListWithToken, removeVideo,
|
getVideosListWithToken, removeVideo,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers, uploadVideo, uploadVideoAndGetId,
|
||||||
userLogin,
|
userLogin,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '../../../shared/extra-utils'
|
} from '../../../shared/extra-utils'
|
||||||
|
@ -210,6 +210,75 @@ describe('Test CLI wrapper', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Manage video redundancies', function () {
|
||||||
|
let anotherServer: ServerInfo
|
||||||
|
let video1Server2: number
|
||||||
|
let servers: ServerInfo[]
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
anotherServer = await flushAndRunServer(2)
|
||||||
|
await setAccessTokensToServers([ anotherServer ])
|
||||||
|
|
||||||
|
await doubleFollow(server, anotherServer)
|
||||||
|
|
||||||
|
servers = [ server, anotherServer ]
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const uuid = (await uploadVideoAndGetId({ server: anotherServer, videoName: 'super video' })).uuid
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
video1Server2 = await getLocalIdByUUID(server.url, uuid)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should add a redundancy', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
const env = getEnvCli(server)
|
||||||
|
|
||||||
|
const params = `add --video ${video1Server2}`
|
||||||
|
|
||||||
|
await execCLI(`${env} ${cmd} redundancy ${params}`)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list redundancies', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
{
|
||||||
|
const env = getEnvCli(server)
|
||||||
|
|
||||||
|
const params = `list-my-redundancies`
|
||||||
|
const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
|
||||||
|
|
||||||
|
expect(stdout).to.contain('super video')
|
||||||
|
expect(stdout).to.contain(`localhost:${server.port}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a redundancy', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
const env = getEnvCli(server)
|
||||||
|
|
||||||
|
const params = `remove --video ${video1Server2}`
|
||||||
|
|
||||||
|
await execCLI(`${env} ${cmd} redundancy ${params}`)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
{
|
||||||
|
const env = getEnvCli(server)
|
||||||
|
const params = `list-my-redundancies`
|
||||||
|
const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
|
||||||
|
|
||||||
|
expect(stdout).to.not.contain('super video')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
|
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
|
||||||
import { createLogger, format, transports } from 'winston'
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
import { getAccessToken, getMyUserInformation } from '@shared/extra-utils'
|
||||||
|
import { User, UserRole } from '@shared/models'
|
||||||
|
|
||||||
let configName = 'PeerTube/CLI'
|
let configName = 'PeerTube/CLI'
|
||||||
if (isTestInstance()) configName += `-${getAppNumber()}`
|
if (isTestInstance()) configName += `-${getAppNumber()}`
|
||||||
|
@ -14,6 +16,19 @@ const config = require('application-config')(configName)
|
||||||
|
|
||||||
const version = require('../../../package.json').version
|
const version = require('../../../package.json').version
|
||||||
|
|
||||||
|
async function getAdminTokenOrDie (url: string, username: string, password: string) {
|
||||||
|
const accessToken = await getAccessToken(url, username, password)
|
||||||
|
const resMe = await getMyUserInformation(url, accessToken)
|
||||||
|
const me: User = resMe.body
|
||||||
|
|
||||||
|
if (me.role !== UserRole.ADMINISTRATOR) {
|
||||||
|
console.error('You must be an administrator.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken
|
||||||
|
}
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
remotes: any[],
|
remotes: any[],
|
||||||
default: number
|
default: number
|
||||||
|
@ -222,5 +237,7 @@ export {
|
||||||
getServerCredentials,
|
getServerCredentials,
|
||||||
|
|
||||||
buildCommonVideoOptions,
|
buildCommonVideoOptions,
|
||||||
buildVideoAttributesFromCommander
|
buildVideoAttributesFromCommander,
|
||||||
|
|
||||||
|
getAdminTokenOrDie
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"application-config": "^1.0.1",
|
"application-config": "^1.0.1",
|
||||||
"cli-table": "^0.3.1",
|
"cli-table3": "^0.5.1",
|
||||||
"netrc-parser": "^3.1.6",
|
"netrc-parser": "^3.1.6",
|
||||||
"webtorrent-hybrid": "^4.0.1"
|
"webtorrent-hybrid": "^4.0.1"
|
||||||
},
|
},
|
||||||
"summon": {
|
"summon": {
|
||||||
"silent": true
|
"silent": true
|
||||||
}
|
},
|
||||||
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ import * as prompt from 'prompt'
|
||||||
import { getNetrc, getSettings, writeSettings } from './cli'
|
import { getNetrc, getSettings, writeSettings } from './cli'
|
||||||
import { isUserUsernameValid } from '../helpers/custom-validators/users'
|
import { isUserUsernameValid } from '../helpers/custom-validators/users'
|
||||||
import { getAccessToken, login } from '../../shared/extra-utils'
|
import { getAccessToken, login } from '../../shared/extra-utils'
|
||||||
|
import * as CliTable3 from 'cli-table3'
|
||||||
const Table = require('cli-table')
|
|
||||||
|
|
||||||
async function delInstance (url: string) {
|
async function delInstance (url: string) {
|
||||||
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
||||||
|
@ -108,10 +107,10 @@ program
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
||||||
|
|
||||||
const table = new Table({
|
const table = new CliTable3({
|
||||||
head: ['instance', 'login'],
|
head: ['instance', 'login'],
|
||||||
colWidths: [30, 30]
|
colWidths: [30, 30]
|
||||||
})
|
}) as CliTable3.HorizontalTable
|
||||||
|
|
||||||
settings.remotes.forEach(element => {
|
settings.remotes.forEach(element => {
|
||||||
if (!netrc.machines[element]) return
|
if (!netrc.machines[element]) return
|
||||||
|
|
|
@ -3,15 +3,11 @@ registerTSPaths()
|
||||||
|
|
||||||
import * as program from 'commander'
|
import * as program from 'commander'
|
||||||
import { PluginType } from '../../shared/models/plugins/plugin.type'
|
import { PluginType } from '../../shared/models/plugins/plugin.type'
|
||||||
import { getAccessToken } from '../../shared/extra-utils/users/login'
|
|
||||||
import { getMyUserInformation } from '../../shared/extra-utils/users/users'
|
|
||||||
import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
|
import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
|
||||||
import { getServerCredentials } from './cli'
|
import { getAdminTokenOrDie, getServerCredentials } from './cli'
|
||||||
import { User, UserRole } from '../../shared/models/users'
|
|
||||||
import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
|
import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
|
||||||
import { isAbsolute } from 'path'
|
import { isAbsolute } from 'path'
|
||||||
|
import * as CliTable3 from 'cli-table3'
|
||||||
const Table = require('cli-table')
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.name('plugins')
|
.name('plugins')
|
||||||
|
@ -82,10 +78,10 @@ async function pluginsListCLI () {
|
||||||
})
|
})
|
||||||
const plugins: PeerTubePlugin[] = res.body.data
|
const plugins: PeerTubePlugin[] = res.body.data
|
||||||
|
|
||||||
const table = new Table({
|
const table = new CliTable3({
|
||||||
head: ['name', 'version', 'homepage'],
|
head: ['name', 'version', 'homepage'],
|
||||||
colWidths: [ 50, 10, 50 ]
|
colWidths: [ 50, 10, 50 ]
|
||||||
})
|
}) as CliTable3.HorizontalTable
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const npmName = plugin.type === PluginType.PLUGIN
|
const npmName = plugin.type === PluginType.PLUGIN
|
||||||
|
@ -192,16 +188,3 @@ async function uninstallPluginCLI (options: any) {
|
||||||
console.log('Plugin uninstalled.')
|
console.log('Plugin uninstalled.')
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAdminTokenOrDie (url: string, username: string, password: string) {
|
|
||||||
const accessToken = await getAccessToken(url, username, password)
|
|
||||||
const resMe = await getMyUserInformation(url, accessToken)
|
|
||||||
const me: User = resMe.body
|
|
||||||
|
|
||||||
if (me.role !== UserRole.ADMINISTRATOR) {
|
|
||||||
console.error('Cannot list plugins if you are not administrator.')
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { registerTSPaths } from '../helpers/register-ts-paths'
|
||||||
|
registerTSPaths()
|
||||||
|
|
||||||
|
import * as program from 'commander'
|
||||||
|
import { getAdminTokenOrDie, getServerCredentials } from './cli'
|
||||||
|
import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
|
||||||
|
import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy'
|
||||||
|
import validator from 'validator'
|
||||||
|
import bytes = require('bytes')
|
||||||
|
import * as CliTable3 from 'cli-table3'
|
||||||
|
import { parse } from 'url'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('plugins')
|
||||||
|
.usage('[command] [options]')
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('list-remote-redundancies')
|
||||||
|
.description('List remote redundancies on your videos')
|
||||||
|
.option('-u, --url <url>', 'Server url')
|
||||||
|
.option('-U, --username <username>', 'Username')
|
||||||
|
.option('-p, --password <token>', 'Password')
|
||||||
|
.action(() => listRedundanciesCLI('my-videos'))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('list-my-redundancies')
|
||||||
|
.description('List your redundancies of remote videos')
|
||||||
|
.option('-u, --url <url>', 'Server url')
|
||||||
|
.option('-U, --username <username>', 'Username')
|
||||||
|
.option('-p, --password <token>', 'Password')
|
||||||
|
.action(() => listRedundanciesCLI('remote-videos'))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('add')
|
||||||
|
.description('Duplicate a video in your redundancy system')
|
||||||
|
.option('-u, --url <url>', 'Server url')
|
||||||
|
.option('-U, --username <username>', 'Username')
|
||||||
|
.option('-p, --password <token>', 'Password')
|
||||||
|
.option('-v, --video <videoId>', 'Video id to duplicate')
|
||||||
|
.action((options) => addRedundancyCLI(options))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('remove')
|
||||||
|
.description('Remove a video from your redundancies')
|
||||||
|
.option('-u, --url <url>', 'Server url')
|
||||||
|
.option('-U, --username <username>', 'Username')
|
||||||
|
.option('-p, --password <token>', 'Password')
|
||||||
|
.option('-v, --video <videoId>', 'Video id to remove from redundancies')
|
||||||
|
.action((options) => removeRedundancyCLI(options))
|
||||||
|
|
||||||
|
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 accessToken = await getAdminTokenOrDie(url, username, password)
|
||||||
|
|
||||||
|
const redundancies = await listVideoRedundanciesData(url, accessToken, target)
|
||||||
|
|
||||||
|
const table = new CliTable3({
|
||||||
|
head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
|
||||||
|
}) as CliTable3.HorizontalTable
|
||||||
|
|
||||||
|
for (const redundancy of redundancies) {
|
||||||
|
const webtorrentFiles = redundancy.redundancies.files
|
||||||
|
const streamingPlaylists = redundancy.redundancies.streamingPlaylists
|
||||||
|
|
||||||
|
let totalSize = ''
|
||||||
|
if (target === 'remote-videos') {
|
||||||
|
const tmp = webtorrentFiles.concat(streamingPlaylists)
|
||||||
|
.reduce((a, b) => a + b.size, 0)
|
||||||
|
|
||||||
|
totalSize = bytes(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instances = uniq(
|
||||||
|
webtorrentFiles.concat(streamingPlaylists)
|
||||||
|
.map(r => r.fileUrl)
|
||||||
|
.map(u => parse(u).host)
|
||||||
|
)
|
||||||
|
|
||||||
|
table.push([
|
||||||
|
redundancy.id.toString(),
|
||||||
|
redundancy.name,
|
||||||
|
redundancy.url,
|
||||||
|
webtorrentFiles.length,
|
||||||
|
streamingPlaylists.length,
|
||||||
|
instances.join('\n'),
|
||||||
|
totalSize
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(table.toString())
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRedundancyCLI (options: { videoId: number }) {
|
||||||
|
const { url, username, password } = await getServerCredentials(program)
|
||||||
|
const accessToken = await getAdminTokenOrDie(url, 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')
|
||||||
|
program.outputHelp()
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addVideoRedundancy({
|
||||||
|
url,
|
||||||
|
accessToken,
|
||||||
|
videoId: options[ 'video' ]
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Video will be duplicated by your instance!')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message.includes(409)) {
|
||||||
|
console.error('This video is already duplicated by your instance.')
|
||||||
|
} else if (err.message.includes(404)) {
|
||||||
|
console.error('This video id does not exist.')
|
||||||
|
} else {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRedundancyCLI (options: { videoId: number }) {
|
||||||
|
const { url, username, password } = await getServerCredentials(program)
|
||||||
|
const accessToken = await getAdminTokenOrDie(url, 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')
|
||||||
|
program.outputHelp()
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoId = parseInt(options[ 'video' ] + '', 10)
|
||||||
|
|
||||||
|
let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos')
|
||||||
|
let videoRedundancy = redundancies.find(r => videoId === r.id)
|
||||||
|
|
||||||
|
if (!videoRedundancy) {
|
||||||
|
redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos')
|
||||||
|
videoRedundancy = redundancies.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 removeVideoRedundancy({
|
||||||
|
url,
|
||||||
|
accessToken,
|
||||||
|
redundancyId: id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Video redundancy removed!')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) {
|
||||||
|
const res = await listVideoRedundancies({
|
||||||
|
url,
|
||||||
|
accessToken,
|
||||||
|
start: 0,
|
||||||
|
count: 100,
|
||||||
|
sort: 'name',
|
||||||
|
target
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.body.data as VideoRedundancy[]
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ program
|
||||||
.command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
|
.command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
|
||||||
.command('repl', 'initiate a REPL to access internals')
|
.command('repl', 'initiate a REPL to access internals')
|
||||||
.command('plugins [action]', 'manage instance plugins/themes').alias('p')
|
.command('plugins [action]', 'manage instance plugins/themes').alias('p')
|
||||||
|
.command('redundancy [action]', 'manage instance redundancies').alias('r')
|
||||||
|
|
||||||
/* Not Yet Implemented */
|
/* Not Yet Implemented */
|
||||||
program
|
program
|
||||||
|
|
|
@ -347,12 +347,15 @@ chunk-store-stream@^4.0.0:
|
||||||
block-stream2 "^2.0.0"
|
block-stream2 "^2.0.0"
|
||||||
readable-stream "^3.4.0"
|
readable-stream "^3.4.0"
|
||||||
|
|
||||||
cli-table@^0.3.1:
|
cli-table3@^0.5.1:
|
||||||
version "0.3.1"
|
version "0.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
|
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
|
||||||
integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM=
|
integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
|
||||||
dependencies:
|
dependencies:
|
||||||
colors "1.0.3"
|
object-assign "^4.1.0"
|
||||||
|
string-width "^2.1.1"
|
||||||
|
optionalDependencies:
|
||||||
|
colors "^1.1.2"
|
||||||
|
|
||||||
clivas@^0.2.0:
|
clivas@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
|
@ -364,10 +367,10 @@ code-point-at@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||||
|
|
||||||
colors@1.0.3:
|
colors@^1.1.2:
|
||||||
version "1.0.3"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||||
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
|
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||||
|
|
||||||
common-tags@^1.8.0:
|
common-tags@^1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
|
@ -1609,7 +1612,7 @@ string-width@^1.0.1:
|
||||||
is-fullwidth-code-point "^1.0.0"
|
is-fullwidth-code-point "^1.0.0"
|
||||||
strip-ansi "^3.0.0"
|
strip-ansi "^3.0.0"
|
||||||
|
|
||||||
"string-width@^1.0.2 || 2":
|
"string-width@^1.0.2 || 2", string-width@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||||
|
|
Loading…
Reference in New Issue