├── .editorconfig ├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── bin ├── index.ts └── reset.d.ts ├── license ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@fisch0920/eslint-config/node"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [transitive-bullshit] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Test Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | node-version: 14 | - 18 15 | - 22 16 | - 23 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pnpm/action-setup@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'pnpm' 25 | 26 | - run: pnpm install --frozen-lockfile --strict-peer-dependencies 27 | - run: pnpm build 28 | - run: pnpm test 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: pnpm/action-setup@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | cache: pnpm 23 | - run: pnpm dlx changelogithub 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | 14 | # production 15 | build/ 16 | dist/ 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # turbo 32 | .turbo 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | .env 42 | 43 | old/ 44 | out/ 45 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | package-manager-strict=false 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import assert from 'node:assert' 3 | import fs from 'node:fs/promises' 4 | import path from 'node:path' 5 | 6 | import { packageManager } from '@pnpm/cli-meta' 7 | import { createResolver } from '@pnpm/client' 8 | import { getConfig } from '@pnpm/config' 9 | import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package' 10 | import { cli } from 'cleye' 11 | import { gracefulExit } from 'exit-hook' 12 | import { oraPromise } from 'ora' 13 | import pMap from 'p-map' 14 | import plur from 'plur' 15 | import YAML from 'yaml' 16 | 17 | async function main() { 18 | const args = cli( 19 | { 20 | name: 'pnpm-update-catalogs', 21 | parameters: ['[pkg...]'], 22 | flags: { 23 | latest: { 24 | type: Boolean, 25 | description: 'Update all catalogs to the latest version', 26 | alias: 'L', 27 | default: false 28 | }, 29 | verbose: { 30 | type: Boolean, 31 | description: 'Verbose output', 32 | alias: 'V', 33 | default: false 34 | }, 35 | catalogs: { 36 | type: [String], 37 | description: 38 | 'Update specific catalogs (by default, all catalogs will be updated)', 39 | alias: 'c' 40 | } 41 | } 42 | }, 43 | () => {}, 44 | process.argv 45 | ) 46 | 47 | const packagesToProcess = 48 | args._.length > 2 ? new Set(args._.slice(2)) : undefined 49 | 50 | const catalogsToProcess = args.flags.catalogs?.length 51 | ? new Set(args.flags.catalogs) 52 | : undefined 53 | 54 | const { config } = await getConfig({ 55 | cliOptions: {}, 56 | packageManager, 57 | workspaceDir: process.cwd() 58 | }) 59 | 60 | const { resolve } = createResolver({ 61 | ...config, 62 | authConfig: config.rawConfig, 63 | retry: { 64 | retries: 0 65 | } 66 | }) 67 | 68 | const { catalogs, workspaceDir } = config 69 | if (!catalogs || !Object.keys(catalogs).length) { 70 | console.error('No workspace catalogs found') 71 | gracefulExit(1) 72 | return 73 | } 74 | 75 | if (!workspaceDir) { 76 | console.error('No workspace directory found') 77 | gracefulExit(1) 78 | return 79 | } 80 | 81 | const packagesProcessed = new Set() 82 | 83 | const allCatalogEntries = Object.entries(catalogs) 84 | .flatMap(([catalogName, catalog]) => { 85 | if (catalogsToProcess && !catalogsToProcess.has(catalogName)) { 86 | return [] 87 | } 88 | 89 | if (!catalog) { 90 | return [] 91 | } 92 | 93 | return Object.entries(catalog) 94 | .map(([packageName, pref]) => { 95 | assert(pref) 96 | 97 | if (packagesToProcess && !packagesToProcess.has(packageName)) { 98 | return undefined 99 | } 100 | 101 | packagesProcessed.add(packageName) 102 | 103 | return { 104 | catalogName, 105 | packageName, 106 | pref 107 | } 108 | }) 109 | .filter(Boolean) 110 | }) 111 | .filter((catalogEntry) => catalogEntry.pref !== 'latest') 112 | 113 | if (packagesToProcess && packagesProcessed.size !== packagesToProcess.size) { 114 | const missingPackages = Array.from(packagesToProcess).filter( 115 | (pkg) => !packagesProcessed.has(pkg) 116 | ) 117 | console.error('Error: missing catalog packages:', missingPackages) 118 | gracefulExit(1) 119 | return 120 | } 121 | 122 | const catalogEntryResolutions = await oraPromise( 123 | pMap( 124 | allCatalogEntries, 125 | async ({ catalogName, packageName, pref }) => { 126 | if (args.flags.verbose) { 127 | console.log(`Resolving ${catalogName} ${packageName}@${pref}`) 128 | } 129 | 130 | const resolution = await resolve( 131 | { alias: packageName, pref }, 132 | { 133 | lockfileDir: config.lockfileDir ?? config.dir, 134 | preferredVersions: {}, 135 | projectDir: config.dir, 136 | registry: pickRegistryForPackage( 137 | config.registries, 138 | packageName, 139 | args.flags.latest ? 'latest' : pref 140 | ) 141 | } 142 | ) 143 | 144 | return { 145 | catalogName, 146 | packageName, 147 | pref, 148 | resolution 149 | } 150 | }, 151 | { 152 | concurrency: 16 153 | } 154 | ), 155 | `Resolving ${allCatalogEntries.length} catalog ${plur('package', allCatalogEntries.length)}...` 156 | ) 157 | 158 | if (args.flags.verbose) { 159 | console.log(JSON.stringify(catalogEntryResolutions, null, 2)) 160 | } 161 | 162 | // Read the original workspace file 163 | const workspaceFilePath = path.join(workspaceDir, 'pnpm-workspace.yaml') 164 | const currentWorkspaceFileContents = await fs.readFile( 165 | workspaceFilePath, 166 | 'utf8' 167 | ) 168 | const workspaceFile = YAML.parseDocument(currentWorkspaceFileContents) 169 | let hasUpdates = false 170 | 171 | // Update the workspace file's YAML 172 | for (const catalogEntryResolution of catalogEntryResolutions) { 173 | const { catalogName, packageName, pref, resolution } = 174 | catalogEntryResolution 175 | 176 | const resolvedVersion = args.flags.latest 177 | ? resolution.latest 178 | : resolution?.manifest?.version 179 | if (!resolvedVersion) { 180 | // No version found 181 | continue 182 | } 183 | 184 | if (pref === resolvedVersion || pref === `^${resolvedVersion}`) { 185 | // Already up-to-date 186 | continue 187 | } 188 | 189 | const prefHasCaret = pref.startsWith('^') 190 | const updatedVersion = prefHasCaret 191 | ? `^${resolvedVersion}` 192 | : resolvedVersion 193 | 194 | const currentCatalog: any = 195 | workspaceFile.get(`catalogs.${catalogName}`) ?? 196 | (catalogName === 'default' ? workspaceFile.get('catalog') : undefined) 197 | assert( 198 | currentCatalog, 199 | `Catalog "${catalogName}" not found in current workspace file` 200 | ) 201 | 202 | const key = currentCatalog.get(packageName) 203 | ? packageName 204 | : currentCatalog.get(`"${packageName}"`) 205 | ? `"${packageName}"` 206 | : currentCatalog.get(`'${packageName}'`) 207 | ? `${packageName}` 208 | : undefined 209 | assert( 210 | key, 211 | `Package "${packageName}" not found in catalog "${catalogName}" in workspace file` 212 | ) 213 | 214 | // Update the version in the catalog 215 | currentCatalog.set(key, updatedVersion) 216 | hasUpdates = true 217 | 218 | console.log( 219 | `Updating catalog "${catalogName}" package "${packageName}" from "${pref}" to "${updatedVersion}"` 220 | ) 221 | } 222 | 223 | if (!hasUpdates) { 224 | console.log('No updates found') 225 | return 226 | } 227 | 228 | await fs.writeFile(workspaceFilePath, workspaceFile.toString()) 229 | console.log(`Updated workspace file: ${workspaceFilePath}`) 230 | } 231 | 232 | try { 233 | await main() 234 | gracefulExit(0) 235 | } catch (err) { 236 | console.error(err) 237 | gracefulExit(1) 238 | } 239 | -------------------------------------------------------------------------------- /bin/reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Travis Fischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pnpm-update-catalogs", 3 | "version": "1.0.4", 4 | "description": "pnpm update for pnpm workspace catalogs.", 5 | "author": "Travis Fischer ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/transitive-bullshit/pnpm-update-catalogs.git" 10 | }, 11 | "packageManager": "pnpm@10.6.5", 12 | "engines": { 13 | "node": ">=18" 14 | }, 15 | "type": "module", 16 | "bin": { 17 | "pnpm-update-catalogs": "./dist/index.js" 18 | }, 19 | "source": "./bin/index.ts", 20 | "types": "./dist/index.d.ts", 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "build": "tsup", 26 | "clean": "del clean", 27 | "test": "run-s test:*", 28 | "test:format": "prettier --check \"**/*.{js,ts,tsx}\"", 29 | "test:lint": "eslint .", 30 | "test:typecheck": "tsc --noEmit", 31 | "release": "bumpp && pnpm publish", 32 | "prebuild": "run-s clean", 33 | "preinstall": "npx only-allow pnpm", 34 | "prerelease": "run-s build", 35 | "pretest": "run-s build" 36 | }, 37 | "dependencies": { 38 | "@pnpm/cli-meta": "^1000.0.4", 39 | "@pnpm/client": "^1000.0.12", 40 | "@pnpm/config": "^1002.5.4", 41 | "@pnpm/pick-registry-for-package": "^1000.0.4", 42 | "cleye": "^1.3.4", 43 | "exit-hook": "^4.0.0", 44 | "ora": "^8.2.0", 45 | "p-map": "^7.0.3", 46 | "plur": "^5.1.0", 47 | "yaml": "^2.7.0" 48 | }, 49 | "devDependencies": { 50 | "@fisch0920/eslint-config": "^1.4.0", 51 | "@total-typescript/ts-reset": "^0.6.1", 52 | "@types/node": "^22.13.13", 53 | "bumpp": "^10.1.0", 54 | "del-cli": "^6.0.0", 55 | "dotenv": "^16.4.7", 56 | "eslint": "^8.57.1", 57 | "npm-run-all2": "^7.0.2", 58 | "only-allow": "^1.2.1", 59 | "prettier": "^3.5.3", 60 | "tsup": "^8.4.0", 61 | "typescript": "^5.8.2", 62 | "vitest": "3.0.9" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # pnpm-update-catalogs 2 | 3 | > pnpm update for `pnpm-workspace.yaml` [catalogs](https://pnpm.io/catalogs). 4 | 5 |

6 | Build Status 7 | NPM 8 | MIT License 9 | Prettier Code Formatting 10 |

11 | 12 | - [Intro](#intro) 13 | - [Features](#features) 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [Example: Update specific packages to latest](#example-update-specific-packages-to-latest) 17 | - [Example: Update all packages to latest](#example-update-all-packages-to-latest) 18 | - [Related tools](#related-tools) 19 | - [License](#license) 20 | 21 | ## Intro 22 | 23 | This CLI provides a temporary fix for: https://github.com/pnpm/pnpm/issues/8641 24 | 25 | Namely, it allows you to run a command at the top of your `pnpm` workspaces monorepo which updates all of the [catalog](https://pnpm.io/catalogs) dependencies. 26 | 27 | ## Features 28 | 29 | - Uses internal `pnpm` packages so config and dependency resolution are as close to the official `pnpm` version as possible 30 | - Formatting and yaml comments are preserved 31 | - By default, follows current version ranges but supports the `--latest` (`-L`) option as well 32 | - Optionally target specific catalogs or specific packages (by default all catalogs and packages will be updated) 33 | 34 | ## Install 35 | 36 | ```sh 37 | npm install -g pnpm-update-catalogs 38 | ``` 39 | 40 | Or you can just run it from `npm`: 41 | 42 | ```sh 43 | npx pnpm-update-catalogs --help 44 | ``` 45 | 46 | ## Usage 47 | 48 | ```sh 49 | pnpm-update-catalogs 50 | 51 | Usage: 52 | pnpm-update-catalogs [flags...] [pkg...] 53 | 54 | Flags: 55 | -c, --catalogs Update specific catalogs (by default, all catalogs will be updated) 56 | -h, --help Show help 57 | -L, --latest Update all catalogs to the latest version 58 | -V, --verbose Verbose output 59 | ``` 60 | 61 | You can optionally pass an array of catalog packages to update. 62 | 63 | > [!NOTE] 64 | > Make sure you run `pnpm install` after updating your workspace file for the updated catalog packages to actually be installed. 65 | 66 | ### Example: Update specific packages to latest 67 | 68 | ```sh 69 | npx pnpm-update-catalogs -L typescript tsx 70 | pnpm install 71 | git diff 72 | ``` 73 | 74 | ### Example: Update all packages to latest 75 | 76 | ```sh 77 | npx pnpm-update-catalogs -L 78 | pnpm install 79 | git diff 80 | ``` 81 | 82 | Example output after running on my [agentic repo](https://github.com/transitive-bullshit/agentic): 83 | 84 | ```diff 85 | diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml 86 | index 4619573..6ce2aaa 100644 87 | --- a/pnpm-workspace.yaml 88 | +++ b/pnpm-workspace.yaml 89 | @@ -34,35 +34,35 @@ catalog: 90 | execa: ^9.5.2 91 | exit-hook: ^4.0.0 92 | fast-xml-parser: ^5.0.9 93 | - genkit: ^1.2.0 94 | - genkitx-openai: ^0.20.1 95 | + genkit: ^1.3.0 96 | + genkitx-openai: ^0.20.2 97 | json-schema-to-zod: ^2.6.0 98 | - jsonrepair: ^3.9.0 # this comment will be preserved 99 | + jsonrepair: ^3.12.0 # this comment will be preserved 100 | jsrsasign: ^10.9.0 101 | ky: ^1.7.5 # this comment will also be preserved 102 | - langchain: ^0.3.3 103 | + langchain: ^0.3.19 104 | lint-staged: ^15.5.0 105 | - llamaindex: ^0.9.11 106 | - mathjs: ^13.0.3 107 | + llamaindex: ^0.9.12 108 | + mathjs: ^13.2.3 109 | npm-run-all2: ^7.0.2 110 | - octokit: ^4.0.2 111 | + octokit: ^4.1.2 112 | only-allow: ^1.2.1 113 | - openai: ^4.87.3 114 | + openai: ^4.89.0 115 | openai-fetch: ^3.4.2 116 | openai-zod-to-json-schema: ^1.0.3 117 | openapi-types: ^12.1.3 118 | - p-map: ^7.0.2 119 | + p-map: ^7.0.3 120 | p-throttle: ^6.2.0 121 | prettier: ^3.5.3 122 | restore-cursor: ^5.1.0 123 | - simple-git-hooks: ^2.11.1 124 | + simple-git-hooks: ^2.12.1 125 | string-strip-html: ^13.4.12 126 | syncpack: 14.0.0-alpha.10 127 | tsup: ^8.4.0 128 | tsx: ^4.19.3 129 | turbo: ^2.4.4 130 | twitter-api-sdk: ^1.2.1 131 | - type-fest: ^4.37.0 132 | + type-fest: ^4.38.0 133 | typescript: ^5.8.2 134 | vitest: ^3.0.9 135 | wikibase-sdk: ^10.2.2 136 | ``` 137 | 138 | ## Related tools 139 | 140 | It looks like [taze](https://github.com/antfu-collective/taze) supports pnpm workspaces, which is likely a much more robust tool than this one. 141 | 142 | `pnpm outdated -r` is useful to see all the dependencies in your workspace that are out-of-date. 143 | 144 | `npx codemod pnpm/catalog` is great for converting your workspace's normal dependencies to use the catalog instead. 145 | 146 | ## License 147 | 148 | MIT © [Travis Fischer](https://x.com/transitive_bs) 149 | 150 | If you found this project interesting, [consider following me on Twitter](https://x.com/transitive_bs). 151 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom.iterable"], 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "noEmit": true, 9 | "target": "es2020", 10 | "outDir": "dist", 11 | "rootDir": ".", 12 | 13 | "allowImportingTsExtensions": false, 14 | "allowJs": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "incremental": false, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "noUncheckedIndexedAccess": true, 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "sourceMap": true, 24 | "strict": true, 25 | "useDefineForClassFields": true 26 | // "verbatimModuleSyntax": true 27 | }, 28 | "include": ["bin"] 29 | } 30 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['bin/index.ts'], 6 | outDir: 'dist', 7 | target: 'node18', 8 | platform: 'node', 9 | format: ['esm'], 10 | splitting: false, 11 | sourcemap: true, 12 | minify: false, 13 | shims: true, 14 | dts: true 15 | } 16 | ]) 17 | --------------------------------------------------------------------------------