diff --git a/package.json b/package.json index 4c1f7143b5..6dddda1fc0 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@types/multer": "^1.4.12", "canvas-confetti": "^1.9.3", "mitt": "^3.0.1", + "ora": "^9.4.1", "react-hook-form-persist": "^3.0.0", "recharts": "^2.15.3", "typescript": "^5.7.3" diff --git a/src/backend/package.json b/src/backend/package.json index faa6faf427..3829a1a323 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -8,7 +8,8 @@ "build": "NODE_OPTIONS='--max-old-space-size=8192' tsc --noEmit false", "start": "node -r dotenv/config dist/backend/index.js", "prisma:manual": "tsx --import dotenv/config ./src/prisma/manual.ts", - "prisma:dev-seed": "tsx --import dotenv/config ./src/prisma/dev-seed.ts" + "prisma:dev-seed": "tsx --import dotenv/config ./src/prisma/dev-seed.ts", + "prisma:dev-setup": "docker compose up -d database && yarn prisma:reset:no-seed --force && yarn workspace backend prisma:dev-seed" }, "dependencies": { "@prisma/client": "^6.2.1", diff --git a/src/backend/src/prisma/context.ts b/src/backend/src/prisma/context.ts index 7d9d81db5f..55833c73c3 100644 --- a/src/backend/src/prisma/context.ts +++ b/src/backend/src/prisma/context.ts @@ -14,6 +14,7 @@ import { Team_Type, Team, Unit, + Work_Package, Vendor, WBS_Element } from '@prisma/client'; @@ -87,3 +88,8 @@ export type ProjectContext = { }; timeline: DateRange; }; + +export type WorkPackageContext = { + workPackage: Work_Package & { wbsElement: WBS_Element }; + timeline: DateRange; +}; diff --git a/src/backend/src/prisma/dates.ts b/src/backend/src/prisma/dates.ts index 9c6240437f..8af22dc738 100644 --- a/src/backend/src/prisma/dates.ts +++ b/src/backend/src/prisma/dates.ts @@ -1,4 +1,6 @@ import { Faker } from '@faker-js/faker'; +import { DateRange } from './context.js'; +import dayjs from 'dayjs'; /* https://fakerjs.dev/api/date.html */ @@ -7,6 +9,14 @@ interface WithFaker { faker: Faker; } +export const SECOND_MS = 1000; +export const MINUTE_MS = SECOND_MS * 60; +export const HOUR_MS = MINUTE_MS * 60; +export const DAY_MS = HOUR_MS * 24; +export const WEEK_MS = DAY_MS * 7; + +export const DAYS_PER_WEEK = 7; + export function generateRandomDate({ faker }: WithFaker, from?: Date, to?: Date) { return faker.date.between({ from: from ?? '2000-01-01', to: to ?? Date.now() }); } @@ -14,3 +24,8 @@ export function generateRandomDate({ faker }: WithFaker, from?: Date, to?: Date) export function generateRandomDateAround({ faker }: WithFaker, date: Date) { return faker.date.recent({ days: 5, refDate: date }); } + +export const daysBetween = ({ start, end }: DateRange): number => Math.max(0, dayjs(end).diff(dayjs(start), 'day')); + +export const clampDate = (date: Date, { start, end }: DateRange): Date => + new Date(Math.min(Math.max(date.getTime(), start.getTime()), end.getTime())); diff --git a/src/backend/src/prisma/dev-seed.ts b/src/backend/src/prisma/dev-seed.ts index cad6541a62..eaff3d271d 100644 --- a/src/backend/src/prisma/dev-seed.ts +++ b/src/backend/src/prisma/dev-seed.ts @@ -15,6 +15,7 @@ import { ShopProcess } from './seed/shop.process.js'; import { TeamProcess } from './seed/team.process.js'; import { ProjectProcess } from './seed/project.process.js'; import { SchedulingProcess } from './seed/scheduling.process.js'; +import { WorkPackageProcess } from './seed/work-package.process.js'; const prisma = new PrismaClient(); @@ -29,7 +30,8 @@ await new SeedRunner() new SchedulingProcess(), new TeamProcess(), new ShopProcess(), - new ProjectProcess() + new ProjectProcess(), + new WorkPackageProcess() ) .run(); diff --git a/src/backend/src/prisma/factories/config-data.factory.ts b/src/backend/src/prisma/factories/config-data.factory.ts index 79599de9cf..5f7410a09d 100644 --- a/src/backend/src/prisma/factories/config-data.factory.ts +++ b/src/backend/src/prisma/factories/config-data.factory.ts @@ -1,6 +1,21 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, Work_Package_Stage } from '@prisma/client'; import { connectOrganization, connectUser } from '../utils/common.factory.js'; +type WorkPackageTemplateConfig = { + templateName: string; + wbsElementName: string; + stage: Work_Package_Stage | null; + duration: number | null; +}; + +type ProjectTemplateConfig = { + templateName: string; + projectName: string; + summary: string; + budget: number; + workPackageTemplates: WorkPackageTemplateConfig[]; +}; + const SEED_CREATED_AT = new Date('2024-01-01T00:00:00.000Z'); export const teamTypeCreateInputs = (organizationId: string): Prisma.Team_TypeCreateInput[] => [ @@ -572,3 +587,29 @@ export const eventTypeCreateInput = ( }) } }); + +export const projectTemplateConfigs: ProjectTemplateConfig[] = [ + { + templateName: 'Standard Hardware Project', + projectName: 'Hardware Project', + summary: 'Standard template for hardware projects.', + budget: 0, + workPackageTemplates: [ + { templateName: 'Research Phase', wbsElementName: 'Research', stage: Work_Package_Stage.RESEARCH, duration: null }, + { templateName: 'Design Phase', wbsElementName: 'Design', stage: Work_Package_Stage.DESIGN, duration: null }, + { + templateName: 'Manufacturing Phase', + wbsElementName: 'Manufacturing', + stage: Work_Package_Stage.MANUFACTURING, + duration: null + }, + { templateName: 'Install Phase', wbsElementName: 'Install', stage: Work_Package_Stage.INSTALL, duration: null }, + { templateName: 'Testing Phase', wbsElementName: 'Testing', stage: Work_Package_Stage.TESTING, duration: null }, + { templateName: 'Final Testing', wbsElementName: 'Final Testing', stage: Work_Package_Stage.TESTING, duration: null } + ] + } +]; + +export const standaloneWorkPackageTemplateConfigs: WorkPackageTemplateConfig[] = [ + { templateName: 'Quick Install', wbsElementName: 'Install', stage: null, duration: 1 } +]; diff --git a/src/backend/src/prisma/factories/project.factory.ts b/src/backend/src/prisma/factories/project.factory.ts index c8859d9ec8..517e54c747 100644 --- a/src/backend/src/prisma/factories/project.factory.ts +++ b/src/backend/src/prisma/factories/project.factory.ts @@ -1,8 +1,8 @@ import { Faker } from '@faker-js/faker'; import { Link_Type, Prisma, WBS_Element_Status } from '@prisma/client'; -import dayjs from 'dayjs'; import { addDaysToDate } from 'shared'; import { DateRange } from '../context.js'; +import { clampDate, daysBetween } from '../dates.js'; export const PROJECTS_PER_CAR = 30; @@ -190,14 +190,6 @@ const PROJECT_LINK_URL_BY_TYPE: Record string> 'Google Drive': (projectSlug) => `https://drive.google.com/drive/folders/${projectSlug}` }; -const clampDate = (date: Date, min: Date, max: Date): Date => { - if (date < min) return new Date(min); - if (date > max) return new Date(max); - return date; -}; - -const daysBetween = ({ start, end }: DateRange): number => Math.max(0, dayjs(end).diff(dayjs(start), 'day')); - const TARGET_BUDGET_PER_CAR = 80_000; export const generateProjectBudgets = ( @@ -256,7 +248,7 @@ export const generateProjectTimeline = (faker: Faker, carDateRange: DateRange): }) ); - const end = clampDate(addDaysToDate(start, durationDays), carStart, carEnd); + const end = clampDate(addDaysToDate(start, durationDays), { start: carStart, end: carEnd }); return { start, end }; }; diff --git a/src/backend/src/prisma/factories/work-package.factory.ts b/src/backend/src/prisma/factories/work-package.factory.ts new file mode 100644 index 0000000000..80b0b1e702 --- /dev/null +++ b/src/backend/src/prisma/factories/work-package.factory.ts @@ -0,0 +1,84 @@ +import { Faker } from '@faker-js/faker'; +import { Prisma, WBS_Element_Status, Work_Package_Stage } from '@prisma/client'; +import { DateRange } from '../context.js'; +import { clampDate, daysBetween } from '../dates.js'; +import { addDaysToDate } from 'shared'; + +const DAYS_PER_WEEK = 7; + +export const generateWorkPackageCount = (faker: Faker): number => + // Each project gets 0–8 work packages. Average work package count is around 5. + faker.helpers.weightedArrayElement([ + { weight: 3, value: 0 }, + { weight: 5, value: 1 }, + { weight: 8, value: 2 }, + { weight: 14, value: 3 }, + { weight: 18, value: 4 }, + { weight: 22, value: 5 }, + { weight: 16, value: 6 }, + { weight: 9, value: 7 }, + { weight: 5, value: 8 } + ]); + +export const generateWorkPackageStage = (faker: Faker): Work_Package_Stage => + faker.helpers.arrayElement(Object.values(Work_Package_Stage)); + +export const generateWorkPackageTimeline = (faker: Faker, projectTimeline: DateRange, blockerEndDate?: Date): DateRange => { + const start = + blockerEndDate && blockerEndDate < projectTimeline.end + ? addDaysToDate(blockerEndDate, 1) + : addDaysToDate( + projectTimeline.start, + faker.number.int({ + min: 0, + max: Math.max(0, daysBetween(projectTimeline) - DAYS_PER_WEEK) + }) + ); + + // duration saved in WEEKS instead of days + const maxDuration = Math.max(1, daysBetween({ start, end: projectTimeline.end }) / DAYS_PER_WEEK); + const duration = faker.number.int({ min: 1, max: Math.min(12, maxDuration) }); + + return { start, end: clampDate(addDaysToDate(start, duration * DAYS_PER_WEEK), { start, end: projectTimeline.end }) }; +}; + +export const workPackageCreateInput = ( + organizationId: string, + carNumber: number, + projectNumber: number, + workPackageNumber: number, + projectId: string, + orderInProject: number, + name: string, + startDate: Date, + duration: number, + stage: Work_Package_Stage, + leadId?: string, + managerId?: string, + blockedByWbsElementIds: string[] = [] +): Prisma.Work_PackageCreateInput => ({ + orderInProject, + startDate, + duration, + stage, + project: { connect: { projectId } }, + ...(blockedByWbsElementIds.length > 0 + ? { + blockedBy: { + connect: blockedByWbsElementIds.map((wbsElementId) => ({ wbsElementId })) + } + } + : {}), + wbsElement: { + create: { + name, + carNumber, + projectNumber, + workPackageNumber, + status: WBS_Element_Status.ACTIVE, + organization: { connect: { organizationId } }, + ...(leadId ? { lead: { connect: { userId: leadId } } } : {}), + ...(managerId ? { manager: { connect: { userId: managerId } } } : {}) + } + } +}); diff --git a/src/backend/src/prisma/processes/seed-runner.ts b/src/backend/src/prisma/processes/seed-runner.ts index 25551a1675..913f4744b9 100644 --- a/src/backend/src/prisma/processes/seed-runner.ts +++ b/src/backend/src/prisma/processes/seed-runner.ts @@ -1,5 +1,6 @@ import { PrismaClient } from '@prisma/client'; -import { SeedProcess, GLOBAL_SEED } from './seed-process.js'; +import { SeedProcess } from './seed-process.js'; +import ora from 'ora'; export class SeedRunner { private instances: SeedProcess[] = []; @@ -20,34 +21,47 @@ export class SeedRunner { const outputs = new Map(); const context: Record = {}; + const total = this.instances.length; const mergeOutputs = (target: Record, source: Record, sourceName: string) => { const duplicateKeys = Object.keys(source).filter((key) => key in target); - if (duplicateKeys.length > 0) { throw new Error(`Duplicate seed output keys from ${sourceName}: ${duplicateKeys.join(', ')}`); } - return Object.assign(target, source); }; - for (const instance of this.instances) { + for (let i = 0; i < this.instances.length; i++) { + const instance = this.instances[i]; instance.prisma = this.prisma; + const start = Date.now(); - const depOutputs = instance.dependencies().reduce>((acc, depClass) => { - const output = outputs.get(depClass.name); - if (!output) throw new Error(`Missing output for dependency: ${depClass.name}`); + const spinner = ora({ + text: `[${i + 1}/${total}] ${instance.constructor.name}...`, + color: 'cyan' + }).start(); - return mergeOutputs(acc, output, depClass.name); - }, {}); + try { + const depOutputs = instance.dependencies().reduce>((acc, depClass) => { + const output = outputs.get(depClass.name); + if (!output) throw new Error(`Missing output for dependency: ${depClass.name}`); + return mergeOutputs(acc, output, depClass.name); + }, {}); - console.log(`Running ${instance.constructor.name} (seed ${GLOBAL_SEED})...`); - const output = await instance.run(depOutputs); + const output = await instance.run(depOutputs); - outputs.set(instance.constructor.name, output); - mergeOutputs(context, output, instance.constructor.name); + outputs.set(instance.constructor.name, output); + mergeOutputs(context, output, instance.constructor.name); - console.log(`${instance.constructor.name} complete`); + spinner.succeed( + `[${i + 1}/${total}] ${instance.constructor.name} complete (${((Date.now() - start) / 1000).toFixed(2)}s)` + ); + } catch (e) { + spinner.fail( + `[${i + 1}/${total}] ${instance.constructor.name} failed (${((Date.now() - start) / 1000).toFixed(2)}s)` + ); + throw e; + } } return context; diff --git a/src/backend/src/prisma/seed/config-data.process.ts b/src/backend/src/prisma/seed/config-data.process.ts index 65a40be98a..df7d4fd551 100644 --- a/src/backend/src/prisma/seed/config-data.process.ts +++ b/src/backend/src/prisma/seed/config-data.process.ts @@ -10,7 +10,8 @@ import { Reimbursement_Product_Other_Reason, Team_Type, Unit, - Vendor + Vendor, + WBS_Element_Template } from '@prisma/client'; import { OrganizationOutput, OrganizationProcess } from './organization.process.js'; import { UsersOutput, UsersProcess } from './user.process.js'; @@ -27,10 +28,13 @@ import { materialTypeCreateInputs, otherReimbursementReasonConfigs, otherReimbursementReasonCreateInput, + projectTemplateConfigs, + standaloneWorkPackageTemplateConfigs, teamTypeCreateInputs, unitCreateInputs, vendorCreateInputs } from '../factories/config-data.factory.js'; +import { connectOrganization, connectUser } from '../utils/common.factory.js'; type ConfigDataInput = OrganizationOutput & UsersOutput; @@ -47,6 +51,8 @@ export type ConfigDataOutput = { reimbursementProductOtherReasons: Reimbursement_Product_Other_Reason[]; calendars: Calendar[]; eventTypes: Event_Type[]; + projectTemplates: WBS_Element_Template[]; + standaloneWpTemplates: WBS_Element_Template[]; }; export class ConfigDataProcess extends SeedProcess { @@ -153,6 +159,64 @@ export class ConfigDataProcess extends SeedProcess { + const wbsElementTemplate = await this.prisma.wBS_Element_Template.create({ + data: { + templateName: config.templateName, + templateNotes: `Seed template for ${config.projectName}.`, + wbsElementName: config.projectName, + userCreated: connectUser(creator.userId), + organization: connectOrganization(organizationId), + projectTemplate: { + create: { + summary: config.summary, + budget: config.budget, + workPackageTemplates: { + create: await Promise.all( + config.workPackageTemplates.map(async (wpConfig) => ({ + wbsElementTemplate: { + create: { + templateName: wpConfig.templateName, + templateNotes: `Seed template for ${wpConfig.wbsElementName}.`, + wbsElementName: wpConfig.wbsElementName, + userCreated: connectUser(creator.userId), + organization: connectOrganization(organizationId) + } + }, + stage: wpConfig.stage, + duration: wpConfig.duration + })) + ) + } + } + } + } + }); + return wbsElementTemplate; + }) + ); + + const standaloneWpTemplates = await Promise.all( + standaloneWorkPackageTemplateConfigs.map((config) => + this.prisma.wBS_Element_Template.create({ + data: { + templateName: config.templateName, + templateNotes: `Seed template for ${config.wbsElementName}.`, + wbsElementName: config.wbsElementName, + userCreated: connectUser(creator.userId), + organization: connectOrganization(organizationId), + workPackageTemplate: { + create: { + stage: config.stage, + duration: config.duration + } + } + } + }) + ) + ); + return { teamTypes, linkTypes, @@ -165,7 +229,9 @@ export class ConfigDataProcess extends SeedProcess; +}; + +const BLOCKED_PERCENTAGE = 0.3; + +export class WorkPackageProcess extends SeedProcess { + dependencies() { + return [OrganizationProcess, UsersProcess, ProjectProcess]; + } + + async run({ organization, projects, leadership, heads, admins, appAdmins }: WorkPackageInput): Promise { + const { organizationId } = organization; + const projectOwners = [...leadership, ...heads, ...admins, ...appAdmins]; + + const allWorkPackageContexts = await Promise.all( + projects.map((projectContext) => this.generateWorkPackagesForProject(organizationId, projectContext, projectOwners)) + ); + + const workPackages = allWorkPackageContexts.flat(); + + const workPackagesByProjectId = workPackages.reduce>((acc, wpContext) => { + const { projectId } = wpContext.workPackage; + acc[projectId] ??= []; + acc[projectId].push(wpContext); + return acc; + }, {}); + + return { workPackages, workPackagesByProjectId }; + } + + private async generateWorkPackagesForProject( + organizationId: string, + { project, timeline }: ProjectContext, + projectOwners: FullUser[] + ): Promise { + const count = generateWorkPackageCount(this.faker); + if (count === 0) return []; + + const { carNumber, projectNumber } = project.wbsElement; + const workPackageContexts: WorkPackageContext[] = []; + + for (let i = 0; i < count; i++) { + // for clarity + const orderInProject = i + 1; + const workPackageNumber = i + 1; + + // determine if this wp is blocked by a previous one + const shouldBeBlocked = i > 0 && this.faker.datatype.boolean({ probability: BLOCKED_PERCENTAGE }); + const blocker = shouldBeBlocked + ? workPackageContexts[this.faker.number.int({ min: 0, max: workPackageContexts.length - 1 })] + : undefined; + + // if the blocker ends too close to the project end, drop the blocking relationship + // this would skew the data with 5-10% of blocking relationships being affected in the worst case scenario + // this happening is extremely rare but does change the data slightly if it does occur. + const effectiveBlocker = + blocker && daysBetween({ start: blocker.timeline.end, end: timeline.end }) >= DAYS_PER_WEEK ? blocker : undefined; + + const wpTimeline = generateWorkPackageTimeline(this.faker, timeline, effectiveBlocker?.timeline.end); + + // pick lead and manager + const lead = this.faker.helpers.arrayElement(projectOwners); + const managerPool = projectOwners.filter((u) => u.userId !== lead.userId); + const manager = this.faker.helpers.arrayElement(managerPool.length > 0 ? managerPool : projectOwners); + const stage = generateWorkPackageStage(this.faker); + const name = `${project.wbsElement.name} WP${workPackageNumber}`; + + const blockedByWbsElementIds = effectiveBlocker ? [effectiveBlocker.workPackage.wbsElement.wbsElementId] : []; + + const workPackage = await this.prisma.work_Package.create({ + data: workPackageCreateInput( + organizationId, + carNumber, + projectNumber, + workPackageNumber, + project.projectId, + orderInProject, + name, + wpTimeline.start, + Math.ceil((wpTimeline.end.getTime() - wpTimeline.start.getTime()) / WEEK_MS), + stage, + lead.userId, + manager.userId, + blockedByWbsElementIds + ), + include: { wbsElement: true } + }); + + workPackageContexts.push({ workPackage, timeline: wpTimeline }); + } + + return workPackageContexts; + } +} diff --git a/yarn.lock b/yarn.lock index 3527f9fa4f..57eb4f6065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9900,7 +9900,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.0.1, chalk@npm:^5.2.0": +"chalk@npm:^5.0.1, chalk@npm:^5.2.0, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 @@ -10139,6 +10139,22 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + +"cli-spinners@npm:^3.2.0": + version: 3.4.0 + resolution: "cli-spinners@npm:3.4.0" + checksum: 10/6a4021c1999011fc34ae714f055dcdafb56309abc1f8fb021ea7d9370dfc524485fe8684226015e5fe6053dd30544e74270184ff7edc3fa4d37043b8efd0a054 + languageName: node + linkType: hard + "cli-table3@npm:^0.6.3": version: 0.6.5 resolution: "cli-table3@npm:0.6.5" @@ -13891,6 +13907,7 @@ __metadata: eslint-plugin-cypress: "npm:4.3.0" eslint-plugin-prettier: "npm:^5.2.2" mitt: "npm:^3.0.1" + ora: "npm:^9.4.1" prettier: "npm:^3.4.2" react-hook-form-persist: "npm:^3.0.0" recharts: "npm:^2.15.3" @@ -14341,6 +14358,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.5.0": + version: 1.6.0 + resolution: "get-east-asian-width@npm:1.6.0" + checksum: 10/3e5370b5df1f0020db711d8a3f9ee2cbfc9c7542daa99a699e9d7b9acf66e7868b89084741565a45d30d80afedf6e1218e0fb8bef7a583924a449c2816777380 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" @@ -15827,6 +15851,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-map@npm:^2.0.2, is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" @@ -16037,6 +16068,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "is-weakmap@npm:^2.0.2": version: 2.0.2 resolution: "is-weakmap@npm:2.0.2" @@ -17498,6 +17536,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: "npm:^2.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + languageName: node + linkType: hard + "longest-streak@npm:^3.0.0": version: 3.1.0 resolution: "longest-streak@npm:3.1.0" @@ -18669,6 +18717,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -19417,6 +19472,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: "npm:^5.0.0" + checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + "open@npm:^10.0.3": version: 10.2.0 resolution: "open@npm:10.2.0" @@ -19463,6 +19527,22 @@ __metadata: languageName: node linkType: hard +"ora@npm:^9.4.1": + version: 9.4.1 + resolution: "ora@npm:9.4.1" + dependencies: + chalk: "npm:^5.6.2" + cli-cursor: "npm:^5.0.0" + cli-spinners: "npm:^3.2.0" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.1.0" + log-symbols: "npm:^7.0.1" + stdin-discarder: "npm:^0.3.2" + string-width: "npm:^8.1.0" + checksum: 10/58b0b6c6ee20b30806e2e0bbf84f81b7517e21c9b721d0b7f186e12b9ef2e0be190174bf53085123efae6c8d120dcd9788708602e78e91188b9e12ff71abc275 + languageName: node + linkType: hard + "outvariant@npm:^1.4.0, outvariant@npm:^1.4.3": version: 1.4.3 resolution: "outvariant@npm:1.4.3" @@ -23220,6 +23300,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: "npm:^7.0.0" + signal-exit: "npm:^4.1.0" + checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c + languageName: node + linkType: hard + "retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -24337,6 +24427,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.3.2": + version: 0.3.2 + resolution: "stdin-discarder@npm:0.3.2" + checksum: 10/63c6912146efe079fd048ecc02e5c3bf5aaa4cb268ad4e365603d845444dd3048daa45868c2690c5fe2d020ba47273c8a20df684a8c424fb4bd7f359c795c2f5 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -24417,6 +24514,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.1.0": + version: 8.2.1 + resolution: "string-width@npm:8.2.1" + dependencies: + get-east-asian-width: "npm:^1.5.0" + strip-ansi: "npm:^7.1.2" + checksum: 10/cfadcc454b357d1a2ef88afb85068c7605900c9920362a16df9b4c320cf411983cee51b9832b70772d138674c2851d506f39c7e669c961a1cdd1258207580805 + languageName: node + linkType: hard + "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -24545,7 +24652,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.2": version: 7.2.0 resolution: "strip-ansi@npm:7.2.0" dependencies: @@ -27488,6 +27595,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors@npm:^2.1.1": + version: 2.1.2 + resolution: "yoctocolors@npm:2.1.2" + checksum: 10/6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 + languageName: node + linkType: hard + "yup@npm:^1.6.1": version: 1.7.1 resolution: "yup@npm:1.7.1"