Multi-language dependency updater CLI tool
Python (pyproject.toml)
|
Tauri (package.json + Cargo.toml)
|
- Multi-Language Support: Node.js, Python, Rust, Go, Ruby, PHP, Java, Swift
- mise Support: Updates tool versions in
mise.toml/.tool-versionsthrough the same workflow - Manifest Updates: Directly updates version specifications in manifest files
- Smart Version Handling: Preserves version range formats (^, ~, >=) while keeping upper bounds intact
- Pinned Version Detection: Skips intentionally pinned versions by default
- Age Filter: Only update to versions released N days/weeks ago
- pnpm Integration: Respects
minimumReleaseAgefrom pnpm settings - Bun Catalogs: Updates Bun
catalog/catalogsdefinitions inpackage.json - Monorepo Support:
.depup, Cargo/pnpm/Go workspaces, Gradle multi-project builds, nested package installs, and Tauri projects - Release Date Display: Shows when each new version was released
- Multiple Output Formats: Text (colored), JSON, diff
- OS: macOS, Linux, Windows
- Rust: 1.85+ (for building from source)
- mise: only needed to update mise tool versions (version lists come from
mise ls-remote). Without it, depup skips mise config files, warns once, and continues with the other languages
brew install owayo/depup/depupwinget install owayo.depupgit clone https://github.com/owayo/depup.git
cd depup
cargo install --path .Download the latest binary from Releases.
curl -L https://github.com/owayo/depup/releases/latest/download/depup-aarch64-apple-darwin.tar.gz | tar xz
sudo mv depup /usr/local/bin/curl -L https://github.com/owayo/depup/releases/latest/download/depup-x86_64-apple-darwin.tar.gz | tar xz
sudo mv depup /usr/local/bin/curl -L https://github.com/owayo/depup/releases/latest/download/depup-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv depup /usr/local/bin/curl -L https://github.com/owayo/depup/releases/latest/download/depup-aarch64-unknown-linux-gnu.tar.gz | tar xz
sudo mv depup /usr/local/bin/Download depup-x86_64-pc-windows-msvc.zip from Releases, extract, and add to PATH.
winget install owayo.depupdoes this for you (it registersdepupon PATH), so the manual download is only needed if you do not use winget. After a winget install, open a new terminal so the updated PATH takes effect.
# Update all dependencies (dry run)
depup -n
# Update Node.js dependencies only
depup --node
# Update with age filter (2 weeks minimum)
depup --age 2w
# Update and show diff
depup --diffdepup [OPTIONS] [PATH]| Option | Short | Description |
|---|---|---|
--cd <DIR> |
-C |
Change to directory before running |
--dry-run |
-n |
Show what would be updated without making changes |
--verbose |
Enable verbose output | |
--quiet |
-q |
Minimal output |
--node |
Update only Node.js dependencies | |
--python |
Update only Python dependencies | |
--rust |
Update only Rust dependencies | |
--go |
Update only Go dependencies | |
--ruby |
Update only Ruby dependencies | |
--php |
Update only PHP dependencies | |
--java |
Update only Java dependencies | |
--swift |
Update only Swift dependencies | |
--mise |
Update only mise tool versions (mise.toml / .tool-versions) | |
--exclude <PKG> |
Exclude specific packages (repeatable) | |
--only <PKG> |
Update only specific packages (repeatable) | |
--include-pinned |
Include pinned versions in update | |
--age <DURATION> |
Minimum release age (e.g., 2w, 10d, 1m). Overrides global config | |
--no-age |
Disable age filter for this run (overrides global config and default) | |
--osv |
Check candidates against the OSV.dev vulnerability database and skip versions with known vulnerabilities (enabled by default) | |
--no-osv |
Disable OSV vulnerability check for this run (overrides global config and default) | |
--max-change <LEVEL> |
Limit allowed bumps: patch (patch only), minor (patch + minor), major (default — all) |
|
--json |
Output results in JSON format | |
--diff |
Show changes in diff format | |
--install |
Run package manager install after update | |
--version |
-V |
Show version |
--help |
-h |
Show help |
# Preview all updates
depup -n
# Update only lodash and typescript
depup --only lodash --only typescript
# Exclude react from updates
depup --exclude react
# --only takes precedence if the same package is also excluded
depup --only lodash --exclude lodash
# Update packages at least 2 weeks old
depup --age 2w
# Update Python and Rust only
depup --python --rust
# Update Java (Gradle) dependencies
depup --java
# Update Swift (Package.swift) dependencies
depup --swift
# Update mise tool versions (mise.toml / .tool-versions)
depup --mise
# JSON output for CI/CD
depup --json
# Update and run npm install
depup --node --install
# Run in a different directory
depup --cd ./projects/myapp -nPinned versions are intentionally fixed and excluded from updates by default:
| Language | Pinned Example | Updated |
|---|---|---|
| Node.js | "1.2.3" |
❌ |
| Node.js | "^1.2.3", "~1.2.3" |
✅ |
| Python | "==1.2.3" |
❌ |
| Python | ">=1.2.3", "^1.2.3" |
✅ |
| Rust | "=1.2.3" |
❌ |
| Rust | "1.2.3", "^1.2.3" |
✅ |
| Go | // pinned comment |
❌ |
| Ruby | '= 1.2.3' |
❌ |
| Ruby | '~> 1.2.3', '>= 1.2.3' |
✅ |
| PHP | "1.2.3" |
❌ |
| PHP | "^1.2.3", "~1.2.3" |
✅ |
| Java | Fixed version in Gradle | ❌ |
| Java | Strict version in Gradle (1.2.3!!) |
❌ |
| Java | Maven Hard requirement ([1.0]) |
❌ |
| Swift | exact: "1.2.3" |
❌ |
| Swift | from: "1.2.3", .upToNextMinor |
✅ |
Use --include-pinned to update pinned versions.
Note: Go dependencies are always included in updates regardless of the
--include-pinnedflag, becausego.modonly supports exact versions (no range specifiers like^or~). All Go versions are effectively "pinned" by nature.Note: Go
excludedirectives are applied to update candidates without rewriting the directive itself. Versions retracted by the upstream module's latestgo.modare also excluded, including closed retract ranges. For modules without tagged versions, depup falls back to the Go Proxy@latestendpoint; an omitted.infoTimeuses the Unix epoch so an unknown release date is not permanently filtered by--age.Note: Gemfile compound constraints such as
gem "pg", ">= 0.18", "< 2.0"are parsed and updated. depup advances only the inclusive lower bound and writes it back across the original arguments, preserving their count, order, quote style, spacing, parenthesized call form, and trailing conditional modifiers. The comparison baseline is the inclusive lower bound regardless of the order the constraints are written in, sogem "pg", "< 2.0", ">= 0.18"is compared against0.18. If the rewritten constraint cannot be split back into the original number of arguments (for example when one argument itself contains a comma), depup reports an error instead of applying an unsafe edit. Exclusion constraints such asgem "rack", "!= 2.2.4"are skipped at the judging stage because replacing part of them can change their meaning.Note: Custom Bundler git source shorthands registered with
git_source(:name) { ... }(for examplegem 'rails', stash: 'forks/rails') are treated as non-registry dependencies and skipped, just like the built-ingit:/github:shorthands.Note: Gemfile entries that point to non-registry sources without a version (
git:,github:,bitbucket:,gist:,path:,source:) are skipped instead of being converted into RubyGems registry constraints. If such an entry explicitly includes a version, depup treats it as Bundler's gemspec constraint and can parse and update it while preserving the source option. Both Ruby option spellings are recognised —git: '...'and the hash-rocket form:git => '...'. Gems declared insidegit ... do/github ... do/path ... do/source ... doblocks are skipped for the same reason, while ordinary blocks such asplatformsandinstall_ifare still processed. Declarations whose arguments continue on the next line (gem "devise",) are skipped rather than reported as versionless registry gems, because that line alone cannot determine the version. Inlinegroup:/groups:options are used to classify development dependencies.Note: Gemfile declarations can use either the common Ruby DSL form (
gem "rack", "~> 3.0") or parenthesized method-call form (gem("rack", "~> 3.0")). Both forms are parsed and updated while preserving the original call style. When the same gem is declared in multiple places (for example both at the top level and inside agroup :testblock), depup refuses the ambiguous write.Note: Cargo renamed dependencies such as
alias = { package = "actual-crate", version = "1" }are fetched by the real package name and written back through the manifest key.--onlyand--excludeaccept either name.Note: When
--onlyis present, it takes precedence over--exclude. This lets an explicit allow-list entry remain updatable even if the same package also appears in a broader exclude list.Note: Composer platform packages such as
php,hhvm,ext-*,lib-*, and Composer API packages are skipped.Note: Composer/Packagist accepts 1-4 segment numeric versions per
composer/semver'sVersionParser, so depup parses and updates four-segment versions like1.2.3.4,^1.0.0.0,~3.4.5.6, and1.0.0.*while rejecting 5+ segment forms as invalid.Note: Composer modifiers may omit the separator or use
./_(composer/semverallows[._-]?), so depup treats5.0.0alpha3,1.0.0.RC1, and1.0.0_beta1as pre-releases and2.2.1p1/2.2.1pl1/2.2.1patch1as patch aliases that sort above the base version. Both forms occur on Packagist today (nikic/php-parserpublishes5.0.0beta1;laminas/laminas-diactorosships security patches as2.2.1p2).Note: Gradle
-SNAPSHOT/.SNAPSHOTversions are excluded from updates. A snapshot is a moving reference that resolves to the newest timestamped build on every resolution, so rewriting it to a fixed release would silently change what the build uses. Stable qualifiers such as.Final,.RELEASE,-jre, and-SP1are still updated.Note: Gradle coordinates declared inside
resolutionStrategy { force ... },constraints { }, anddependencySubstitution { }are not reported as dependencies. They restate a version that is already declared elsewhere, and treating them as separate declarations made the shared coordinate ambiguous and blocked the update.Note: Go versions tagged
+incompatibleare filtered using the same rule thegocommand applies: once a+incompatibleversion is reached in semver order, it and everything above it are discarded if the preceding compatible version has a realgo.mod. Without this, a module likegithub.com/libp2p/go-libp2pwould be "updated" fromv0.49.0to a 2018-erav6.0.23+incompatiblethat still builds successfully.Note: A
pyproject.tomlthat configures a non-PyPI default index — a Poetrypriority = "primary"/"default"source, a uv[[tool.uv.index]] default = trueor[tool.uv] index-url, or a PDM source overridingpypi— has all of its dependencies skipped, with a warning. depup only queries PyPI, so updating those dependencies would replace private packages with same-named public ones.
depup preserves the original version range format:
"^1.2.3" → "^2.0.0" (caret preserved)
"~1.2.3" → "~1.3.0" (tilde preserved)
"~1.2" → "~1.9" (tilde segment count preserved — widening it would narrow the range)
"~1" → "~2" (single-segment tilde keeps its major-level width)
"~1.2 <2.0.0" → "~1.9 <2.0.0" (tilde inside a comparator set keeps its segment count too)
"~1, <5.0" → "~4, <5.0" (Cargo multi-requirement, tilde width preserved)
"~> 7.0" → "~> 8.1" (RubyGems pessimistic operator, segment count preserved)
">=1.0.0" → ">=2.0.0" (range preserved)
"requests (>=2.28,<3); python_version < '3.12'" → "requests (>=2.31,<3); python_version < '3.12'" (PEP 508 parentheses and marker preserved)
"coverage [toml] >=7,<8" → "coverage [toml] >=7.6,<8" (PEP 508 extras spacing preserved)
"'paramiko>=3.5.0,<4.0.0,'" → "'paramiko>=3.9.1,<4.0.0,'" (PEP 508 trailing comma preserved)
"'paramiko>=3.5.0,<4.0.0'" → "'paramiko>=3.9.1,<4.0.0'" (TOML literal string quote preserved)
"1.x" → "2.x" (wildcard shape preserved)
"1.2.x - 2.3.x" → "1.9.x - 2.3.x" (npm hyphen range with x-range endpoints)
"1.x.x" → "2.x.x" (all wildcard positions preserved)
"1.2.*" → "1.3.*" (wildcard shape preserved)
"v1.*" → "v2.*" (leading `v` preserved)
"V1.*" → "V2.*" (Composer uppercase `V` preserved)
"^1.x" → "^2.x" (npm caret + x-range, operator preserved)
"~1.2.x" → "~2.3.x" (npm tilde + x-range, operator preserved)
"=1.x" → "=2.x" (npm equality + x-range, operator preserved)
"=1.2" → "=2.3" (npm partial comparator, operator preserved)
"5.3.+" → "5.4.+" (Gradle prefix preserved)
"5.3.+!!" → "6.1.+!!" (Gradle strict dynamic prefix preserved)
"1.2.3!!" → "2.0.0!!" (Gradle strict preserved)
"[1.7, 1.8[!!" → "[1.7.36, 1.8[!!" (Gradle strict range without a preferred version)
"[1.0]" → "[2.0]" (Maven Hard requirement preserved)
"[1.2.3.Final]" → "[1.3.0]" (Maven Hard requirement with qualifier)
group = "com.google.guava", name = "guava", version = "32.1.2-jre" → version = "33.4.0-jre" (Gradle Kotlin map notation)
junit = "junit:junit:4.13.2" → "junit:junit:4.13.3" (Gradle version catalog library)
guava = "32.1.2-jre" → "33.4.0-jre" (Gradle version catalog version reference)
prefer("1.7.25") → prefer("1.7.36") (Gradle rich version inside a strict range)
"org.slf4j:slf4j-api:[1.7, 1.8[!!1.7.25" → "org.slf4j:slf4j-api:[1.7, 1.8[!!1.7.36" (Gradle strict range shorthand with prefer)
"group:name:1.0.0:classifier@zip" → "group:name:1.1.0:classifier@zip" (Gradle classifier/extension preserved)
Floating selectors such as "*", npm dist-tags like "latest", and Gradle dynamic selectors ("latest.release", "latest.integration", "latest.milestone", and any user-defined latest.<status>) are skipped to avoid changing them into exact versions. Multi-segment fully-floating wildcards without a numeric anchor (Composer's *.*, v*, V*, x.x) and empty Maven ranges ([,], (,)) are also skipped to prevent phantom updates or "always outdated" misjudgements. Wildcard tokens (x/X/*) followed by numeric segments (1.x.3, ^x.0.0) are rejected at parse time because they are invalid x-ranges in node-semver / semver and would produce malformed output.
For npm semver tokens, depup validates prerelease and build metadata identifiers before parsing them as updateable constraints. Identifiers with underscores (1.2.3-rc_1), empty identifier segments (1.2.3-alpha..1), and numeric prerelease identifiers with leading zeroes (1.2.3-01) are skipped instead of being normalized into malformed package.json constraints.
Build metadata is stripped before the leading-zero prerelease check. SemVer allows build identifiers to contain hyphens and leading zeroes, so versions such as 1.0.0+2024-01 and 1.2.3+00 remain valid and updateable — only the prerelease segment is validated.
For npm partial comparators, =1.2 and =1 follow node-semver's partial-version rules instead of being treated as pinned exact versions. depup keeps the = operator and updates only the visible segment shape (=1.2 → =2.3, =1 → =2).
Version candidates are ordered with ecosystem-specific rules. Node.js, Rust, Go, and Swift use SemVer (including numeric prereleases such as 1.0.0-1) and ignore build metadata when comparing precedence, so 1.1.3 and 1.1.3+spec-1.1.0 do not trigger a metadata-only update. Python uses PEP 440 normalization; Ruby follows RubyGems segment ordering and treats alphabetic or hyphenated versions as prereleases; Composer patch aliases (-p1, -pl1, -patch1) sort after the corresponding release; and Java uses Gradle's documented version ordering. Numeric components are compared without a fixed integer-size limit.
Node.js also accepts the node-semver-compatible legacy tilde spelling ~>1.2.3 and preserves ~> when updating. Composer accepts explicit equality (=1.2.3, ==1.2.3) while preserving the operator. Its <>1.2.3 exclusion spelling is parsed but intentionally not rewritten.
Gradle rich version declarations using strictly, require, prefer, and reject are parsed in dependency blocks such as implementation("org.slf4j:slf4j-api") { version { ... } }. String notation shorthand supports exact, dynamic-prefix, and range constraints, including group:name:1.2.3!!, group:name:5.3.+!!, group:name:[1.7, 1.8[!!, and a strict range with a preferred version such as group:name:[1.7, 1.8[!!1.7.25. When strictly or require declares a range and prefer declares the selected version, depup keeps the range as the upper-bound constraint and updates the prefer value. Versions listed with reject are excluded from update candidates, including dynamic rejects such as 2.+ and ranges such as [1.5,1.9).
Gradle declaration wrappers are supported: platform(...), enforcedPlatform(...), and testFixtures(...). BOM declarations such as implementation platform('com.google.cloud:libraries-bom:26.1.0') and testImplementation(platform("org.junit:junit-bom:5.10.0")) are parsed and updated, and the surrounding configuration name is still used for the dev/production classification. Gradle variables declared with ext.<name> = '...' / project.ext.<name> = "..." are resolved alongside ext { ... } blocks, and qualified references such as ${Versions.retrofit} resolve by their final segment. A short name defined more than once with different values is left untouched to avoid picking the wrong object's value.
Gradle version catalogs under gradle/*.versions.toml are detected as Java manifests. depup parses [libraries] entries written as alias = "group:name:version", module = "group:name", group / name / version, and version.ref; referenced [versions] entries are updated in place. Rich version tables with strictly, require, prefer, reject, and rejectAll follow the same candidate rules as Gradle build files. [plugins] entries are skipped because Gradle plugin IDs are not Maven Central coordinates.
Python compatible release clauses follow PEP 440: ~=1.2 (= >=1.2,<2.0) and ~=1.2.3 (= >=1.2.3,<1.3.0) are treated as ranges with an explicit upper bound, so updates stay within the compatible range (~=1.2.3 stays in the 1.2 series, ~=1.2 stays in the 1.x series) and preserve the original segment count (~=1.2 → ~=1.9). The invalid single-segment form ~=1 is skipped.
PEP 440 prefix matching is accepted only for release-segment == / != specifiers such as ==1.2.* and !=1.2.*. Invalid prefix forms such as >=1.0.*, ~=1.0.*, ==1.0a1.*, ==1.0.post1.*, and ==1.0+local.* are skipped at parse time. Arbitrary equality (===1.0.*) remains an exact pinned specifier, not prefix matching.
JVM milestone releases are treated as prereleases and excluded from stable candidates: 4.0.0-M1, the legacy Spring Boot dot form 2.0.0.M1, and the spelled-out -milestone1. Without this, a project on assertj-core 3.24.2 would be bumped to 4.0.0-M1, junit-bom 5.10.0 to 5.13.0-M3, and spring-core 5.3.23 to 7.0.0-M6. Detection is limited to tokens where m is immediately followed by digits, so stable JVM qualifiers (.Final, -jre, -android, .RELEASE, .GA, -SP1) and identifiers like -macos1 are never misclassified. As with other prereleases, a project already on a milestone keeps milestone candidates so it can advance to the next one.
PEP 440 prerelease versions are detected and excluded by default even when written without a separator (e.g. 2.0.0rc1, 1.0rc1, 1.0.0a1), so a stable dependency is never accidentally bumped to a release candidate. Post-releases (1.0.post1) compare as newer than the corresponding release, and epochs (1!2.3) take precedence in comparison. A post-release attached to a prerelease (1.0a1.post1) is also recognised as newer than the underlying prerelease (1.0a1 < 1.0a1.post1 < 1.0), so users tracking an alpha can still pick up its post-release fixes.
PEP 440 local versions are handled as Python-specific version semantics rather than semver build metadata. Exact and exclusion specifiers preserve local labels (==1.0+cu121, !=1.0+local1), Python candidate ordering treats local versions as newer than the same public version (1.0+local > 1.0) and compares local segments per PEP 440 (1.0+1 > 1.0+abc, 1.0+abc.2 > 1.0+abc.1). Ordered and compatible specifiers with local labels, which PEP 440 does not permit (>=1.0+local, ~=1.0+local, >=1.0+local,<2.0), are skipped.
In Poetry's [tool.poetry.dependencies], a bare version string without an operator (requests = "2.28.0") is an exact pin — Poetry's official "Exact requirements", equivalent to ==2.28.0. depup parses it as an exact/pinned dependency for both the simple form and inline tables ({ version = "1.26.0", optional = true }), so it surfaces just like an explicit ==2.28.0 (shown as pinned, updatable with --include-pinned) and is rewritten without adding an operator (4.2.1 → 5.0.0). This bare-exact interpretation applies only in the Poetry context; pip / PEP 508 requirements still require an operator, so a bare version there is not accepted.
depup respects upper-bound range constraints (both exclusive and inclusive):
">=3.5.0,<4.0.0" → ">=3.9.1,<4.0.0"
">=1.0,<=2.0" → ">=2.0,<=2.0"
"4.0.0..<5.0.0" → "4.99.0..<5.0.0"
"4.0.0...4.9.9" → "4.9.9...4.9.9"
"1.2.0 - 2.0.0" → "1.9.3 - 2.0.0" (npm hyphen)
"1.0 - 2.0" → "2.0.9 - 2.0" (npm/Composer partial upper expands to `<2.1`)
"[1.0,2.0)" → "[1.9.3,2.0)" (Maven-style)
"[1.0,2.0]" → "[2.0,2.0]" (Maven-style)
"[1.0,2.0.Final)" → "[1.9.3,2.0.Final)" (Maven qualifier)
"[1.0,2.0-beta1-SNAPSHOT)" → "[1.9.3,2.0-beta1-SNAPSHOT)" (multi-part Maven qualifier)
"[1.0,2.0[" → "[1.9.3,2.0[" (Maven alt upper bracket)
"<4.0.0" → skipped (upper-bound only)
">1.0.0" → skipped (exclusive lower bound)
"]1.0,2.0[" → skipped (exclusive Maven lower bound)
For npm/Composer hyphen ranges, a partial right-hand side like 1.0 - 2.0 is interpreted as a wildcard-expanded exclusive upper bound, so 2.0.x updates remain eligible while 2.1.0 is excluded.
node-semver's HYPHENRANGE accepts XRANGEPLAIN on both sides, so x-range endpoints such as 1.x - 2.x are valid and updateable. Endpoints that depup rejects elsewhere (a digit after a wildcard like 1.x.3, or a fully floating *) stay rejected.
Composer rejects the ~> operator (Invalid operator "~>"), so depup skips it for PHP rather than writing back a constraint Composer cannot read. Node accepts ~> because node-semver defines it as valid.
Cargo dependencies carrying registry-index = "..." are excluded from updates just like registry = "...", since both point at a registry other than crates.io.
When a dependency has a range with an upper bound (e.g., >=3.5.0,<4.0.0, >=1.0,<=2.0, 4.0.0...4.9.9), depup will:
- Not propose versions that exceed the upper bound
- Keep inclusive boundaries (
<=,...) eligible - Preserve the original constraint shape in the manifest file
- Update only the lower-bound side to the newest compatible version within the range
Constraints that cannot be rewritten safely are skipped instead of being rewritten partially. This includes examples such as npm/Composer OR constraints (^1 || ^2) and Composer's backward-compatible single-pipe spelling (^1 | ^2), any exclusion constraint containing != (!=1.2.3, >=1.0, !=1.5.0, <2.0), upper-bound-only constraints (<4.0.0, <=2.0), strict lower bounds (>1.0.0), Maven-style ranges without a lower bound ((,2.0]), and Maven ranges with an exclusive lower bound (]1.0,2.0[). Composer (composer/semver) also spells not-equal as <>, so exclusion constraints using it (>=1.0 <>1.5.0 <2.0, >=1.0,<>1.5.0,<2.0) are skipped just like their != equivalents.
For JSON manifests, depup only rewrites dependency sections it parses. In package.json, overrides is left untouched; in composer.json, sections such as replace, provide, and conflict are left untouched.
If the same dependency key is declared more than once in a manifest, or multiple dependencies share one Gradle version variable or version-catalog version.ref, depup refuses the ambiguous write instead of changing skipped or pinned declarations implicitly.
For Bun workspaces, depup parses and updates root package.json catalog definitions in both top-level catalog / catalogs and workspaces.catalog / workspaces.catalogs. Workspace package references such as "react": "catalog:" and "jest": "catalog:testing" are kept as catalog references; depup updates the shared catalog entry instead. pnpm catalogs defined in pnpm-workspace.yaml are not yet parsed as manifests, so package.json catalog: references backed by pnpm catalogs are skipped safely.
For TOML manifests, depup preserves both basic strings ("...") and literal strings ('...') when updating supported dependency sections. In Cargo.toml, dependency updates are limited to dependency tables such as [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], and target-specific dependency tables; metadata tables are left untouched. Cargo git tag updates use the same section scoping for inline tables and multiline tables, preserve single or double quotes, and also support [patch.<registry>] / [patch.<registry>.<package>]. Cargo dependencies that specify a non-crates-io registry are skipped because depup only queries crates.io. Cargo comparison ranges may contain more than two comma-separated requirements, for example >=1.0, <2.0, >=1.0.100. Mixed multi-requirement constraints that combine caret/tilde/wildcard with comparators, such as ^1.2.2, <1.5, are validated with semver::VersionReq and detected as ranges; constraints without an upper bound that cannot be rewritten safely are skipped instead of being silently dropped.
Beyond PEP 621, PEP 735, and Poetry, depup also reads uv's legacy [tool.uv] dev-dependencies and PDM's [tool.pdm.dev-dependencies]. Dependencies routed through [tool.uv.sources] to a non-PyPI source (workspace = true, git, path, url, or an index other than pypi) are skipped so a workspace member or custom index is never overwritten with a same-named PyPI package. Poetry multiline dependency tables ([tool.poetry.dependencies.<name>], [tool.poetry.group.<g>.dependencies.<name>]) and TOML quoted keys ("zope.interface", "ruamel.yaml") are parsed and updated too — names containing a dot require quoting in TOML, so the writer must cover the same range the parser reads.
In [project], [tool.rye], and [tool.uv] sections, depup rewrites only the dependencies / dev-dependencies arrays; metadata strings such as name, description, and keywords are never modified even if they look like PEP 508 specifiers. Python PEP 508 version lists may include a trailing comma, such as >=3.5,<4,; depup parses and preserves that comma when updating the lower bound. Poetry dependencies with a non-pypi source are skipped, including PEP 621 dependencies enriched by tool.poetry.dependencies, because depup only queries PyPI. Poetry's multiple-constraints array form (foo = [{version = "<=1.9", python = ">=3.6,<3.8"}, {version = "^2.0", python = ">=3.8"}]) is also skipped, because depup cannot safely rewrite an individual array element without per-element requires_python resolution.
For Gradle string notation, depup preserves classifier and extension suffixes such as :resources@zip or @aar, and skips dependencies that appear only in // line comments or /* ... */ block comments. Gradle version catalog updates preserve the original TOML string or table shape where the version is declared.
For npm comparator sets, depup supports bare partial lower bounds such as 1.2 <2.0.0 and preserves the partial shape when updating the lower side.
For Swift GitHub dependencies, depup accepts HTTPS URLs, scp-style SSH URLs (git@github.com:owner/repo.git), standard SSH URLs (ssh://git@github.com/owner/repo.git), and GitHub's SSH-over-443 URLs (ssh://git@ssh.github.com:443/owner/repo.git). It recognizes both v1.2.3 and V1.2.3 tag prefixes from GitHub tags, while Package.swift version requirement strings are validated as strict SemVer (X.Y.Z, no leading zeroes). depup also skips Package.swift dependencies that appear inside // line comments or /* ... */ block comments.
Per the SPM semver 2.0.0 specification, depup parses and updates dependencies that include prerelease identifiers (1.0.0-beta.1) and build metadata (1.0.0+build.123), including combined forms (1.0.0-rc.1+sha.abc).
depup also parses .package(...) declarations with trailing arguments such as traits: [...] (SPM 6.1) or moduleAliases: [...], updating only the version requirement while preserving the extra arguments. Swift Package Registry id: dependencies (.package(id: "scope.name", ...)) are not yet supported and are skipped; only GitHub URL dependencies are processed.
For go.mod, depup treats block endings with trailing comments such as ) // direct deps as normal block endings when parsing and updating require, replace, and exclude blocks.
Quoted go.mod module paths and versions, such as require "golang.org/x/text" "v0.14.0", are parsed and updated while preserving the quotes.
go.mod updates preserve the original LF or CRLF line endings in both single-line and block require declarations.
Tool versions declared in mise config files go through the same workflow as every other language (detect → parse → judge → write). Version lists come from mise ls-remote <tool> --json, so every backend mise supports (core / aqua / ubi / asdf / npm: / cargo: / go: / pipx: …) works out of the box.
mise.toml, .mise.toml, mise/config.toml, .mise/config.toml, .config/mise.toml, .config/mise/config.toml, .tool-versions
mise.local.toml (personal local override, usually gitignored) and mise.<env>.toml (per-environment overlay) are intentionally left alone so depup never rewrites a single environment behind your back.
| Form | Example | Handling |
|---|---|---|
| Exact | node = "26.7.0" |
Updated to the latest version |
| Prefix | node = "26" / "26.7" |
Segment count preserved (26 → 27, 26.7 → 26.8) |
| Explicit selector | go = "prefix:1.19" |
prefix: preserved (prefix:1.24) |
| Vendored | java = "temurin-21.0.5" |
Stays within the same vendor (temurin-21.0.9) |
| Inline table | python = { version = "3.13", virtualenv = ".venv" } |
Only version is rewritten; other options are preserved |
| Table form | [tools.terraform] + version = "1.15.0" |
Same as above |
| Floating | latest / lts / system |
Left as-is — updating would not change the meaning |
| Non-version | ref:master / path:./shfmt / sub-2:lts |
Left as-is |
| Multiple versions | python = ["3.12", "3.13"] |
Skipped — there is no single version to rewrite |
Most of the 3000+ entries mise ls-remote java returns carry a vendor prefix (temurin-, graalvm-community-, zulu-, …). depup only considers candidates with the same prefix as the current value, so a project on temurin-21 is never rewritten to zulu-27.
Only the [tools] section is rewritten; identically named keys under [settings], [env], [tasks], or [alias] are untouched. Quote style (" / '), trailing comments, CRLF line endings, and the column alignment of .tool-versions are all preserved.
When [settings] minimum_release_age is explicitly set, depup treats it as a project policy — the same way it treats pnpm's and bun's minimumReleaseAge — and it takes precedence over the CLI --age (mise's built-in 24h default counts as "unset" and is not adopted).
[settings]
minimum_release_age = "7d" # s / m (minutes) / h / d / w / M / yNote: mise's
mmeans minutes (humantime convention), unlike depup's--age 1m, which means one month.
mise ls-remote applies mise's own minimum_release_age by default and hides newer releases, so depup passes --minimum-release-age 0 to fetch everything and keeps age evaluation in one place. minimum_release_age_excludes is not interpreted by depup; if it is set, depup prints a warning.
With --install, depup runs mise install and passes the resolved age as MISE_MINIMUM_RELEASE_AGE.
The --age option ensures stability by only updating to versions that have been released for a certain period. A 1 week (1w) age filter is applied by default to all runs unless overridden:
# Default — implicit --age 1w
depup
# Only update to versions at least 2 weeks old
depup --age 2w
# Only update to versions at least 10 days old
depup --age 10d
# Only update to versions at least 1 month old
depup --age 1m
# Disable the age filter for this run
depup --no-agedepup auto-generates ~/.config/depup/config.toml on first run with commented defaults. Edit it to change depup's defaults globally:
# ~/.config/depup/config.toml
# Apply this age filter by default to every depup run.
# Accepts the same format as --age (Nd / Nw / Nm). Omit to use the built-in default (1w).
age = "1w"
# Check candidates against the OSV.dev vulnerability database (built-in default: true).
# Set to false to opt out of the check on every run.
osv = truePriority order (highest first):
--no-age(disables age entirely)--age <DURATION>CLI flag~/.config/depup/config.tomlagevalue- Built-in default (
1w)
minimumReleaseAge declared in the project is always treated as the project policy and takes precedence over any CLI / config age value. The full resolution order is:
- Project
minimumReleaseAge(highest — see sources below; takes the stricter value when multiple sources disagree) - CLI
--age <DURATION> --no-age(only effective if no project policy is set)~/.config/depup/config.tomlagevalue- Built-in default
1w
When a project policy overrides the CLI value, depup prints a yellow warning so the active source is visible:
⚠ --age ignored: project's minimumReleaseAge (14 days from pnpm-workspace.yaml) takes precedence
To bypass the project setting, remove or override it in the project file.
pnpm (any of the following — first non-empty value wins):
.npmrc(minimum-release-age=10d)pnpm-workspace.yaml(minimumReleaseAge: 14400in minutes)package.json(pnpm.settings.minimumReleaseAge)
bun (bunfig.toml):
[install]
minimumReleaseAge = 259200 # seconds (e.g. 3 days)mise ([settings] in mise.toml and friends):
[settings]
minimum_release_age = "7d" # s / m (minutes) / h / d / w / M / yWhen several sources exist, depup uses the stricter (larger) value.
With --install, depup audits the crates whose locked version the install actually changed, and rolls back any that violate the age policy:
Auditing transitive Rust dependencies [██████▒▒▒▒] 18/24 (Auditing hyper)
. — 13 transitive dep(s) rolled back to satisfy --age:
hyper 1.11.1 → 1.11.0
Only changed entries are audited. crates.io serves one request per second, so walking an entire lockfile (hundreds of crates) would stall the run for several minutes with nothing on screen. If Cargo.lock did not exist before the install, every entry counts as new — the audit is then capped at 180 seconds and whatever is left over is reported as unchecked without failing the run.
The GitHub Tags API does not return per-tag release timestamps, so Swift packages are exempt from the --age filter (they are always eligible for updates regardless of the cutoff).
Use --max-change <LEVEL> to cap how aggressive depup is about a bump:
# Only allow patch bumps (1.0.0 → 1.0.5 OK, 1.0.0 → 1.1.0 NG)
depup --max-change patch
# Allow patch + minor (1.0.0 → 1.5.3 OK, 1.0.0 → 2.0.0 NG)
depup --max-change minor
# Default — allow all bumps including major
depup --max-change majorWhen a newer candidate exists but exceeds the cap, that dependency is skipped with reason max-change=<LEVEL> instead of being updated. The same cap also applies to Cargo git dependencies that track a tag.
Set a default cap in ~/.config/depup/config.toml:
# Limit every depup run to patch + minor by default
max_change = "minor"Priority order (highest first):
--max-change <LEVEL>CLI flag~/.config/depup/config.tomlmax_change- Built-in default (no cap)
depup queries the public OSV.dev database for each candidate version and skips versions with known vulnerabilities. This check is enabled by default — no flag required. Combined with the age filter, depup naturally falls back to the next safe, mature version:
# OSV check runs by default
depup
# Same as above (explicit opt-in, overrides `osv = false` in global config)
depup --osv
# Disable OSV check for this run (overrides global config and default)
depup --no-osv- The OSV.dev API is public and does not require any authentication token.
- Swift packages are skipped — OSV indexes packages by GitHub repository URL rather than by GitHub Tags–style names, so Swift queries from depup would not match.
- mise tools are skipped — each backend has its own version scheme and namespace, so they cannot be mapped onto a single OSV ecosystem.
- API errors do not block updates; affected versions remain in the candidate list and the failure is reported in
--verbose.
When a candidate version has a known vulnerability, depup automatically falls back to the next older safe version. Updates that pass the OSV check are marked with ✓ OSV:
$ depup --install --include-pinned
⚠ OSV: dompurify 3.4.8 vulnerable (GHSA-vxr8-fq34-vvx9)
./package.json (Node.js) — 9 updates, 41 skips
@mui/icons-material 9.0.1 → 9.1.0 [minor] (2026/06/08 08:30) ✓ OSV
@mui/material 9.0.1 → 9.1.0 [minor] (2026/06/08 08:29) ✓ OSV
@tanstack/react-query 5.100.14 → 5.101.0 [minor] (2026/06/02 19:24) ✓ OSV
next 16.2.6 → 16.2.9 [patch] (2026/06/09 23:02) ✓ OSV
openai 6.39.1 → 6.42.0 [minor] (2026/06/03 22:39) ✓ OSV
react 19.2.6 → 19.2.7 [patch] (2026/06/01 18:00) ✓ OSV
react-dom 19.2.6 → 19.2.7 [patch] (2026/06/01 18:01) ✓ OSV
@types/node 25.9.1 → 25.9.2 [patch] (2026/06/05 22:33) ✓ OSV 🔧
@types/react 19.2.15 → 19.2.17 [patch] (2026/06/05 20:10) ✓ OSV 🔧
Errors:
✗ OSV check for dompurify: 3.4.8 vulnerable, falling back (GHSA-vxr8-fq34-vvx9)
Summary:
9 package(s) updated (4 minor, 5 patch)
41 package(s) skipped
The falling back message is an expected notification of safe behavior and does not affect the exit code.
The check is on by default. Opt out permanently in the auto-generated ~/.config/depup/config.toml:
# ~/.config/depup/config.toml
# Skip the OSV lookup on every run (built-in default is true).
osv = falsePriority order (highest first):
--no-osv(disables the check)--osvCLI flag~/.config/depup/config.tomlosvvalue- Built-in default (
true— OSV check enabled)
If the config file is missing, or it has no osv key, the built-in default applies and the check runs.
When --install triggers uv sync for a Python project, depup always sets UV_MALWARE_CHECK=1 in the environment. This enables uv's preview malware check — a feature announced alongside uv audit but distinct from the uv audit command. On every sync operation (uv add, uv sync, etc.), uv cross-references the currently locked resolution against OSV's MAL advisories and terminates the sync before any malicious package is installed.
- Always on — no opt-in flag required.
- Older uv releases that pre-date the feature simply do not act on the variable, so enabling it unconditionally does not break existing builds.
- Astral marks the feature as preview, so its exact behavior may change. The check runs inside uv, and uv's exit code propagates through depup. A malware match terminates the sync with a uv-side error message.
When --install processes a PHP project, depup runs composer update rather than composer install. composer install reuses the existing lock file and cannot reflect constraints that depup has just changed in composer.json; composer update resolves those constraints and refreshes composer.lock.
🔧indicates devDependencies- Release date shown in
(yyyy/mm/dd HH:MM)format - Change type:
[major],[minor],[patch]
depup --json{
"manifests": [
{
"path": "package.json",
"language": "node",
"updates": [
{
"type": "update",
"dependency": {
"name": "lodash",
"version_spec": "^4.17.20"
},
"new_version": "4.17.21",
"released_at": "2024-12-15T10:30:00Z"
}
]
}
]
}depup --diff--- package.json
+++ package.json
@@ dependencies @@
- "lodash": "^4.17.20"
+ "lodash": "^4.17.21"For monorepo projects with multiple subdirectories, create a .depup file at the project root to list additional directories to process:
# .depup
gui # Frontend app
api # Backend API
shared # Shared libraries
Run depup from the root directory to update dependencies across all listed directories at once. The root directory itself is always scanned in addition to the listed directories. Version lookups are cached, so shared packages are only fetched once.
When --install is used, depup runs the package manager in the nearest matching monorepo directory for each updated manifest, so nested apps install in their own directories instead of the repository root.
#starts a comment (line or inline)- Empty lines are ignored
- Paths are relative to the
.depupfile location - Absolute paths, parent-directory (
..) traversal, and symlinks resolving outside the.depupdirectory are rejected - Non-existent directories are warned and skipped
- The root directory is always included as a scan target
depup detects pnpm-workspace.yaml and processes all workspace packages. Both block-style (- 'packages/*') and flow-style (packages: ['packages/*', 'apps/*']) packages arrays are supported, including negation patterns (!packages/legacy).
depup expands [workspace] members (including glob patterns like crates/*) and skips entries listed in [workspace] exclude.
depup expands the use directives in go.work (both the single-line and use ( ... ) block forms) and processes each member module's go.mod. Without this, a repository whose root has no go.mod would report "no updates" even though every member has outdated dependencies.
depup expands include ':app', ':core' from settings.gradle / settings.gradle.kts (both the Groovy and Kotlin DSL forms) and processes each subproject's build.gradle / build.gradle.kts, plus buildSrc/. Most dependency declarations live in subprojects, so scanning only the root build file would miss them.
Expanded paths are subject to the same containment checks as .depup: absolute paths, .. traversal, and symlinks resolving outside the project are rejected.
depup automatically detects src-tauri/Cargo.toml in Tauri projects.
Tauri projects require the npm @tauri-apps/api package and the Rust tauri crate to have matching major/minor versions. depup automatically synchronizes these versions to prevent build errors.
# Error example (version mismatch)
Found version mismatched Tauri packages:
tauri (v2.10.1) : @tauri-apps/api (v2.9.1)
# depup automatically synchronizes versions
@tauri-apps/api: 2.9.1 → 2.10.0
tauri: 2.9.0 → 2.10.1
Both packages are automatically adjusted to the same major.minor version (e.g., 2.10.x).
# Debug build
cargo build
# Release build
cargo build --release
# Run tests
cargo test
# Install locally
cargo install --path .Contributions are welcome! Please feel free to submit a Pull Request.



