Browse Source

CLI: Remaining content-directory/media functionality - https://github.com/Joystream/joystream/issues/1589

Leszek Wiesner 4 years ago
parent
commit
375de1519b

+ 1 - 0
cli/package.json

@@ -26,6 +26,7 @@
     "cli-ux": "^5.4.5",
     "fluent-ffmpeg": "^2.1.2",
     "inquirer": "^7.1.0",
+    "inquirer-datepicker-prompt": "^0.4.2",
     "ipfs-http-client": "^47.0.1",
     "ipfs-only-hash": "^1.0.2",
     "it-all": "^1.0.4",

+ 1 - 0
cli/src/@types/inquirer-datepicker-prompt/index.d.ts

@@ -0,0 +1 @@
+declare module 'inquirer-datepicker-prompt'

+ 1 - 1
cli/src/Api.ts

@@ -523,7 +523,7 @@ export default class Api {
   }
 
   async entityById(id: number): Promise<Entity | null> {
-    const exists = !!(await this._api.query.contentDirectory.curatorGroupById.size(id))
+    const exists = !!(await this._api.query.contentDirectory.entityById.size(id)).toNumber()
     return exists ? await this._api.query.contentDirectory.entityById<Entity>(id) : null
   }
 

+ 19 - 0
cli/src/base/ContentDirectoryCommandBase.ts

@@ -19,6 +19,10 @@ import _ from 'lodash'
 import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { createType } from '@joystream/types'
 import chalk from 'chalk'
+import { flags } from '@oclif/command'
+
+const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
+type Context = typeof CONTEXTS[number]
 
 /**
  * Abstract base class for commands related to content directory
@@ -26,6 +30,21 @@ import chalk from 'chalk'
 export default abstract class ContentDirectoryCommandBase extends RolesCommandBase {
   group = WorkingGroups.Curators // override group for RolesCommandBase
 
+  static contextFlag = flags.enum({
+    name: 'context',
+    required: false,
+    description: `Actor context to execute the command in (${CONTEXTS.join('/')})`,
+    options: [...CONTEXTS],
+  })
+
+  async promptForContext(message = 'Choose in which context you wish to execute the command'): Promise<Context> {
+    return this.simplePrompt({
+      message,
+      type: 'list',
+      choices: CONTEXTS.map((c) => ({ name: c, value: c })),
+    })
+  }
+
   // Use when lead access is required in given command
   async requireLead(): Promise<void> {
     await this.getRequiredLead()

+ 5 - 0
cli/src/base/DefaultCommandBase.ts

@@ -2,6 +2,7 @@ import ExitCodes from '../ExitCodes'
 import Command from '@oclif/command'
 import inquirer, { DistinctQuestion } from 'inquirer'
 import chalk from 'chalk'
+import inquirerDatepicker from 'inquirer-datepicker-prompt'
 
 /**
  * Abstract base class for pretty much all commands
@@ -103,4 +104,8 @@ export default abstract class DefaultCommandBase extends Command {
     if (!err) this.exit(ExitCodes.OK)
     super.finally(err)
   }
+
+  async init() {
+    inquirer.registerPrompt('datetime', inquirerDatepicker)
+  }
 }

+ 61 - 0
cli/src/base/MediaCommandBase.ts

@@ -0,0 +1,61 @@
+import ContentDirectoryCommandBase from './ContentDirectoryCommandBase'
+import { VideoEntity } from 'cd-schemas/types/entities'
+import fs from 'fs'
+import { DistinctQuestion } from 'inquirer'
+
+const MAX_USER_LICENSE_CONTENT_LENGTH = 4096
+
+/**
+ * Abstract base class for higher-level media commands
+ */
+export default abstract class MediaCommandBase extends ContentDirectoryCommandBase {
+  async promptForNewLicense(): Promise<VideoEntity['license']> {
+    let license: VideoEntity['license']
+    const licenseType: 'known' | 'custom' = await this.simplePrompt({
+      type: 'list',
+      message: 'Choose license type',
+      choices: [
+        { name: 'Creative Commons', value: 'known' },
+        { name: 'Custom (user-defined)', value: 'custom' },
+      ],
+    })
+    if (licenseType === 'known') {
+      license = { new: { knownLicense: await this.promptForEntityId('Choose License', 'KnownLicense', 'code') } }
+    } else {
+      let licenseContent: null | string = null
+      while (licenseContent === null) {
+        try {
+          const licensePath = await this.simplePrompt({ message: 'Path to license file:' })
+          licenseContent = fs.readFileSync(licensePath).toString()
+        } catch (e) {
+          this.warn("The file was not found or couldn't be accessed, try again...")
+        }
+        if (licenseContent !== null && licenseContent.length > MAX_USER_LICENSE_CONTENT_LENGTH) {
+          this.warn(`The license content cannot be more than ${MAX_USER_LICENSE_CONTENT_LENGTH} characters long`)
+          licenseContent = null
+        }
+      }
+      license = { new: { userDefinedLicense: { new: { content: licenseContent } } } }
+    }
+
+    return license
+  }
+
+  async promptForPublishedBeforeJoystream(type: 'add' | 'update', current?: number): Promise<number | undefined> {
+    const publishedBefore = await this.simplePrompt({
+      type: 'confirm',
+      message: `Do you want to ${type} first publication date (publishedBeforeJoystream)?`,
+      default: false,
+    })
+    if (publishedBefore) {
+      const options = ({
+        type: 'datetime',
+        message: 'Date of first publication',
+        format: ['yyyy', '-', 'mm', '-', 'dd', ' ', 'hh', ':', 'MM', ' ', 'TT'],
+      } as unknown) as DistinctQuestion // Need to assert, because we use datetime plugin which has no TS support
+      const date = await this.simplePrompt(options)
+      return Math.floor(new Date(date).getTime() / 1000)
+    }
+    return current
+  }
+}

