deps-cli 2.0.0

CLI for running deps-lsp's dependency-health checks in CI, pre-commit, and shell workflows
Documentation

deps-cli

Crates.io docs.rs CI codecov License: MIT

Run deps-lsp's dependency-health checks from the command line, in CI, or in a shell script — no editor required.

This crate is part of the deps-lsp workspace. deps-cli check walks a workspace, routes every manifest it finds through the same 14-ecosystem classification pipeline (deps-engine) that powers deps-lsp's hover and diagnostics, and reports outdated/yanked/vulnerable/unsatisfiable/deprecated/license/mutable-ref-pin findings as a table, as JSON, or as SARIF 2.1.0, with a CI-friendly exit code. deps-cli update is the write-back counterpart: it plans and applies version-requirement edits to one manifest.

[!IMPORTANT] deps-cli implements no classification logic of its own — every verdict comes from the exact function deps-lsp calls for its LSP diagnostics, so a deps-cli check result and an editor's diagnostics for the same manifest never disagree.

Features

  • Security-hardened workspace walk — routes every discovered file through the same EcosystemRegistry the LSP server uses, across all 14 supported ecosystems (Cargo, npm, PyPI, Go, Bundler, Dart, Maven, Gradle, Swift, Composer, NuGet, Deno, GitHub Actions, GitLab CI/CD). check does not honor .gitignore/.ignore by default, because in a CI gate (git checkout && deps-cli check . against an untrusted fork PR) both files are attacker-controlled input — a one-line addition to either would otherwise silently remove a manifest from the scan with no warning and exit code 0. A compiled-in denylist (node_modules, target, vendor, .venv, and other common dependency/build/VCS directories) still keeps the scan fast and on-signal without depending on either file. Pass --respect-gitignore to restore standard .gitignore/.ignore awareness when scanning a target you trust as much as your own deps.toml. A manifest reachable only through a symlink is detected and reported the same way regardless of this flag; pass --follow-symlinks to also resolve and scan it, bounded so it can never escape the walked root or loop forever.
  • Table, JSON, or SARIF output — a human-readable table (default) grouped by file and severity, a versioned JSON document for machine consumption, or a SARIF 2.1.0 document for github/codeql-action/upload-sarif and other SARIF consumers
  • Configurable failure policy — --fail-on picks which finding categories make the run exit non-zero, so a pipeline can gate on vulnerabilities without failing on merely-outdated dependencies
  • CI-safe by construction — a deps.toml auto-discovered from the scanned repository itself can never weaken what --fail-on observes or redirect credentials to an attacker host; only an explicitly-passed --config is trusted with the full policy (see Configuration)
  • update subcommand — plans and atomically writes version-requirement edits for one manifest's outdated or OSV-vulnerable dependencies, with --dry-run, --package selection, and table/json output (see update subcommand)
  • OSV-checked upgrade targets — an upgrade target (including a cooldown fallback) is independently OSV-verified before update writes it, and a flagged or unverified one is refused rather than silently substituted
  • Freshness cooldown — update skips a version published within freshness.cooldown_secs (default 3 days) and targets the newest already-cooled-down, verified candidate instead
  • Safe rewrites — an unresolved version placeholder (${VAR}, $(VAR), %VAR%, self.version, string interpolation) is never overwritten by a rewrite
  • Offline mode — --offline serves only already-cached registry data, useful for air-gapped CI runners or fast local iteration

Installation

From crates.io

cargo install deps-cli

[!TIP] Use cargo binstall deps-cli for faster installation without compilation.

Install script

curl -fsSL https://raw.githubusercontent.com/bug-ops/deps-lsp/main/scripts/install-deps-cli.sh | sh

Detects your OS and CPU architecture, downloads the matching release archive, verifies its SHA256 checksum, and installs deps-cli to ${CARGO_HOME:-~/.cargo}/bin (falling back to ~/.local/bin) — no Rust toolchain required. Pin a release with --tag <version> (or the DEPS_CLI_VERSION env var); override the install directory with --install-dir <dir> (or DEPS_CLI_INSTALL_DIR).

[!IMPORTANT] Windows is not supported by the script — download the .zip release asset from the table below instead.

Pre-built binaries

Download from GitHub Releases:

