Skip to Content

packguard scan

Walks a repo, discovers every scannable project recursively, resolves every installed version against the registry, and persists the result into the local SQLite store.

Synopsis

packguard scan [path] [--no-recursive] [--depth N] [--include GLOB]… [--exclude GLOB]… [--dry-run] [--yes] [--offline] [--force]

Behaviour (v0.2.0)

Scan is recursive by default — you point it at any directory, and it finds every scannable project under it. packguard scan on a monorepo root is a single invocation, not one per project.

Discovery happens in two steps:

  1. Marker-driven (fastest, most precise). If the target has a monorepo marker, PackGuard reads the declared workspaces and scans only those:

    • pnpm-workspace.yaml (globs under packages:)
    • package.json with workspaces (npm / yarn classic)
    • turbo.json, nx.json, lerna.json, rush.json
  2. Filesystem walk (fallback). No marker, or the marker didn’t find anything? PackGuard walks the tree for package.json + pyproject.toml, respecting .gitignore and skipping:

    • node_modules/, .pnpm/, target/, dist/, build/, .next/, .venv/, __pycache__/, vendor/, .git/, .turbo/, .nx/, .cache/, coverage/
    • Anything deeper than --depth (default 4)

Each discovered project is scanned independently — a parse failure on one doesn’t abort the others. The final summary lists per success, per skipped project with the reason.

If path itself is a valid project (has a package.json or pyproject.toml), it’s scanned directly and discovery is skipped — the single-project v0.1.0 behaviour is preserved when it’s what you want.

Ecosystems

  • npmpackage-lock.json v2/v3, pnpm-lock.yaml v6/v7/v9, yarn.lock (manifest-only)
  • PyPIpoetry.lock, uv.lock, requirements*.txt (declared-only)

Caching

A SHA-256 fingerprint of (manifest + lockfiles) gates the registry round-trip per project. Re-runs that match the cached fingerprint short-circuit with no changes since last scan. Pass --force to invalidate.

Flags

FlagEffect
--no-recursivePre-0.2.0 behaviour: fail with no supported manifest at <path> if path doesn’t carry a manifest. Disables discovery entirely.
--depth NCap the filesystem walk depth when no marker is found. Default 4.
--include GLOBWiden discovery with an extra glob (repeatable). Useful for non-standard layouts.
--exclude GLOBSkip paths matching a glob (repeatable). Additive on top of the built-in denylist.
--dry-runList the projects that would be scanned, without scanning. Zero registry hits, zero store writes.
--yesBypass the >50 projects found, continue? confirmation prompt. Use in CI.
--offlineFail cleanly when the cache was never populated. No registry calls.
--forceInvalidate the fingerprint cache and force a fresh scan on every matched project.
--store <path>Override the SQLite store path (global flag on every command).

Examples

# Scan a whole monorepo — one invocation, N projects. packguard scan . # Preview the discovery result without hitting any registry. packguard scan . --dry-run # Limit the walk to direct children only (skip nested monorepos). packguard scan . --depth 1 # Exclude a legacy project that ships an unsupported lockfile format. packguard scan . --exclude 'front/v1' # Scan only a specific workspace (bypass discovery). packguard scan ./apps/web --no-recursive # CI-friendly: non-interactive, fresh every time. packguard scan . --yes --force

Robustness

  • Recursive mode: one project’s parse error is a red inline line; the scan continues and the process exits 0 as long as at least one project succeeded. Failed projects are listed at the bottom with their reason.
  • Single-project mode (explicit path with manifest, or --no-recursive): a parse error fails the whole command.
  • Unsupported lockfile versions (e.g. package-lock.json v1): skipped with a warning, never aborts the wider scan.

pip declared-only mode

pip doesn’t ship a native lockfile. PackGuard parses requirements*.txt in best-effort PEP 508 and only treats a requirement as installed when it uses an exact pin (pkg==1.2.3). Loose ranges like flake8>=7.0 stay at installed = None and classify as Unknown / Warning.

If you need full coverage on a pip-only repo, move to pyproject.toml + uv.lock, or run pip-compile --generate-hashes to produce a lockfile-equivalent.

Last updated on