+ 50 - 0
cli/src/commands/content-directory/initialize.ts

@@ -0,0 +1,50 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { CreateClass } from 'cd-schemas/types/extrinsics/CreateClass'
+import { getInputs, InputParser, ExtrinsicsHelper } from 'cd-schemas'
+import { AddClassSchema } from 'cd-schemas/types/extrinsics/AddClassSchema'
+import { EntityBatch } from 'cd-schemas/types/EntityBatch'
+
+export default class InitializeCommand extends ContentDirectoryCommandBase {
+  static description =
+    'Initialize content directory with input data from @joystream/content library. Requires lead access.'
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    await this.requireLead()
+    await this.requestAccountDecoding(account)
+
+    const classInputs = getInputs<CreateClass>('classes').map(({ data }) => data)
+    const schemaInputs = getInputs<AddClassSchema>('schemas').map(({ data }) => data)
+    const entityBatchInputs = getInputs<EntityBatch>('entityBatches').map(({ data }) => data)
+
+    const currentClasses = await this.getApi().availableClasses()
+
+    if (currentClasses.length) {
+      this.log('There are already some existing classes in the current content directory.')
+      await this.requireConfirmation('Do you wish to continue anyway?')
+    }
+
+    const txHelper = new ExtrinsicsHelper(this.getOriginalApi())
+    const parser = new InputParser(this.getOriginalApi(), classInputs, schemaInputs, entityBatchInputs)
+
+    this.log(`Initializing classes (${classInputs.length} input files found)...\n`)
+    const classExtrinsics = parser.getCreateClassExntrinsics()
+    await txHelper.sendAndCheck(account, classExtrinsics, 'Class initialization failed!')
+
+    this.log(`Initializing schemas (${schemaInputs.length} input files found)...\n`)
+    const schemaExtrinsics = await parser.getAddSchemaExtrinsics()
+    await txHelper.sendAndCheck(account, schemaExtrinsics, 'Schemas initialization failed!')
+
+    this.log(`Initializing entities (${entityBatchInputs.length} input files found)`)
+    const entityOperations = await parser.getEntityBatchOperations()
+
+    this.log(`Sending Transaction extrinsic (${entityOperations.length} operations)...\n`)
+    await txHelper.sendAndCheck(
+      account,
+      [this.getOriginalApi().tx.contentDirectory.transaction({ Lead: null }, entityOperations)],
+      'Entity initialization failed!'
+    )
+
+    this.log('DONE')
+  }
+}

+ 57 - 0
cli/src/commands/content-directory/removeEntity.ts