Platform Architecture Binary
Linux x86_64 (glibc) deps-cli-x86_64-unknown-linux-gnu
Linux aarch64 (glibc) deps-cli-aarch64-unknown-linux-gnu
Linux x86_64 (musl) deps-cli-x86_64-unknown-linux-musl
Linux aarch64 (musl) deps-cli-aarch64-unknown-linux-musl
macOS x86_64 deps-cli-x86_64-apple-darwin
macOS Apple Silicon deps-cli-aarch64-apple-darwin
Windows x86_64 deps-cli-x86_64-pc-windows-msvc.exe
Windows ARM64 deps-cli-aarch64-pc-windows-msvc.exe

Docker

The GitHub Action image (ghcr.io/bug-ops/deps-lsp-github-action) also bundles a prebuilt deps-cli binary, SHA256-verified against the matching GitHub release — no Rust toolchain, and nothing to trust beyond the image itself. Its default entrypoint is fixed to the Action's own check --format sarif contract, so override it to run deps-cli directly:

docker run --rm -v "$PWD:/workspace" -w /workspace \
  --entrypoint deps-cli ghcr.io/bug-ops/deps-lsp-github-action:1 check

linux/amd64 and linux/arm64 only. Tags: latest, X, X.Y, X.Y.Z — see Image tags.

From source

git clone https://github.com/bug-ops/deps-lsp
cd deps-lsp
cargo install --path crates/deps-cli

As a workspace dependency

To drive the check pipeline programmatically:

[dependencies]
deps-cli = "2.0"

[!IMPORTANT] Requires Rust 1.98 or later.

Usage

# Check the current directory, human-readable table output (default)
deps-cli check

# Check specific paths
deps-cli check Cargo.toml package.json services/api/

# Fail the run only on real vulnerabilities and unsatisfiable requirements
deps-cli check --fail-on vulnerable,unsatisfiable

# Machine-readable output for a CI step that parses results
deps-cli check --format json

# SARIF 2.1.0 output for github/codeql-action/upload-sarif
deps-cli check --format sarif > results.sarif

# CI-friendly: never touch the network, use only what's already cached
deps-cli check --offline

# Loosen the freshness window for this run only
deps-cli check --cooldown 3d

# Use an explicit, fully-trusted config file
deps-cli check --config ./ci/deps-strict.toml

# Restore .gitignore/.ignore awareness (only for a fully-trusted scan target — see Features)
deps-cli check --respect-gitignore

# Resolve and scan a symlinked manifest (only for a fully-trusted scan target — see Features)
deps-cli check --follow-symlinks

Exit codes

CI pipelines script against the exit code directly:

Code Meaning
0 Clean — no finding matched the --fail-on policy
1 Policy violation — at least one finding matched --fail-on
2 Execution error — a registry was unreachable, a deps.toml/manifest failed to parse, or a walked path was unreadable

A real policy violation (1) always takes precedence over an unrelated execution error elsewhere in the run — one malformed manifest in a large workspace never hides a genuine --fail-on hit behind a less specific 2.

--fail-on categories

outdated, yanked, vulnerable, unsatisfiable, mutable-ref, license, deprecated. Defaults to vulnerable,yanked,unsatisfiable when the flag is omitted. A finding that matches none of these seven categories (for example, an unresolved or unknown package) is always reported in the output but can never fail a run through this flag.

update subcommand

deps-cli update <MANIFEST> reads exactly one manifest, plans version-requirement edits for its outdated (default mode) or OSV-vulnerable (--security-only) dependencies, and writes them back atomically — the non-interactive counterpart to deps-lsp's "update all outdated" code lens and vulnerability-fix quick action:

# Update every outdated dependency
deps-cli update Cargo.toml

# Only the named dependencies (repeatable)
deps-cli update --package serde --package tokio Cargo.toml

# Only OSV-vulnerable dependencies, targeting each advisory's own recommended fix
deps-cli update --security-only Cargo.toml

# Plan without writing, inspecting the machine-readable result
deps-cli update --dry-run --format json Cargo.toml

# Honor [update].ignore rules from an explicit config, using cached registry data only
deps-cli update --config ./ci/deps-strict.toml --offline Cargo.toml

