diff --git a/.changeset/great-spies-obey.md b/.changeset/great-spies-obey.md new file mode 100644 index 0000000..1e44516 --- /dev/null +++ b/.changeset/great-spies-obey.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Make the dry run mode work for the "up" command using the "--dry" CLI option diff --git a/.changeset/loud-comics-obey.md b/.changeset/loud-comics-obey.md new file mode 100644 index 0000000..3aa7c50 --- /dev/null +++ b/.changeset/loud-comics-obey.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Improve the default reporter with good looking output that has colors and animations. In non-interactive environments the animations are not used (this includes CI environments). diff --git a/.changeset/many-items-kneel.md b/.changeset/many-items-kneel.md new file mode 100644 index 0000000..909fae5 --- /dev/null +++ b/.changeset/many-items-kneel.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Improve the "up" command flow and the usage of reporters, handle migration errors and automatic skipping of migrations. diff --git a/package.json b/package.json index a732154..5a89089 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "space": true, "prettier": true, "rules": { - "complexity": 0 + "complexity": 0, + "capitalized-comments": 0 }, "overrides": [ { diff --git a/packages/cli/package.json b/packages/cli/package.json index d433a5d..6c25a69 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,13 @@ "license": "MIT", "dependencies": { "@emigrate/plugin-tools": "workspace:*", - "cosmiconfig": "8.3.6" + "ansis": "2.0.2", + "cosmiconfig": "8.3.6", + "elegant-spinner": "3.0.0", + "figures": "6.0.1", + "is-interactive": "2.0.0", + "log-update": "6.0.0", + "pretty-ms": "8.0.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f5a6627..cf3efb1 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -213,6 +213,4 @@ try { } else { console.error(error); } - - process.exit(1); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f780588..921446e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,5 @@ export * from './types.js'; export const emigrate = () => { - console.log('Done!'); + // console.log('Done!'); }; diff --git a/packages/cli/src/new-command.ts b/packages/cli/src/new-command.ts index 6b26088..137b481 100644 --- a/packages/cli/src/new-command.ts +++ b/packages/cli/src/new-command.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin } from '@emigrate/plugin-tools'; import { BadOptionError, MissingArgumentsError, MissingOptionError, UnexpectedError } from './errors.js'; import { type Config } from './types.js'; -import { stripLeadingPeriod } from './strip-leading-period.js'; +import { withLeadingPeriod } from './with-leading-period.js'; export default async function newCommand({ directory, template, plugins = [], extension }: Config, name: string) { if (!directory) { @@ -34,7 +34,7 @@ export default async function newCommand({ directory, template, plugins = [], ex throw new UnexpectedError(`Failed to read template file: ${templatePath}`, { cause: error }); } - filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.${stripLeadingPeriod( + filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.${withLeadingPeriod( extension ?? fileExtension, )}`; } @@ -56,7 +56,7 @@ export default async function newCommand({ directory, template, plugins = [], ex if (extension && !hasGeneratedFile) { content = ''; - filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.${stripLeadingPeriod(extension)}`; + filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.${withLeadingPeriod(extension)}`; } if (!filename || content === undefined) { diff --git a/packages/cli/src/plugin-reporter-default.ts b/packages/cli/src/plugin-reporter-default.ts index b56b863..844d843 100644 --- a/packages/cli/src/plugin-reporter-default.ts +++ b/packages/cli/src/plugin-reporter-default.ts @@ -1,39 +1,344 @@ -import { type ReporterPlugin } from '@emigrate/plugin-tools/types'; +import path from 'node:path'; +import ansis from 'ansis'; +import logUpdate from 'log-update'; +import elegantSpinner from 'elegant-spinner'; +import figures from 'figures'; +import isInteractive from 'is-interactive'; +import prettyMs from 'pretty-ms'; +import { + type MigrationMetadata, + type MigrationMetadataFinished, + type ReporterPlugin, +} from '@emigrate/plugin-tools/types'; -const reporterDefault: ReporterPlugin = { - onInit({ dry, directory }) { - console.log(`Running migrations in: ${directory}${dry ? ' (dry run)' : ''}`); - }, - onCollectedMigrations(migrations) { - console.log(`Found ${migrations.length} pending migrations`); - }, - onLockedMigrations(migrations) { - console.log(`Locked ${migrations.length} migrations`); - }, - onMigrationStart(migration) { - console.log(`- ${migration.relativeFilePath} (running)`); - }, - onMigrationSuccess(migration) { - console.log(`- ${migration.relativeFilePath} (success) [${migration.duration}ms]`); - }, - onMigrationError(migration, error) { - console.error(`- ${migration.relativeFilePath} (failed!) [${migration.duration}ms]`); - console.error(error.cause ?? error); - }, - onMigrationSkip(migration) { - console.log(`- ${migration.relativeFilePath} (skipped)`); - }, - onFinished(migrations, error) { - const totalDuration = migrations.reduce((total, migration) => total + migration.duration, 0); +type Status = ReturnType; - if (error) { - console.error('Failed to run migrations! [total duration: %dms]', totalDuration); - console.error(error.cause ?? error); +const interactive = isInteractive(); +const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall; + +const formatDuration = (duration: number): string => { + const pretty = prettyMs(duration); + + return ansis.yellow(pretty.replaceAll(/([^\s\d]+)/g, ansis.dim('$1'))); +}; + +const getTitle = ({ directory, dry, cwd }: { directory: string; dry: boolean; cwd: string }) => { + return `${ansis.bgBlueBright(ansis.black(' Emigrate '))} ${ansis.gray(cwd + path.sep)}${directory}${ + dry ? ansis.yellow(' (dry run)') : '' + }`; +}; + +const getMigrationStatus = ( + migration: MigrationMetadata | MigrationMetadataFinished, + activeMigration?: MigrationMetadata, +) => { + if ('status' in migration) { + return migration.status; + } + + return migration.name === activeMigration?.name ? 'running' : 'pending'; +}; + +const getIcon = (status: Status) => { + switch (status) { + case 'running': { + return ansis.cyan(spinner()); + } + + case 'pending': { + return ansis.gray(figures.pointerSmall); + } + + case 'done': { + return ansis.green(figures.tick); + } + + case 'failed': { + return ansis.red(figures.cross); + } + + case 'skipped': { + return ansis.yellow(figures.circle); + } + + default: { + return ' '; + } + } +}; + +const getName = (name: string, status?: Status) => { + switch (status) { + case 'failed': { + return ansis.red(name); + } + + case 'skipped': { + return ansis.yellow(name); + } + + default: { + return name; + } + } +}; + +const getMigrationText = ( + migration: MigrationMetadata | MigrationMetadataFinished, + activeMigration?: MigrationMetadata, +) => { + const nameWithoutExtension = migration.name.slice(0, -migration.extension.length); + const status = getMigrationStatus(migration, activeMigration); + const parts = [' ', getIcon(status)]; + + parts.push(`${getName(nameWithoutExtension, status)}${ansis.dim(migration.extension)}`); + + if ('status' in migration) { + parts.push(ansis.gray(`(${migration.status})`)); + } else if (migration.name === activeMigration?.name) { + parts.push(ansis.gray('(running)')); + } + + if ('duration' in migration && migration.duration) { + parts.push(formatDuration(migration.duration)); + } + + return parts.join(' '); +}; + +const getError = (error?: Error, indent = ' ') => { + if (!error) { + return ''; + } + + let errorTitle: string; + let stack: string[] = []; + + if (error.stack) { + // @ts-expect-error error won't be undefined here + [errorTitle, ...stack] = error.stack.split('\n'); + } else if (error.name) { + errorTitle = `${error.name}: ${error.message}`; + } else { + errorTitle = error.message; + } + + const parts = [`${indent}${ansis.bold.red(errorTitle)}`, ...stack.map((line) => `${indent}${ansis.dim(line)}`)]; + + if (error.cause instanceof Error) { + const nextIndent = `${indent} `; + parts.push(`\n${nextIndent}${ansis.bold('Original error cause:')}\n`, getError(error.cause, nextIndent)); + } + + return parts.join('\n'); +}; + +const getSummary = (migrations: Array = []) => { + const total = migrations.length; + let done = 0; + let failed = 0; + let skipped = 0; + + for (const migration of migrations) { + const status = getMigrationStatus(migration); + switch (status) { + case 'done': { + done++; + break; + } + + case 'failed': { + failed++; + break; + } + + case 'skipped': { + skipped++; + break; + } + + default: { + break; + } + } + } + + const statusLine = [ + failed ? ansis.bold.red(`${failed} failed`) : '', + done ? ansis.bold.green(`${done} done`) : '', + skipped ? ansis.bold.yellow(`${skipped} skipped`) : '', + ] + .filter(Boolean) + .join(ansis.dim(' | ')); + + if (!statusLine) { + return ''; + } + + return ` ${statusLine}${ansis.gray(` (${total} total)`)}`; +}; + +const getHeaderMessage = (migrations?: MigrationMetadata[], lockedMigrations?: MigrationMetadata[]) => { + if (!migrations || !lockedMigrations) { + return ''; + } + + if (migrations.length === 0) { + return ' No pending migrations found'; + } + + if (migrations.length === lockedMigrations.length) { + return ` ${ansis.bold(migrations.length.toString())} ${ansis.dim('pending migrations to run')}`; + } + + if (lockedMigrations.length === 0) { + return ` ${ansis.bold(`0 of ${migrations.length}`)} ${ansis.dim('pending migrations to run')} ${ansis.redBright( + '(all locked)', + )}`; + } + + return ` ${ansis.bold(`${lockedMigrations.length} of ${migrations.length}`)} ${ansis.dim( + 'pending migrations to run', + )} ${ansis.yellow(`(${migrations.length - lockedMigrations.length} locked)`)}`; +}; + +class DefaultFancyReporter implements Required { + #migrations: Array | undefined; + #lockedMigrations: MigrationMetadata[] | undefined; + #activeMigration: MigrationMetadata | undefined; + #error: Error | undefined; + #directory!: string; + #cwd!: string; + #dry!: boolean; + #interval: NodeJS.Timeout | undefined; + + onInit(parameters: { directory: string; cwd: string; dry: boolean }): void | PromiseLike { + this.#directory = parameters.directory; + this.#dry = parameters.dry; + this.#cwd = parameters.cwd; + + this.#start(); + } + + onCollectedMigrations(migrations: MigrationMetadata[]): void | PromiseLike { + this.#migrations = migrations; + } + + onLockedMigrations(migrations: MigrationMetadata[]): void | PromiseLike { + this.#lockedMigrations = migrations; + } + + onMigrationStart(migration: MigrationMetadata): void | PromiseLike { + this.#activeMigration = migration; + } + + onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike { + this.#finishMigration(migration); + } + + onMigrationError(migration: MigrationMetadataFinished, _error: Error): void | PromiseLike { + this.#finishMigration(migration); + } + + onMigrationSkip(migration: MigrationMetadataFinished): void | PromiseLike { + this.#finishMigration(migration); + } + + onFinished(_migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike { + this.#error = error; + this.#activeMigration = undefined; + this.#stop(); + } + + #finishMigration(migration: MigrationMetadataFinished): void { + if (!this.#migrations) { return; } - console.log(`Successfully ran ${migrations.length} migrations! [total duration: ${totalDuration}ms]`); - }, -}; + const index = this.#migrations.findIndex((m) => m.name === migration.name); + + if (index !== -1) { + this.#migrations[index] = migration; + } + } + + #render(): void { + const parts = [ + getTitle({ directory: this.#directory, dry: this.#dry, cwd: this.#cwd }), + getHeaderMessage(this.#migrations, this.#lockedMigrations), + this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '', + getSummary(this.#migrations), + getError(this.#error), + ]; + logUpdate('\n' + parts.filter(Boolean).join('\n\n') + '\n'); + } + + #start(): void { + this.#render(); + this.#interval = setInterval(() => { + this.#render(); + }, 80).unref(); + } + + #stop(): void { + if (this.#interval) { + clearInterval(this.#interval); + this.#interval = undefined; + } + + this.#render(); + } +} + +class DefaultReporter implements Required { + #migrations?: MigrationMetadata[]; + #lockedMigrations?: MigrationMetadata[]; + + onInit(parameters: { directory: string; cwd: string; dry: boolean }): void | PromiseLike { + console.log(''); + console.log(getTitle(parameters)); + console.log(''); + } + + onCollectedMigrations(migrations: MigrationMetadata[]): void | PromiseLike { + this.#migrations = migrations; + } + + onLockedMigrations(migrations: MigrationMetadata[]): void | PromiseLike { + this.#lockedMigrations = migrations; + + console.log(getHeaderMessage(this.#migrations, this.#lockedMigrations)); + console.log(''); + } + + onMigrationStart(migration: MigrationMetadata): void | PromiseLike { + console.log(getMigrationText(migration, migration)); + } + + onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike { + console.log(getMigrationText(migration)); + } + + onMigrationError(migration: MigrationMetadataFinished, _error: Error): void | PromiseLike { + console.error(getMigrationText(migration)); + } + + onMigrationSkip(migration: MigrationMetadataFinished): void | PromiseLike { + console.log(getMigrationText(migration)); + } + + onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike { + console.log(''); + console.log(getSummary(migrations)); + console.log(''); + + if (error) { + console.error(getError(error)); + console.log(''); + } + } +} + +const reporterDefault = interactive ? new DefaultFancyReporter() : new DefaultReporter(); export default reporterDefault; diff --git a/packages/cli/src/strip-leading-period.ts b/packages/cli/src/strip-leading-period.ts deleted file mode 100644 index 33073a0..0000000 --- a/packages/cli/src/strip-leading-period.ts +++ /dev/null @@ -1 +0,0 @@ -export const stripLeadingPeriod = (string: string) => string.replace(/^\./, ''); diff --git a/packages/cli/src/up-command.ts b/packages/cli/src/up-command.ts index 8cb996e..04c6b8d 100644 --- a/packages/cli/src/up-command.ts +++ b/packages/cli/src/up-command.ts @@ -18,7 +18,7 @@ import { MissingOptionError, } from './errors.js'; import { type Config } from './types.js'; -import { stripLeadingPeriod } from './strip-leading-period.js'; +import { withLeadingPeriod } from './with-leading-period.js'; import pluginLoaderJs from './plugin-loader-js.js'; import pluginReporterDefault from './plugin-reporter-default.js'; @@ -76,7 +76,7 @@ export default async function upCommand({ directory, dry = false, plugins = [] } name, filePath, relativeFilePath: path.relative(cwd, filePath), - extension: stripLeadingPeriod(path.extname(name)), + extension: withLeadingPeriod(path.extname(name)), directory, cwd, }; @@ -108,7 +108,7 @@ export default async function upCommand({ directory, dry = false, plugins = [] } [ extension, loaderPlugins.find((plugin) => - plugin.loadableExtensions.some((loadableExtension) => stripLeadingPeriod(loadableExtension) === extension), + plugin.loadableExtensions.some((loadableExtension) => withLeadingPeriod(loadableExtension) === extension), ), ] as const, ), @@ -123,33 +123,56 @@ export default async function upCommand({ directory, dry = false, plugins = [] } await reporter.onCollectedMigrations?.(migrationFiles); if (migrationFiles.length === 0 || dry || migrationHistoryError) { - await reporter.onLockedMigrations?.([]); + await reporter.onLockedMigrations?.(migrationFiles); - for await (const migration of migrationFiles) { + const finishedMigrations: MigrationMetadataFinished[] = migrationFiles.map((migration) => ({ + ...migration, + duration: 0, + status: 'skipped', + })); + + for await (const migration of finishedMigrations) { await reporter.onMigrationSkip?.(migration); } - await reporter.onFinished?.( - migrationFiles.map((migration) => ({ ...migration, status: 'skipped', duration: 0 })), - migrationHistoryError, - ); + await reporter.onFinished?.(finishedMigrations, migrationHistoryError); return; } - const lockedMigrationFiles = await storage.lock(migrationFiles); + let lockedMigrationFiles: MigrationMetadata[] = []; - let cleaningUp = false; + try { + lockedMigrationFiles = await storage.lock(migrationFiles); + + await reporter.onLockedMigrations?.(lockedMigrationFiles); + } catch (error) { + for await (const migration of migrationFiles) { + await reporter.onMigrationSkip?.({ ...migration, duration: 0, status: 'skipped' }); + } + + await reporter.onFinished?.([], error instanceof Error ? error : new Error(String(error))); + return; + } + + const nonLockedMigrations = migrationFiles.filter((migration) => !lockedMigrationFiles.includes(migration)); + + for await (const migration of nonLockedMigrations) { + await reporter.onMigrationSkip?.({ ...migration, duration: 0, status: 'skipped' }); + } + + let cleaningUp: Promise | undefined; const cleanup = async () => { if (cleaningUp) { - return; + return cleaningUp; } process.off('SIGINT', cleanup); process.off('SIGTERM', cleanup); - cleaningUp = true; - await storage.unlock(lockedMigrationFiles); + cleaningUp = storage.unlock(lockedMigrationFiles); + + return cleaningUp; }; process.on('SIGINT', cleanup); @@ -162,8 +185,9 @@ export default async function upCommand({ directory, dry = false, plugins = [] } const lastMigrationStatus = finishedMigrations.at(-1)?.status; if (lastMigrationStatus === 'failed' || lastMigrationStatus === 'skipped') { - await reporter.onMigrationSkip?.(migration); - finishedMigrations.push({ ...migration, status: 'skipped', duration: 0 }); + const finishedMigration: MigrationMetadataFinished = { ...migration, status: 'skipped', duration: 0 }; + await reporter.onMigrationSkip?.(finishedMigration); + finishedMigrations.push(finishedMigration); continue; } @@ -204,7 +228,7 @@ export default async function upCommand({ directory, dry = false, plugins = [] } const duration = getDuration(start); const finishedMigration: MigrationMetadataFinished = { ...migration, - status: 'done', + status: 'failed', duration, error: errorInstance, }; @@ -218,7 +242,7 @@ export default async function upCommand({ directory, dry = false, plugins = [] } } finally { const firstError = finishedMigrations.find((migration) => migration.status === 'failed')?.error; - await reporter.onFinished?.(finishedMigrations, firstError); await cleanup(); + await reporter.onFinished?.(finishedMigrations, firstError); } } diff --git a/packages/cli/src/with-leading-period.ts b/packages/cli/src/with-leading-period.ts new file mode 100644 index 0000000..7bc6340 --- /dev/null +++ b/packages/cli/src/with-leading-period.ts @@ -0,0 +1 @@ +export const withLeadingPeriod = (string: string) => (string.startsWith('.') ? string : `.${string}`); diff --git a/packages/plugin-tools/src/types.ts b/packages/plugin-tools/src/types.ts index ff166f1..3465bd1 100644 --- a/packages/plugin-tools/src/types.ts +++ b/packages/plugin-tools/src/types.ts @@ -121,9 +121,9 @@ export type MigrationMetadata = { */ cwd: string; /** - * The extension of the migration file + * The extension of the migration file, with a leading period * - * @example js + * @example .js */ extension: string; }; @@ -207,7 +207,7 @@ export type ReporterPlugin = Partial<{ * * @param migration Information about the migration that was skipped. */ - onMigrationSkip(migration: MigrationMetadata): Awaitable; + onMigrationSkip(migration: MigrationMetadataFinished): Awaitable; /** * Called when the migration process has finished. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faad354..7dbde51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,27 @@ importers: '@emigrate/plugin-tools': specifier: workspace:* version: link:../plugin-tools + ansis: + specifier: 2.0.2 + version: 2.0.2 cosmiconfig: specifier: 8.3.6 version: 8.3.6(typescript@5.2.2) + elegant-spinner: + specifier: 3.0.0 + version: 3.0.0 + figures: + specifier: 6.0.1 + version: 6.0.1 + is-interactive: + specifier: 2.0.0 + version: 2.0.0 + log-update: + specifier: 6.0.0 + version: 6.0.0 + pretty-ms: + specifier: 8.0.0 + version: 8.0.0 devDependencies: '@emigrate/tsconfig': specifier: workspace:* @@ -1370,6 +1388,13 @@ packages: type-fest: 1.4.0 dev: false + /ansi-escapes@6.2.0: + resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 3.13.1 + dev: false + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1404,6 +1429,11 @@ packages: engines: {node: '>=12'} dev: false + /ansis@2.0.2: + resolution: {integrity: sha512-D64onic45SdssSfEKNJOybZhDSE9BFryY6LX1CvRzuChBC7SJGJ+VZzgiWrXTEiOK3f/MA7wyFbu34/PvhVDsQ==} + engines: {node: '>=12.13'} + dev: false + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -2070,6 +2100,15 @@ packages: resolution: {integrity: sha512-V0ZhSu1BQZKfG0yNEL6Dadzik8E1vAzfpVOapdSiT9F6yapEJ3Bk+4tZ4SMPdWiUchCgnM/ByYtBzp5ntzDMIA==} dev: false + /elegant-spinner@3.0.0: + resolution: {integrity: sha512-nWUuor3FWTGYAch7SY0unb5qLzs7eAc24ic9PBh+eQctFNQ4IDWJqBpBgsL4SrrGHHN0mJoL7CpWZby5t2KjFg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: false @@ -2744,6 +2783,13 @@ packages: reusify: 1.0.4 dev: false + /figures@6.0.1: + resolution: {integrity: sha512-0oY/olScYD4IhQ8u//gCPA4F3mlTn2dacYmiDm/mbDQvpmLjV4uH+zhsQ5IyXRyvqkvtUkXkNdGvg5OFJTCsuQ==} + engines: {node: '>=18'} + dependencies: + is-unicode-supported: 2.0.0 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2890,6 +2936,11 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: false + /get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: false @@ -3352,6 +3403,13 @@ packages: engines: {node: '>=12'} dev: false + /is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + dependencies: + get-east-asian-width: 1.2.0 + dev: false + /is-get-set-prop@1.0.0: resolution: {integrity: sha512-DvAYZ1ZgGUz4lzxKMPYlt08qAUqyG9ckSg2pIjfvcQ7+pkVNUHk8yVLXOnCLe5WKXhLop8oorWFBJHpwWQpszQ==} dependencies: @@ -3374,6 +3432,11 @@ packages: is-docker: 3.0.0 dev: false + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + /is-js-type@2.0.0: resolution: {integrity: sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw==} dependencies: @@ -3509,6 +3572,11 @@ packages: engines: {node: '>=10'} dev: false + /is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + dev: false + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -3850,6 +3918,17 @@ packages: wrap-ansi: 8.1.0 dev: false + /log-update@6.0.0: + resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==} + engines: {node: '>=18'} + dependencies: + ansi-escapes: 6.2.0 + cli-cursor: 4.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -4341,6 +4420,11 @@ packages: lines-and-columns: 1.2.4 dev: false + /parse-ms@3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} + engines: {node: '>=12'} + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4532,6 +4616,13 @@ packages: react-is: 18.2.0 dev: false + /pretty-ms@8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} + engines: {node: '>=14.16'} + dependencies: + parse-ms: 3.0.0 + dev: false + /proto-props@2.0.0: resolution: {integrity: sha512-2yma2tog9VaRZY2mn3Wq51uiSW4NcPYT1cQdBagwyrznrilKSZwIZ0UG3ZPL/mx+axEns0hE35T5ufOYZXEnBQ==} engines: {node: '>=4'} @@ -4941,6 +5032,14 @@ packages: is-fullwidth-code-point: 4.0.0 dev: false + /slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + dev: false + /smartwrap@2.0.2: resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==} engines: {node: '>=6'} @@ -5052,6 +5151,15 @@ packages: strip-ansi: 7.1.0 dev: false + /string-width@7.0.0: + resolution: {integrity: sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + dev: false + /string.prototype.padend@3.1.5: resolution: {integrity: sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==} engines: {node: '>= 0.4'} @@ -5466,6 +5574,11 @@ packages: engines: {node: '>=12.20'} dev: false + /type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + dev: false + /typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} @@ -5858,6 +5971,15 @@ packages: strip-ansi: 7.1.0 dev: false + /wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + dependencies: + ansi-styles: 6.2.1 + string-width: 7.0.0 + strip-ansi: 7.1.0 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false