@@ -0,0 +1,57 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { Actor } from '@joystream/types/content-directory'
+import { createType } from '@joystream/types'
+import ExitCodes from '../../ExitCodes'
+
+export default class RemoveEntityCommand extends ContentDirectoryCommandBase {
+  static description = 'Removes a single entity by id (can be executed in Member, Curator or Lead context)'
+  static flags = {
+    context: ContentDirectoryCommandBase.contextFlag,
+  }
+
+  static args = [
+    {
+      name: 'id',
+      required: true,
+      description: 'ID of the entity to remove',
+    },
+  ]
+
+  async run() {
+    let {
+      args: { id },
+      flags: { context },
+    } = this.parse(RemoveEntityCommand)
+
+    const entity = await this.getEntity(id, undefined, undefined, false)
+    const [, entityClass] = await this.classEntryByNameOrId(entity.class_id.toString())
+
+    if (!context) {
+      context = await this.promptForContext()
+    }
+
+    const account = await this.getRequiredSelectedAccount()
+    let actor: Actor
+    if (context === 'Curator') {
+      actor = await this.getCuratorContext([entityClass.name.toString()])
+    } else if (context === 'Member') {
+      const memberId = await this.getRequiredMemberId()
+      if (
+        !entity.entity_permissions.controller.isOfType('Member') ||
+        entity.entity_permissions.controller.asType('Member').toNumber() !== memberId
+      ) {
+        this.error('You are not the entity controller!', { exit: ExitCodes.AccessDenied })
+      }
+      actor = createType('Actor', { Member: memberId })
+    } else {
+      actor = createType('Actor', { Lead: null })
+    }
+
+    await this.requireConfirmation(
+      `Are you sure you want to remove entity ${id} of class ${entityClass.name.toString()}?`
+    )
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'removeEntity', [actor, id])
+  }
+}

+ 44 - 0
cli/src/commands/media/removeChannel.ts

@@ -0,0 +1,44 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { Entity } from '@joystream/types/content-directory'
+import { createType } from '@joystream/types'
+import { ChannelEntity } from 'cd-schemas/types/entities'
+
+export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
+  static description = 'Removes a channel (required controller access).'
+  static args = [
+    {
+      name: 'id',
+      required: false,
+      description: 'ID of the Channel entity',
+    },
+  ]
+
+  async run() {
+    const {
+      args: { id },
+    } = this.parse(RemoveChannelCommand)
+
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = createType('Actor', { Member: memberId })
+
+    await this.requestAccountDecoding(account)
+
+    let channelEntity: Entity, channelId: number
+    if (id) {
+      channelId = parseInt(id)
+      channelEntity = await this.getEntity(channelId, 'Channel', memberId)
+    } else {
+      const [id, channel] = await this.promptForEntityEntry('Select a channel to remove', 'Channel', 'title', memberId)
+      channelId = id.toNumber()
+      channelEntity = channel
+    }
+    const channel = await this.parseToKnownEntityJson<ChannelEntity>(channelEntity)
+
+    await this.requireConfirmation(`Are you sure you want to remove "${channel.title}" channel?`)
+
+    const api = this.getOriginalApi()
+    this.log(`Removing Channel entity (ID: ${channelId})...`)
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.removeEntity(actor, channelId))
+  }
+}

+ 47 - 0
cli/src/commands/media/removeVideo.ts

@@ -0,0 +1,47 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { Entity } from '@joystream/types/content-directory'
+import { VideoEntity } from 'cd-schemas/types/entities'
+import { createType } from '@joystream/types'
+
+export default class RemoveVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Remove given Video entity and associated entities (VideoMedia, License) from content directory.'
+  static args = [
+    {
+      name: 'id',
+      required: false,
+      description: 'ID of the Video entity',
+    },
+  ]
+
+  async run() {
+    const {
+      args: { id },
+    } = this.parse(RemoveVideoCommand)
+
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = createType('Actor', { Member: memberId })
+
+    await this.requestAccountDecoding(account)
+
+    let videoEntity: Entity, videoId: number
+    if (id) {
+      videoId = parseInt(id)
+      videoEntity = await this.getEntity(videoId, 'Video', memberId)
+    } else {
+      const [id, video] = await this.promptForEntityEntry('Select a video to remove', 'Video', 'title', memberId)
+      videoId = id.toNumber()
+      videoEntity = video
+    }
+
+    const video = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
+
+    const api = this.getOriginalApi()
+    this.log(`Removing the Video entity (ID: ${videoId})...`)
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.removeEntity(actor, videoId))
+    this.log(`Removing the VideoMedia entity (ID: ${video.media})...`)
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.removeEntity(actor, video.media))
+    this.log(`Removing the License entity (ID: ${video.license})...`)
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.removeEntity(actor, video.license))
+  }
+}

