diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 00000000..349bd661 --- /dev/null +++ b/.github/workflows/changelog-check.yml @@ -0,0 +1,86 @@ +name: Check Changelog + +on: + workflow_call: + inputs: + base-branch: + required: false + type: string + head-ref: + required: true + type: string + labels: + description: 'JSON string of PR labels' + required: true + type: string + repo: + description: 'The repository to check' + required: true + type: string + secrets: + gh-token: + required: true + +jobs: + check-changelog: + runs-on: ubuntu-latest + steps: + - name: Check PR Labels + id: label-check + env: + PR_LABELS: ${{ inputs.labels }} + run: | + if echo "$PR_LABELS" | jq -e '.[] | select(.name == "no-changelog")' > /dev/null; then + echo "no-changelog label found, skipping changelog check." + echo "skip_check=true" >> "$GITHUB_OUTPUT" + else + echo "No no-changelog label found, proceeding with check." + echo "skip_check=false" >> "$GITHUB_OUTPUT" + fi + shell: bash + + - name: Check out target repository + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + uses: actions/checkout@v4 + with: + repository: ${{ inputs.repo }} + ref: ${{ inputs.head-ref }} + path: target-repo + fetch-depth: 0 + + - name: Checkout github-tools repository + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + uses: actions/checkout@v4 + with: + repository: MetaMask/github-tools + ref: main + path: github-tools + + - name: Enable Corepack + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + run: corepack enable + shell: bash + + - name: Set up Node.js + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + uses: actions/setup-node@v4 + with: + node-version-file: ./github-tools/.nvmrc + cache-dependency-path: ./github-tools/yarn.lock + cache: yarn + + - name: Install dependencies + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + run: yarn --immutable + shell: bash + working-directory: ./github-tools + + - name: Check Changelog + if: ${{ steps.label-check.outputs.skip_check != 'true' }} + id: changelog-check + shell: bash + working-directory: ./github-tools + env: + BASE_BRANCH: ${{ inputs.base-branch || 'main' }} + run: | + yarn run changelog:check ../target-repo "$BASE_BRANCH" diff --git a/package.json b/package.json index 5b77bc9c..575c226f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "description": "Tools for interacting with the GitHub API to do metrics gathering", "scripts": { + "changelog:check": "ts-node src/changelog-check.ts", "count-references-to-contributor-docs": "ts-node --swc src/scripts/count-references-to-contributor-docs/cli.ts", "gen:commits": "node .github/scripts/generate-rc-commits.mjs", "get-review-metrics": "ts-node src/get-review-metrics.ts", @@ -20,6 +21,7 @@ "update-release-sheet": "node .github/scripts/update-release-sheet.mjs" }, "dependencies": { + "@metamask/auto-changelog": "^3.4.4", "@metamask/utils": "^7.1.0", "@octokit/graphql": "^7.0.1", "@octokit/request": "^8.1.1", @@ -28,6 +30,7 @@ "@types/luxon": "^3.3.0", "axios": "^0.24.0", "csv-parse": "5.6.0", + "execa": "^9.5.2", "googleapis": "144.0.0", "luxon": "^3.3.0", "ora": "^5.4.1", diff --git a/src/changelog-check.ts b/src/changelog-check.ts new file mode 100644 index 00000000..715d3267 --- /dev/null +++ b/src/changelog-check.ts @@ -0,0 +1,259 @@ +import { parseChangelog } from '@metamask/auto-changelog'; +import { execa } from 'execa'; +import fs from 'fs/promises'; +import path from 'path'; + +type PackageJson = { + workspaces: string[]; +}; + +/** + * Gets the workspace patterns from package.json. + * + * @param repoPath - The path to the repository. + * @returns Array of workspace patterns. + */ +async function getWorkspacePatterns(repoPath: string): Promise { + const packageJsonPath = path.join(repoPath, 'package.json'); + const content = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(content) as PackageJson; + + if (!Array.isArray(packageJson.workspaces)) { + return []; + } + + return packageJson.workspaces; +} + +/** + * This function gets the workspace base and package name from the file path. + * + * @param filePath - The path to the file. + * @param workspacePatterns - The workspace patterns. + * @returns An object containing the base directory and package name, or null if no match is found. + */ +function getPackageInfo( + filePath: string, + workspacePatterns: string[], +): { base: string; package: string } | null { + for (const pattern of workspacePatterns) { + // Extract the base directory (everything before the *) + const wildcardIndex = pattern.indexOf('*'); + if (wildcardIndex === -1) { + continue; + } + + const baseDir = pattern.substring(0, wildcardIndex); + + if (filePath.startsWith(baseDir)) { + // Extract the package name (everything between baseDir and the next slash) + const remainingPath = filePath.substring(baseDir.length); + const nextSlashIndex = remainingPath.indexOf('/'); + + if (nextSlashIndex !== -1) { + const packageName = remainingPath.substring(0, nextSlashIndex); + return { + base: baseDir, + package: packageName, + }; + } + } + } + + return null; +} + +/** + * Gets the list of changed files between the current branch and baseRef. + * + * @param repoPath - The path to the repository. + * @param baseRef - The base reference to compare against. + * @returns Array of changed file paths. + */ +async function getChangedFiles( + repoPath: string, + baseRef: string, +): Promise { + try { + await execa('git', ['fetch', 'origin', baseRef], { + cwd: repoPath, + }); + + const { stdout } = await execa( + 'git', + ['diff', '--name-only', `origin/${baseRef}...HEAD`], + { + cwd: repoPath, + }, + ); + + return stdout.split('\n').filter(Boolean); + } catch (error) { + console.error('Failed to get changed files:', error); + throw error; + } +} + +/** + * Reads and validates a changelog file. + * + * @param changelogPath - The path to the changelog file to check. + */ +async function checkChangelogFile(changelogPath: string): Promise { + try { + const changelogContent = await fs.readFile(changelogPath, 'utf-8'); + + if (!changelogContent) { + throw new Error('CHANGELOG.md is empty or missing'); + } + + const changelogUnreleasedChanges = parseChangelog({ + changelogContent, + repoUrl: '', // Not needed as we're only parsing unreleased changes + }).getReleaseChanges('Unreleased'); + + if (Object.values(changelogUnreleasedChanges).length === 0) { + throw new Error( + "❌ No new entries detected under '## Unreleased' section. Please update the changelog.", + ); + } + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + throw new Error(`❌ CHANGELOG.md not found at ${changelogPath}`); + } + throw error; + } +} + +/** + * Gets the list of changed packages from the changed files. + * + * @param files - The list of changed files. + * @param workspacePatterns - The workspace patterns. + * @returns Array of changed package information. + */ +async function getChangedPackages( + files: string[], + workspacePatterns: string[], +): Promise< + { + base: string; + package: string; + }[] +> { + const changedPackages = new Map(); + + for (const file of files) { + // Skip workflow files + if (file.startsWith('.github/workflows/')) { + continue; + } + + const packageInfo = getPackageInfo(file, workspacePatterns); + if (packageInfo) { + // Skip test files, docs, and changelog files + if ( + !file.match(/\.(test|spec)\./u) && + !file.includes('__tests__/') && + !file.includes('/docs/') && + !file.endsWith('CHANGELOG.md') + ) { + changedPackages.set(packageInfo.package, packageInfo); + } + } + } + + return Array.from(changedPackages.values()); +} + +/** + * Main function to run the changelog check. + */ +async function main() { + // Parse command-line arguments + const args = process.argv.slice(2); + + const [repoPath, baseRef] = args; + + if (!repoPath || !baseRef) { + console.error( + '❌ Usage: ts-node src/check-changelog.ts ', + ); + throw new Error('❌ Missing required arguments.'); + } + + const fullRepoPath = path.resolve(process.cwd(), repoPath); + + // Verify the repo path exists + try { + await fs.access(fullRepoPath); + } catch { + throw new Error(`Repository path not found: ${fullRepoPath}`); + } + + const workspacePatterns = await getWorkspacePatterns(fullRepoPath); + + if (workspacePatterns.length > 0) { + console.log( + 'Running in monorepo mode - checking changelogs for changed packages...', + ); + + const changedFiles = await getChangedFiles(fullRepoPath, baseRef); + if (!changedFiles.length) { + console.log('No changed files found. Exiting successfully.'); + return; + } + + const changedPackages = await getChangedPackages( + changedFiles, + workspacePatterns, + ); + if (!changedPackages.length) { + console.log( + 'No package code changes detected that would require changelog updates.', + ); + return; + } + + const checkResults = await Promise.all( + changedPackages.map(async (pkgInfo) => { + try { + await checkChangelogFile( + path.join( + fullRepoPath, + pkgInfo.base, + pkgInfo.package, + 'CHANGELOG.md', + ), + ); + console.log( + `✅ CHANGELOG.md for ${pkgInfo.package} has been correctly updated.`, + ); + return { package: pkgInfo.package, success: true }; + } catch (error) { + console.error( + `❌ Changelog check failed for package ${pkgInfo.package}:`, + error, + ); + return { package: pkgInfo.package, success: false, error }; + } + }), + ); + + const hasError = checkResults.some((result) => !result.success); + + if (hasError) { + throw new Error('One or more changelog checks failed'); + } + } else { + console.log( + 'Running in single-repo mode - checking changelog for the entire repository...', + ); + await checkChangelogFile(path.join(fullRepoPath, 'CHANGELOG.md')); + console.log('✅ CHANGELOG.md has been correctly updated.'); + } +} + +main().catch((error) => { + throw error; +}); diff --git a/src/scripts/count-references-to-contributor-docs/cli.ts b/src/scripts/count-references-to-contributor-docs/cli.ts index 9cc60738..9a43268e 100644 --- a/src/scripts/count-references-to-contributor-docs/cli.ts +++ b/src/scripts/count-references-to-contributor-docs/cli.ts @@ -15,7 +15,7 @@ const REPOSITORY_NAMES = [ 'snaps', ] as const; -type RepositoryName = typeof REPOSITORY_NAMES[number]; +type RepositoryName = (typeof REPOSITORY_NAMES)[number]; /** * It is not necessary for us to query all of the pull requests or pull requests diff --git a/yarn.lock b/yarn.lock index 9a7e42e0..108a0de9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -941,6 +941,21 @@ __metadata: languageName: node linkType: hard +"@metamask/auto-changelog@npm:^3.4.4": + version: 3.4.4 + resolution: "@metamask/auto-changelog@npm:3.4.4" + dependencies: + diff: "npm:^5.0.0" + execa: "npm:^5.1.1" + prettier: "npm:^2.8.8" + semver: "npm:^7.3.5" + yargs: "npm:^17.0.1" + bin: + auto-changelog: dist/cli.js + checksum: 10/70e98529a153ebeab10410dbc3f567014999f77ed82f2b52f1b36501b28a4e3614c809a90c89600a739d7710595bfecc30e2260410e6afac7539f8db65a48f2c + languageName: node + linkType: hard + "@metamask/eslint-config-jest@npm:^12.0.0": version: 12.1.0 resolution: "@metamask/eslint-config-jest@npm:12.1.0" @@ -997,6 +1012,7 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^2.3.1" "@lavamoat/preinstall-always-fail": "npm:^1.0.0" + "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eslint-config": "npm:^12.0.0" "@metamask/eslint-config-jest": "npm:^12.0.0" "@metamask/eslint-config-nodejs": "npm:^12.0.0" @@ -1024,6 +1040,7 @@ __metadata: eslint-plugin-n: "npm:^15.7.0" eslint-plugin-prettier: "npm:^4.2.1" eslint-plugin-promise: "npm:^6.1.1" + execa: "npm:^9.5.2" googleapis: "npm:144.0.0" jest: "npm:^28.1.3" jest-it-up: "npm:^2.0.2" @@ -1395,6 +1412,13 @@ __metadata: languageName: node linkType: hard +"@sec-ant/readable-stream@npm:^0.4.1": + version: 0.4.1 + resolution: "@sec-ant/readable-stream@npm:0.4.1" + checksum: 10/aac89581652ac85debe7c5303451c2ebf8bf25ca25db680e4b9b73168f6940616d9a4bbe3348981827b1159b14e2f2e6af4b7bd5735cac898c12d5c51909c102 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.24.1": version: 0.24.28 resolution: "@sinclair/typebox@npm:0.24.28" @@ -1409,6 +1433,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^4.0.0": + version: 4.0.0 + resolution: "@sindresorhus/merge-streams@npm:4.0.0" + checksum: 10/16551c787f5328c8ef05fd9831ade64369ccc992df78deb635ec6c44af217d2f1b43f8728c348cdc4e00585ff2fad6e00d8155199cbf6b154acc45fe65cbf0aa + languageName: node + linkType: hard + "@sinonjs/commons@npm:^1.7.0": version: 1.8.1 resolution: "@sinonjs/commons@npm:1.8.1" @@ -3294,6 +3325,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 10/01b7b440f83a997350a988e9d2f558366c0f90f15be19f4aa7f1bb3109a4e153dfc3b9fbf78e14ea725717017407eeaa2271e3896374a0181e8f52445740846d + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -3905,7 +3943,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.0.0": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -3922,6 +3960,26 @@ __metadata: languageName: node linkType: hard +"execa@npm:^9.5.2": + version: 9.5.2 + resolution: "execa@npm:9.5.2" + dependencies: + "@sindresorhus/merge-streams": "npm:^4.0.0" + cross-spawn: "npm:^7.0.3" + figures: "npm:^6.1.0" + get-stream: "npm:^9.0.0" + human-signals: "npm:^8.0.0" + is-plain-obj: "npm:^4.1.0" + is-stream: "npm:^4.0.1" + npm-run-path: "npm:^6.0.0" + pretty-ms: "npm:^9.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^4.0.0" + yoctocolors: "npm:^2.0.0" + checksum: 10/652fa492e7e1052becba12275aafd1c3d249967a4400f798877aa6c21fafcd8182ee3ce09a54f0379785635f32a4adeef77b2edb83d7e8a55b06819ed102ff2a + languageName: node + linkType: hard + "executable@npm:^4.1.0": version: 4.1.1 resolution: "executable@npm:4.1.1" @@ -4036,6 +4094,15 @@ __metadata: languageName: node linkType: hard +"figures@npm:^6.1.0": + version: 6.1.0 + resolution: "figures@npm:6.1.0" + dependencies: + is-unicode-supported: "npm:^2.0.0" + checksum: 10/9822d13630bee8e6a9f2da866713adf13854b07e0bfde042defa8bba32d47a1c0b2afa627ce73837c674cf9a5e3edce7e879ea72cb9ea7960b2390432d8e1167 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -4344,6 +4411,16 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^9.0.0": + version: 9.0.1 + resolution: "get-stream@npm:9.0.1" + dependencies: + "@sec-ant/readable-stream": "npm:^0.4.1" + is-stream: "npm:^4.0.1" + checksum: 10/ce56e6db6bcd29ca9027b0546af035c3e93dcd154ca456b54c298901eb0e5b2ce799c5d727341a100c99e14c523f267f1205f46f153f7b75b1f4da6d98a21c5e + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -4696,6 +4773,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^8.0.0": + version: 8.0.1 + resolution: "human-signals@npm:8.0.1" + checksum: 10/903389a018b16f330c5e0f6e8b76d592c79552152ea892f249e5290e71c790f5722dc9b740fedd4bdef30566754a69012aaed97a6a528da0d417fad990a6f515 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -4984,6 +5068,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^4.1.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 10/6dc45da70d04a81f35c9310971e78a6a3c7a63547ef782e3a07ee3674695081b6ca4e977fbb8efc48dae3375e0b34558d2bcd722aec9bddfa2d7db5b041be8ce + languageName: node + linkType: hard + "is-plain-object@npm:^5.0.0": version: 5.0.0 resolution: "is-plain-object@npm:5.0.0" @@ -5026,6 +5117,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^4.0.1": + version: 4.0.1 + resolution: "is-stream@npm:4.0.1" + checksum: 10/cbea3f1fc271b21ceb228819d0c12a0965a02b57f39423925f99530b4eb86935235f258f06310b67cd02b2d10b49e9a0998f5ececf110ab7d3760bae4055ad23 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -5060,6 +5158,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -6295,6 +6400,16 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^6.0.0": + version: 6.0.0 + resolution: "npm-run-path@npm:6.0.0" + dependencies: + path-key: "npm:^4.0.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10/1a1b50aba6e6af7fd34a860ba2e252e245c4a59b316571a990356417c0cdf0414cabf735f7f52d9c330899cb56f0ab804a8e21fb12a66d53d7843e39ada4a3b6 + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -6541,6 +6656,13 @@ __metadata: languageName: node linkType: hard +"parse-ms@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-ms@npm:4.0.0" + checksum: 10/673c801d9f957ff79962d71ed5a24850163f4181a90dd30c4e3666b3a804f53b77f1f0556792e8b2adbb5d58757907d1aa51d7d7dc75997c2a56d72937cbc8b7 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -6569,6 +6691,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -6684,12 +6813,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.7.1": - version: 2.7.1 - resolution: "prettier@npm:2.7.1" +"prettier@npm:^2.7.1, prettier@npm:^2.8.8": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" bin: prettier: bin-prettier.js - checksum: 10/9d29f81c1a470efca6851cd926a3e132a8d9c9d290c3d084c917c1c5aad5c392551406cf6012c724a136bd15911ede5eadc255d121c2761813b33a541a9c34c6 + checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 languageName: node linkType: hard @@ -6705,6 +6834,15 @@ __metadata: languageName: node linkType: hard +"pretty-ms@npm:^9.0.0": + version: 9.2.0 + resolution: "pretty-ms@npm:9.2.0" + dependencies: + parse-ms: "npm:^4.0.0" + checksum: 10/a65a1d81560867f4f7128862fdbf0e1c2d3c5607bf75cae7758bf8111e2c4b744be46e084704125a38ba918bb43defa7a53aaff0f48c5c2d95367d3148c980d9 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -7229,10 +7367,10 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.0.1 - resolution: "signal-exit@npm:4.0.1" - checksum: 10/3d345d3a6c3ed533aa8107f8fab73257cf8ddacd0a297c00d0fd3c960a9802900e3cbc518076254ee4ab99ef253b059929d6bf907c5fbc805fdc8a186d900bc0 +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f languageName: node linkType: hard @@ -7524,6 +7662,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-final-newline@npm:4.0.0" + checksum: 10/b5fe48f695d74863153a3b3155220e6e9bf51f4447832998c8edec38e6559b3af87a9fe5ac0df95570a78a26f5fa91701358842eab3c15480e27980b154a145f + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -7907,6 +8052,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10/bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -8195,7 +8347,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1": +"yargs@npm:^17.0.1, yargs@npm:^17.3.1": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -8223,3 +8375,10 @@ __metadata: checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"yoctocolors@npm:^2.0.0": + version: 2.1.1 + resolution: "yoctocolors@npm:2.1.1" + checksum: 10/563fbec88bce9716d1044bc98c96c329e1d7a7c503e6f1af68f1ff914adc3ba55ce953c871395e2efecad329f85f1632f51a99c362032940321ff80c42a6f74d + languageName: node + linkType: hard