diff --git a/package.json b/package.json index ff4ff2b..ce0468d 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "electron-log": "^5.2.0", "electron-squirrel-startup": "^1.0.1", "framer-motion": "^11.5.6", + "glob": "^11.0.0", "p-settle": "^5.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/Analytics.tsx b/src/components/Analytics.tsx index b1f26da..9e9c98d 100644 --- a/src/components/Analytics.tsx +++ b/src/components/Analytics.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState } from 'react' import type { TimeSeries, TimeValue } from '../types' import { addSamplesToTimeseries } from '../utils/timeseries' import TimeseriesChart from './TimeseriesChart' -import { QUERY_KEYS, STORE_KEYS } from '../constants' +import { GameAnalyticsPattern, QUERY_KEYS, STORE_KEYS } from '../constants' import { useQuery } from '@tanstack/react-query' interface AnalyticsWindow { @@ -43,12 +43,6 @@ interface TimeseriesUpdates { type PlayersTimeseriesUpdates = Record -const soldContainer = /Successfully sold a container worth: \$([,\d]+.\d+)!/ -const soldContainer2 = /Sold \d+ item\(s\) for \$([,\d]+.\d+)!/ - -const playerKilledPVPLegacy = - /(\S+) has been killed by (\S+) with ([.\d]+) health left./ - const formatPlayerKey = (userName: string, serverName: string) => `${userName}@${serverName}` @@ -145,6 +139,15 @@ function topUpAnalyticsTimeSeries(analytics: Record) { return result } +export const useGameAnalyticsPatternsQuery = () => + useQuery({ + queryKey: [QUERY_KEYS.useGameAnalyticsPatterns], + queryFn: () => window.api.store.get(STORE_KEYS.gameAnalyticsPatterns), + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }) + export const useGameLogDirectoriesQuery = () => useQuery({ queryKey: [QUERY_KEYS.useGameLogDirectories], @@ -162,101 +165,75 @@ export function Analytics() { duration: 60 * 1000, samples: 24 * 60, }) + const gameAnalyticsPatterns = useGameAnalyticsPatternsQuery() const gameLogDirectories = useGameLogDirectoriesQuery() const [showGameLogDirectories, setShowGameLogDirectories] = useState(false) const [showGameLogFiles, setShowGameLogFiles] = useState(false) useEffect(() => { - let total = 0 - let earliestTimestamp: Date | undefined if (!gameLogDirectories.isSuccess || !gameLogDirectories.data?.length) return + const stats: Record< + string, + { + earliestTimestamp: Date + series: Record + } + > = {} + const handle = window.api.readGameLogs( gameLogDirectories.data, (lines) => { const updates: PlayersTimeseriesUpdates = {} for (const line of lines) { - if (!earliestTimestamp) earliestTimestamp = line.timestamp + const key = formatPlayerKey(line.userName, line.serverName) + const stat = + stats[key] || + (stats[key] = { earliestTimestamp: line.timestamp, series: {} }) + ensureAnalyticsTimeSeriesUpdate( updates, line.userName, line.serverName ).sources.add(line.source) - const playerKilledPVPLegacyMatch = line.content.match( - playerKilledPVPLegacy - ) - if (playerKilledPVPLegacyMatch) { - let userNameMatch = false - if (playerKilledPVPLegacyMatch[1] === line.userName) { - addAnalyticsTimeSeriesUpdate( - updates, - line.userName, - line.serverName, - 'deaths', - line.timestamp, - 1 - ) - userNameMatch = true + if (!gameAnalyticsPatterns.isSuccess) continue + for (const pattern of gameAnalyticsPatterns.data) { + const match = line.content.match(pattern.pattern) + if ( + !match || + (pattern.usernameIndex !== undefined && + match[pattern.usernameIndex] !== line.userName) + ) { + continue } - if (playerKilledPVPLegacyMatch[2] === line.userName) { - addAnalyticsTimeSeriesUpdate( - updates, - line.userName, - line.serverName, - 'kills', - line.timestamp, - 1 - ) - userNameMatch = true - } - if (userNameMatch) - console.log( - 'PVP kill', - playerKilledPVPLegacyMatch[1], - 'was killed by', - playerKilledPVPLegacyMatch[2], - 'with', - playerKilledPVPLegacyMatch[3], - 'health left', - line.source - ) - continue - } - - let soldContainerValue = 0 - const soldContainerMatch = line.content.match(soldContainer) - if (soldContainerMatch) { - soldContainerValue = parseFloat( - soldContainerMatch[1].replace(/,/g, '') - ) - } - const soldContainerMatch2 = line.content.match(soldContainer2) - if (soldContainerMatch2) { - soldContainerValue = parseFloat( - soldContainerMatch2[1].replace(/,/g, '') - ) - } - - if (soldContainerValue) { - total += soldContainerValue + const value = + (pattern.valueIndex && + parseFloat(match[pattern.valueIndex]?.replace(/,/g, ''))) || + 1 + const seriesStat = + stat.series[pattern.name] || + (stat.series[pattern.name] = { total: 0 }) + seriesStat.total += value addAnalyticsTimeSeriesUpdate( updates, line.userName, line.serverName, - 'sold', + pattern.name, line.timestamp, - soldContainerValue + value ) + const totalSeconds = - (line.timestamp.getTime() - earliestTimestamp.getTime()) / 1000 - const ratePerMinute = (total * 60) / totalSeconds + (line.timestamp.getTime() - stat.earliestTimestamp.getTime()) / + 1000 + const ratePerMinute = (seriesStat.total * 60) / totalSeconds console.log( - `${line.userName}@${line.serverName} Sold container value`, - soldContainerValue, + `${line.userName}@${line.serverName} ${pattern.name}`, + value, 'Total', - total, + seriesStat.total, 'Rate', ratePerMinute.toFixed(2), 'per minute', @@ -265,7 +242,6 @@ export function Analytics() { line.timestamp, line.source ) - continue } } setAnalytics((analytics) => @@ -278,6 +254,8 @@ export function Analytics() { return () => window.api.removeListener(handle) }, [ analyticsWindow, + gameAnalyticsPatterns.isSuccess, + gameAnalyticsPatterns.data, gameLogDirectories.isSuccess, gameLogDirectories.data, setAnalytics, @@ -307,7 +285,9 @@ export function Analytics() { onChange={(event) => setAnalyticsProfile(event.target.value)} > {Object.keys(analytics).map((profile) => ( - + ))} diff --git a/src/constants.ts b/src/constants.ts index 47e23e7..81cb612 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -185,6 +185,15 @@ export const gameAccount = z.object({ yggdrasilToken: minecraftLoginResponse, }) +export const gameAnalyticsPattern = z.object({ + name: z.string(), + pattern: z + .union([z.string(), z.instanceof(RegExp)]) + .transform((x) => new RegExp(x)), + usernameIndex: z.number().optional(), + valueIndex: z.number().optional(), +}) + export const gameInstall = z.object({ name: z.string(), path: z.string(), @@ -213,10 +222,12 @@ export type MojangStringsTemplate = z.infer export type XSTSAuthorizeResponse = z.infer export type GameAccount = z.infer +export type GameAnalyticsPattern = z.infer export type GameInstall = z.infer export type Waypoint = z.infer export const STORE_KEYS = { + gameAnalyticsPatterns: 'gameAnalyticsPatterns', gameAccounts: 'gameAccounts', gameInstalls: 'gameInstalls', gameLogDirectories: 'gameLogDirectories', @@ -224,6 +235,7 @@ export const STORE_KEYS = { } export const QUERY_KEYS = { + useGameAnalyticsPatterns: 'useGameAnalyticsPatterns', useGameInstalls: 'useGameInstalls', useGameLogDirectories: 'useGameLogDirectories', useMojangVersionManifests: 'useMojangVersionManifests', diff --git a/src/store.ts b/src/store.ts index be9094f..f12e49d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,28 +1,46 @@ import { z } from 'zod' import Store from 'zod-electron-store' -import { getDefaultGameLogDirectories } from './utils/gamelog' import { gameAccount, GameAccount, + gameAnalyticsPattern, gameInstall, GameInstall, STORE_KEYS, waypoint, } from './constants' +import { + getDefaultAnalyticsPatterns, + getDefaultGameLogDirectories, +} from './utils/gamelog' export { Store } export const storeSchema = z.object({ gameAccounts: z.array(gameAccount).default([]), + gameAnalyticsPatterns: z + .array(gameAnalyticsPattern) + .default(getDefaultAnalyticsPatterns()), gameInstalls: z.array(gameInstall).default([]), - gameLogDirectories: z.array(z.string()).default(getDefaultGameLogDirectories()), + gameLogDirectories: z + .array(z.string()) + .default(getDefaultGameLogDirectories()), waypoints: z.array(waypoint).default([]), }) export type StoreSchema = z.infer -export const newStore = () => new Store({ schema: storeSchema }) +export const newStore = () => + new Store({ + schema: storeSchema, + serialize: (value) => + JSON.stringify( + value, + (_, x) => (x instanceof RegExp ? x.toString().slice(1,-1) : x), + '\t' + ), + }) export const updateGameAccount = ( store: Store, diff --git a/src/utils/gamelog.ts b/src/utils/gamelog.ts index d3ba17f..8e9ca44 100644 --- a/src/utils/gamelog.ts +++ b/src/utils/gamelog.ts @@ -1,10 +1,12 @@ import fs from 'fs' +import { glob } from 'glob' import os from 'os' import path from 'path' import split2 from 'split2' import stream from 'stream' import { Tail } from 'tail' import zlib from 'zlib' +import { GameAnalyticsPattern } from '../constants' import type { GameLog, GameLogLine } from '../types' export interface GameLogContext { @@ -55,16 +57,36 @@ export function getDefaultGameLogDirectories() { `${process.env.HOME}/Library/Application Support/minecraft/logs` ) paths.push(`${process.env.HOME}/.lunarclient/offline/multiver/logs`) + paths.push('/Applications/MultiMC.app/Data/instances/*/.minecraft/logs') break } - return paths.filter((path) => { - try { - return fs.statSync(path).isDirectory() - } catch { - return false - } - }) + return paths +} + +export function getDefaultAnalyticsPatterns(): GameAnalyticsPattern[] { + return [ + { + name: 'sold', + pattern: /Successfully sold a container worth: \$([,\d]+.\d+)!/, + valueIndex: 1, + }, + { + name: 'sold', + pattern: /Sold \d+ item\(s\) for \$([,\d]+.\d+)!/, + valueIndex: 1, + }, + { + name: 'deaths', + pattern: /(\S+) has been killed by (\S+) with ([.\d]+) health left./, + usernameIndex: 1, + }, + { + name: 'kills', + pattern: /(\S+) has been killed by (\S+) with ([.\d]+) health left./, + usernameIndex: 2, + }, + ] } export async function findGameLogFiles( @@ -75,12 +97,9 @@ export async function findGameLogFiles( const logFiles: string[] = [] for (const dir of gameLogDirectories) { try { - const files = await fs.promises.readdir(dir) - logFiles.push( - ...files - .filter((file) => file.endsWith('.log') || file.endsWith('.log.gz')) - .map((file) => path.join(dir, file)) - ) + const pattern = path.join(dir, '*.{log,log.gz}') + const files = await glob(pattern) + logFiles.push(...files) } catch { continue } diff --git a/yarn.lock b/yarn.lock index e460c78..9d90554 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1634,6 +1634,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" @@ -3424,6 +3436,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -4608,6 +4629,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fork-ts-checker-webpack-plugin@^7.2.13: version "7.3.0" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz#a9c984a018493962360d7c7e77a67b44a2d5f3aa" @@ -4887,6 +4916,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -5593,6 +5634,13 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -5954,6 +6002,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^11.0.0: + version "11.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39" + integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -6108,6 +6161,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -6185,6 +6245,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -6625,6 +6690,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -6725,6 +6795,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-to-regexp@0.1.10: version "0.1.10" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" @@ -7551,6 +7629,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -7704,6 +7787,15 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +"string-width-cjs@npm: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" + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -7713,7 +7805,7 @@ statuses@2.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0, string-width@^5.0.1: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -7764,6 +7856,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +"strip-ansi-cjs@npm: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-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -8507,6 +8606,15 @@ word-wrap@^1.2.3, word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"