+ 17 - 24
cli/src/commands/media/updateVideo.ts

@@ -1,16 +1,15 @@
-import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
 import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
-import { LicenseEntity } from 'cd-schemas/types/entities/LicenseEntity'
 import { InputParser } from 'cd-schemas'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { Actor, Entity } from '@joystream/types/content-directory'
 import { createType } from '@joystream/types'
 import { flags } from '@oclif/command'
+import MediaCommandBase from '../../base/MediaCommandBase'
 
-export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
-  static description = 'Update existing video information (requires a membership).'
+export default class UpdateVideoCommand extends MediaCommandBase {
+  static description = 'Update existing video information (requires controller/maintainer access).'
   static flags = {
     // TODO: ...IOFlags, - providing input as json
     asCurator: flags.boolean({
@@ -38,7 +37,7 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
     let memberId: number | undefined, actor: Actor
 
     if (asCurator) {
-      actor = await this.getCuratorContext(['Video', 'License'])
+      actor = await this.getCuratorContext(['Video'])
     } else {
       memberId = await this.getRequiredMemberId()
       actor = createType('Actor', { Member: memberId })
@@ -59,7 +58,11 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
     const currentValues = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
     const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
 
-    const { language: currLanguageId, category: currCategoryId, license: currLicenseId } = currentValues
+    const {
+      language: currLanguageId,
+      category: currCategoryId,
+      publishedBeforeJoystream: currPublishedBeforeJoystream,
+    } = currentValues
 
     const customizedPrompts: JsonSchemaCustomPrompts<VideoEntity> = [
       [
@@ -70,20 +73,13 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
         'category',
         () => this.promptForEntityId('Choose Video category', 'ContentCategory', 'name', undefined, currCategoryId),
       ],
+      [
+        'publishedBeforeJoystream',
+        () => this.promptForPublishedBeforeJoystream('update', currPublishedBeforeJoystream),
+      ],
     ]
     const videoPrompter = new JsonSchemaPrompter<VideoEntity>(videoJsonSchema, currentValues, customizedPrompts)
 
-    // Updating a license is currently a bit more tricky since it's a nested relation
-    const currKnownLicenseId = (await this.getAndParseKnownEntity<LicenseEntity>(currLicenseId)).knownLicense
-    const knownLicenseId = await this.promptForEntityId(
-      'Choose a license',
-      'KnownLicense',
-      'code',
-      undefined,
-      currKnownLicenseId
-    )
-    const updatedLicense: LicenseEntity = { knownLicense: knownLicenseId }
-
     // Prompt for other video data
     const updatedProps: Partial<VideoEntity> = await videoPrompter.promptMultipleProps([
       'language',
@@ -93,7 +89,10 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
       'thumbnailURL',
       'duration',
       'isPublic',
+      'isExplicit',
       'hasMarketing',
+      'publishedBeforeJoystream',
+      'skippableIntroDuration',
     ])
 
     if (asCurator) {
@@ -105,12 +104,6 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
     // Parse inputs into operations and send final extrinsic
     const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
     const videoUpdateOperations = await inputParser.getEntityUpdateOperations(updatedProps, 'Video', videoId)
-    const licenseUpdateOperations = await inputParser.getEntityUpdateOperations(
-      updatedLicense,
-      'License',
-      currentValues.license
-    )
-    const operations = [...videoUpdateOperations, ...licenseUpdateOperations]
-    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, operations], true)
+    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, videoUpdateOperations], true)
   }
 }

+ 59 - 0
cli/src/commands/media/updateVideoLicense.ts