Flags: --package <NAME> (repeatable), --security-only, --dry-run, --format table|json, --config <PATH>, --offline, and --cooldown <DURATION> (--cooldown has no effect under --security-only).

  • Default mode honors the freshness cooldown (default 3 days) even with no flags: a version published inside the window is replaced by the newest already-cooled-down, OSV-verified candidate that does not drop below the in-use version (or, with no lock file, the declared requirement), or the dependency is skipped when none exists. A flagged or unverified candidate is refused and the run exits non-zero.
  • --security-only rewrites to each advisory's recommended fix, independently re-verified against OSV, and classifies every vulnerable dependency as applied, requires-lockfile-update, or unfixable. It overrides every [update].ignore rule.
  • [update].ignore ({ name = "tokio", update_types = ["major"] }) is honored only from an explicit --config <path> — update never auto-discovers a deps.toml.
  • Exit codes: 0 all selected updates applied or deliberately skipped, 1 at least one wanted fix could not be applied, 2 execution error. A non-zero exit does not imply the manifest is unmodified; inspect each item's outcome in --format json.

See the update usage mdBook section for the full reference.

Pre-commit hook

This repository ships a .pre-commit-hooks.yaml at its root defining a deps-lsp-check hook (language: system, entry: deps-cli check), per FR-017.

[!NOTE] language: system means pre-commit does not install anything for this hook — deps-cli must already be on your PATH. The fastest way to get it there is the install script (curl -fsSL .../install-deps-cli.sh | sh); cargo install deps-cli and cargo install --path crates/deps-cli from a checkout also work. An earlier language: rust hook definition could not install from this repository's root at all, since it is a virtual workspace manifest (no [package]) — see #1074.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/bug-ops/deps-lsp
    rev: <tag>
    hooks:
      - id: deps-lsp-check

GitHub Action

crates/github-action wraps deps-cli check --format sarif as a Docker-based action. It writes a SARIF file but does not upload it — wire github/codeql-action/upload-sarif after it in your own workflow. Pin uses: to a released tag rather than @main — see the action's own README for why:

- uses: bug-ops/deps-lsp/crates/github-action@v2.0.0
  id: deps-check
  with:
    fail-on: vulnerable,yanked,unsatisfiable
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: ${{ steps.deps-check.outputs.sarif-file }}
- name: Fail the build on a policy violation
  if: steps.deps-check.outputs.exit-code == '1'
  run: exit 1

The action itself only fails the job on an execution error — any exit code other than 0 (clean) or 1 (a --fail-on category matched), including a deps-cli panic or a refused SARIF output path. A --fail-on policy violation (exit-code 1) does not fail the step; sarif-file is usually still produced and uploaded then too, though it may be unset even at exit-code 0 or 1 if the output path couldn't be written. It is your own workflow's decision — shown above — whether to fail the build on a policy violation. See crates/github-action/README.md for the full input/output reference.

[!NOTE] Piping stdout directly to a file, as in deps-cli check --format sarif > results.sarif above, is at risk of truncation if deps-cli crashes mid-write — a partial SARIF file handed to upload-sarif fails confusingly rather than cleanly. The GitHub Action wrapper does not have this risk: it captures deps-cli's exit code before deciding whether sarif-file is set, so a crash leaves sarif-file unset instead of pointing at a truncated file (#1063). Prefer the Action, or apply the same exit-code check yourself, when scripting the CLI form directly in CI.

Configuration

deps-cli reuses deps-lsp's own PolicyConfig schema, loaded from a deps.toml file:

[diagnostics]
vulnerabilities_enabled = true
mutable_ref_pin_enabled = true

[freshness]
cooldown_secs = 259200 # 3 days, Dependabot's default

[gossip]
enabled = false # opt-in deps.dev cooldown signals; explicit --config only

[update]
ignore = [{ name = "tokio", update_types = ["major"] }] # honored only via --config

[network]
offline = false

[license_policy]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]

deps-cli looks for ./deps.toml (relative to the walked root) when --config is not given.

[!WARNING] An auto-discovered deps.toml — one found by this default lookup, not passed explicitly via --config — has its registries, network, gossip, and diagnostics.*_enabled sections (and a few other gate-relevant fields) reset to their safe defaults before use. This is deliberate: the repository a CI job is checking is not a trusted source for the policy that judges it, and a checked-in deps.toml on an attacker-controlled branch must not be able to disable the check or redirect a registry credential to another host. Any section ignored this way is named in a warning on stderr. Only a config path given explicitly via --config (the operator's own choice, not the scanned repository's) is trusted in full.

License

MIT