@@ -0,0 +1,59 @@
+import MediaCommandBase from '../../base/MediaCommandBase'
+import { LicenseEntity, VideoEntity } from 'cd-schemas/types/entities'
+import { InputParser } from 'cd-schemas'
+import { Entity } from '@joystream/types/content-directory'
+import { createType } from '@joystream/types'
+
+export default class UpdateVideoLicenseCommand extends MediaCommandBase {
+  static description = 'Update existing video license (requires controller/maintainer access).'
+  // TODO: ...IOFlags, - providing input as json
+
+  static args = [
+    {
+      name: 'id',
+      description: 'ID of the Video',
+      required: false,
+    },
+  ]
+
+  async run() {
+    const {
+      args: { id },
+    } = this.parse(UpdateVideoLicenseCommand)
+
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = createType('Actor', { Member: memberId })
+
+    await this.requestAccountDecoding(account)
+
+    let videoEntity: Entity, videoId: number
+    if (id) {
+      videoId = parseInt(id)
+      videoEntity = await this.getEntity(videoId, 'Video', memberId)
+    } else {
+      const [id, video] = await this.promptForEntityEntry('Select a video to update', 'Video', 'title', memberId)
+      videoId = id.toNumber()
+      videoEntity = video
+    }
+
+    const video = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
+    const currentLicense = await this.getAndParseKnownEntity<LicenseEntity>(video.license)
+
+    this.log('Current license:', currentLicense)
+
+    const updateInput: Partial<VideoEntity> = {
+      license: await this.promptForNewLicense(),
+    }
+
+    const api = this.getOriginalApi()
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
+    const videoUpdateOperations = await inputParser.getEntityUpdateOperations(updateInput, 'Video', videoId)
+
+    this.log('Setting new license...')
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.transaction(actor, videoUpdateOperations), true)
+
+    this.log(`Removing old License entity (ID: ${video.license})...`)
+    await this.sendAndFollowTx(account, api.tx.contentDirectory.removeEntity(actor, video.license))
+  }
+}

+ 10 - 4
cli/src/commands/media/uploadVideo.ts

@@ -1,4 +1,3 @@
-import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
 import VideoMediaEntitySchema from 'cd-schemas/schemas/entities/VideoMediaEntity.schema.json'
 import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
@@ -20,6 +19,7 @@ import last from 'it-last'
 import toBuffer from 'it-to-buffer'
 import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'
 import ffmpeg from 'fluent-ffmpeg'
+import MediaCommandBase from '../../base/MediaCommandBase'
 
 ffmpeg.setFfmpegPath(ffmpegInstaller.path)
 
@@ -34,7 +34,7 @@ type VideoMetadata = {
   duration?: number
 }
 
-export default class UploadVideoCommand extends ContentDirectoryCommandBase {
+export default class UploadVideoCommand extends MediaCommandBase {
   static description = 'Upload a new Video to a channel (requires a membership).'
   static flags = {
     // TODO: ...IOFlags, - providing input as json
@@ -331,7 +331,6 @@ export default class UploadVideoCommand extends ContentDirectoryCommandBase {
     const { pixelWidth, pixelHeight } = await videoMediaPrompter.promptMultipleProps(['pixelWidth', 'pixelHeight'])
     const language = await this.promptForEntityId('Choose Video language', 'Language', 'name')
     const category = await this.promptForEntityId('Choose Video category', 'ContentCategory', 'name')
-    const license = await this.promptForEntityId('Choose License', 'KnownLicense', 'code')
     const videoProps = await videoPrompter.promptMultipleProps([
       'title',
       'description',
@@ -340,8 +339,14 @@ export default class UploadVideoCommand extends ContentDirectoryCommandBase {
       'isPublic',
       'isExplicit',
       'hasMarketing',
+      'skippableIntroDuration',
     ])
 
+    const license = await videoPrompter.promptSingleProp('license', () => this.promptForNewLicense())
+    const publishedBeforeJoystream = await videoPrompter.promptSingleProp('publishedBeforeJoystream', () =>
+      this.promptForPublishedBeforeJoystream('add')
+    )
+
     // Create final inputs
     const videoMediaInput: VideoMediaEntity = {
       encoding,
@@ -355,8 +360,9 @@ export default class UploadVideoCommand extends ContentDirectoryCommandBase {
       channel: channelId,
       language,
       category,
-      license: { new: { knownLicense: license } },
+      license,
       media: { new: videoMediaInput },
+      publishedBeforeJoystream,
     }
 
     // Parse inputs into operations and send final extrinsic

+ 79 - 3
yarn.lock

@@ -5568,7 +5568,7 @@ ansi-colors@^4.1.1:
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
   integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
 
-ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
+ansi-escapes@^3.0.0, ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
   integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
@@ -7545,6 +7545,11 @@ character-reference-invalid@^1.0.0:
   resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz#1647f4f726638d3ea4a750cf5d1975c1c7919a85"
   integrity sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==
 
+chardet@^0.4.0:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+  integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=
+
 chardet@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -9133,11 +9138,16 @@ date-fns@^2.0.1:
   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f"
   integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==
 
-dateformat@^3.0.0:
+dateformat@^3.0.0, dateformat@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
   integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
 
+datejs@^1.0.0-rc3:
+  version "1.0.0-rc3"
+  resolved "https://registry.yarnpkg.com/datejs/-/datejs-1.0.0-rc3.tgz#bffa1efedefeb41fdd8a242af55afa01fb58de57"
+  integrity sha1-v/oe/t7+tB/diiQq9Vr6AftY3lc=
+
 de-indent@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -11188,6 +11198,15 @@ extend@^3.0.0, extend@~3.0.2:
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
   integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
 
+external-editor@^2.0.4:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
+  integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==
+  dependencies:
+    chardet "^0.4.0"
+    iconv-lite "^0.4.17"
+    tmp "^0.0.33"
+
 external-editor@^3.0.3:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
@@ -13419,7 +13438,7 @@ i18next@^19.6.3:
   dependencies:
     "@babel/runtime" "^7.10.1"
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
+iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -13649,6 +13668,19 @@ init-package-json@^1.10.3:
     validate-npm-package-license "^3.0.1"
     validate-npm-package-name "^3.0.0"
 
+inquirer-datepicker-prompt@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/inquirer-datepicker-prompt/-/inquirer-datepicker-prompt-0.4.2.tgz#d4419daccf7fdd099751ecb82210560a1437f67c"
+  integrity sha1-1EGdrM9/3QmXUey4IhBWChQ39nw=
+  dependencies:
+    chalk "^2.1.0"
+    cli-cursor "^2.1.0"
+    dateformat "^3.0.2"
+    datejs "^1.0.0-rc3"
+    inquirer "^3.3.0"
+    lodash "^4.17.4"
+    util "^0.10.3"
+
 inquirer@6.5.0:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42"
@@ -13668,6 +13700,26 @@ inquirer@6.5.0:
     strip-ansi "^5.1.0"
     through "^2.3.6"
 
+inquirer@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+  integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==
+  dependencies:
+    ansi-escapes "^3.0.0"
+    chalk "^2.0.0"
+    cli-cursor "^2.1.0"
+    cli-width "^2.0.0"
+    external-editor "^2.0.4"
+    figures "^2.0.0"
+    lodash "^4.3.0"
+    mute-stream "0.0.7"
+    run-async "^2.2.0"
+    rx-lite "^4.0.8"
+    rx-lite-aggregates "^4.0.8"
+    string-width "^2.1.0"
+    strip-ansi "^4.0.0"
+    through "^2.3.6"
+
 inquirer@^6.2.0:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
@@ -16839,6 +16891,11 @@ lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
+lodash@^4.3.0:
+  version "4.17.20"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
 log-driver@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
@@ -22349,6 +22406,18 @@ run@^1.4.0:
   dependencies:
     minimatch "*"
 
+rx-lite-aggregates@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+  integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=
+  dependencies:
+    rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+  integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
+
 rxjs-compat@^6.6.0:
   version "6.6.2"
   resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.6.2.tgz#23592564243cf24641a5d2e2d2acfc8f6b127186"
@@ -25469,6 +25538,13 @@ util@0.10.3:
   dependencies:
     inherits "2.0.1"
 
+util@^0.10.3:
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+  dependencies:
+    inherits "2.0.3"
+
 util@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"