Documentation menu

Deliberate dependency updates

flog upgrade implements deterministic within-range maintenance and an interactive-only path for crossing declared range bounds. Interactive package picking remains deliberately deferred; use dependency arguments and workspace selectors to narrow a plan.

Flog needs a dependency-maintenance workflow that is safer than silently rewriting every manifest to whatever a registry currently calls latest. The workflow must still make a large workspace practical to maintain: one complete, deterministic plan should cover the root and selected workspace manifests.

flog update remains the command that securely updates the flog executable. Dependency maintenance uses the distinct flog upgrade command so a human or script cannot confuse package changes with a tool update.

Minimum workflow#

The command supports non-interactive planning and a single confirmation step, not an interactive package picker. For a Primate-sized workspace, the intended flow is:

# Inspect every compatible manifest and lockfile change without writing.
flog upgrade --recursive --dry-run

# Keep a machine-readable plan with the review.
flog upgrade --recursive --dry-run --json > upgrade-plan.json

# Deliberately cross range bounds only at a real terminal with confirmation.
flog upgrade --recursive --latest

Automation may pass --yes only for within-range upgrades; non-interactive input is never treated as confirmation. --latest rejects --yes, --dry-run, --json, and every non-terminal invocation so a script cannot cross range bounds. --dry-run never writes. After confirmation, upgrade publishes the reviewed package.json and flog.lock bytes and installs that exact graph. Download, extraction, or lifecycle failure restores those files and preserves the prior activated installation. Verified cache entries and external lifecycle side effects cannot be rolled back. --dry-run remains read-only and performs no installation. --cache and --ignore-scripts control the installation phase. --offline requires its package artifacts in the verified cache but does not replace the registry metadata needed to plan newer versions. --no-install applies the planned manifest and lock changes atomically without downloading, materializing, or running lifecycle scripts. When every selected dependency is already current, upgrade --no-install remains truly non-installing. Successful machine-readable results include the installed snapshot, workspace, cache-hit, download, and lifecycle-script counts under installation.

The interface is:

flog upgrade [options] [dependency ...]

  -r, --recursive           select the root and every workspace importer
      --workspace <selector>
                            select a workspace name, path, file, or glob
      --cwd <directory>     locate the package or workspace from this directory
      --latest              deliberately update supported ranges past their bounds
      --prod                select dependencies and optionalDependencies only
      --dev                 select devDependencies only
      --dry-run             build and display the complete plan without writing
      --no-install          update manifests and flog.lock without installing
  -y, --yes                 confirm the displayed plan non-interactively
      --offline             require installation artifacts in the verified cache
      --cache <directory>   use this package artifact cache directory
      --ignore-scripts      do not run dependency lifecycle scripts
      --json                emit a versioned machine-readable plan or result

Without --recursive or --workspace, the selected importer is the package containing --cwd, or the process directory. --recursive includes the root importer as well as every workspace package; this is intentional because root development tools must not be missed. Workspace selectors use the same strict exact-name, directory, owned-file, and path-glob rules as other commands. The root is not a declared workspace and is selected only by the default or --recursive, not by --workspace .. With no dependency arguments, every eligible direct declaration in the selected importers is considered. Dependency arguments match declared names, including the installed name of an npm: alias, not arbitrary transitive packages.

By default, selected direct dependencies may advance only within their current specifier and manifests remain byte-identical. --latest is the deliberate request to cross a supported range boundary and rewrite that declaration. A transitive-only update mode and arbitrary depth controls are not part of the minimum workflow.

Candidates must have greater SemVer precedence than the currently locked version. A tag that points to an equal or older version is reported but never causes an implicit downgrade. Adding dependencies, removing dependencies, changing package versions, auditing vulnerabilities, publishing, and choosing workspace release versions are separate operations.

Specifier policy#

Every occurrence is planned as the tuple (manifest, dependency field, declared name). Flog never moves a declaration between dependency fields and never silently merges duplicate declarations. Shadowed entries remain visible in the plan even when another field determines installation.

The implementation uses these rules:

Declaration Default plan --latest plan
Exact version unchanged manifest; selected lock target may not exceed it replace with the exact version named by latest
Simple caret range newest satisfying version ^ plus the version named by latest
Simple tilde range newest satisfying version ~ plus the version named by latest
Named dist-tag follow the same tag and keep its text follow the same tag; do not replace next or another channel with latest
npm: alias apply the corresponding rule to the aliased package and preserve the alias name preserve npm:, the aliased name, and the simple range style
workspace: preserve and do not query a registry preserve and do not query a registry
Peer dependency report as preserved report as preserved
Complex range newest satisfying version without rewriting block automatic rewriting and require a deliberate manual edit

Simple ranges are complete exact, caret, or tilde versions. Wildcards, partial versions, comparator sets, hyphen ranges, and logical OR ranges are complex: turning one into a caret or exact range changes policy, not just version data. The planner must explain the unsupported rewrite and make no writes if a selected entry is blocked.

Peer ranges are compatibility claims made by the package being maintained. Registry metadata cannot prove that a new peer major is supported, so a broad upgrade must not widen, narrow, or restyle them. They remain in the plan as review items. A manually changed peer range is validated during proposed graph resolution just like any other manifest change.

Prerelease intent is also never inferred away. A named prerelease tag such as next continues to follow that tag. A range containing a prerelease can advance within its existing npm prerelease semantics, but --latest does not replace it with a stable release or guess a different prerelease channel. Conversely, a stable range is not rewritten to a prerelease if a registry has assigned one to latest. Crossing either boundary requires a manual range or dist-tag edit followed by a new plan.

These rules cover Primate's current update surface without changing its public compatibility contracts: ordinary direct dependencies use exact, caret, tilde, alias, or workspace forms, while its partial major ranges are peer declarations.

Complete plan before writes#

flog upgrade requires an existing strict flog.lock that matches every manifest and the selected platform. This baseline prevents an update from combining pre-existing drift with new changes. If it is absent or stale, the operator first reconciles the existing manual edits and reviews the resulting file changes with:

flog lock

Run flog install separately when the reviewed reconciliation should be materialized.

Planning then proceeds in this order:

  1. Discover the root and workspaces through the canonical package.json workspace model and select importers and direct declarations.
  2. Read the matching lockfile and record hashes of every input file.
  3. Fetch each required npm packument once through the normal registry configuration, including scoped registries and read credentials.
  4. Select candidates and patch dependency string values in memory.
  5. Resolve the complete proposed graph in memory, including platform and peer checks, without writing a lockfile.
  6. Compare every manifest and lockfile field, then sort the plan by importer path, dependency-field order, and declared package name.
  7. Display the entire plan. A blocker anywhere prevents all writes.
  8. After confirmation, publish the planned files and install their exact locked graph before reporting success.

Resolution should prefer every existing locked target that is still valid and was not selected for update. Selected direct declarations may move according to the table above. Dependencies introduced or constrained differently by a new direct version are resolved normally. This minimizes unrelated transitive churn without treating an invalid old snapshot as authoritative. The complete lockfile diff remains part of the plan, including changed transitive packages, peer contexts, optional skips, and platform decisions.

The proposed lock bytes are produced during planning and are the exact bytes written after confirmation. Flog must not fetch newer metadata or resolve a different graph between displaying a plan and applying it in the same process. A later invocation always creates a fresh plan; a saved JSON report is an audit record, not a replayable authorization token.

Manifest and lockfile replacement#

Changing one dependency string must preserve unrelated manifest bytes: top-level key order, indentation, trailing newline choice, and unknown fields. The implementation locates validated dependency-object string tokens and replace only their JSON string spans. Duplicate or structurally ambiguous keys must be rejected rather than normalized through a map re-encode.

Before the first replacement, flog re-reads every input and verifies the hashes captured by the plan. All outputs are staged in their target directories with the original file modes. Each target is replaced with a same-filesystem atomic rename; flog.lock is replaced last. An ordinary write or rename failure rolls back already replaced files from staged originals.

Multiple files in different directories cannot be one POSIX atomic rename, so the command must not claim crash-level all-or-none semantics. Individual files must never be torn or invalid JSON. A process or machine failure can leave valid manifests with a stale lock; the next command detects that mismatch and directs the operator to rerun the plan. No package installation or publication can be triggered by recovery.

Human and JSON output#

Human output shows, for every changed declaration, its root-relative manifest path, field, declared and actual alias name, old specifier and locked version, new specifier and selected version, and why it changed. It separately lists preserved workspace and peer declarations, blocked entries, and all direct or transitive lock-only changes. Secrets, registry credentials, and tarball query parameters never appear.

JSON uses a versioned top-level object and deterministic arrays. The minimum fields are:

{
  "version": 1,
  "mode": "latest",
  "outcome": "dry-run",
  "applied": false,
  "changes": [
    {
      "path": "packages/example/package.json",
      "field": "dependencies",
      "name": "library",
      "actualName": "library",
      "fromSpecifier": "^1.4.0",
      "fromVersion": "1.9.2",
      "toSpecifier": "^2.1.0",
      "toVersion": "2.1.0",
      "reason": "latest"
    }
  ],
  "preserved": [],
  "blocked": [],
  "lockfile": {
    "path": "flog.lock",
    "changed": true,
    "sha256": "<digest of proposed canonical bytes>"
  }
}

Root-relative slash-separated paths keep output stable across machines. The outcome distinguishes planned, dry-run, no-op, blocked, confirmation-required, canceled, failed, and applied. A dry run writes the plan with applied: false. A successful confirmed command writes the same logical plan with applied: true. Cancellation and failed application must never be reported as applied.

Interactive selection and acceptance#

The reusable planner, source-preserving manifest patcher, locked-version preference, in-memory proposed-project resolution, deterministic dry runs, confirmation, stale-input detection, and staged replacement are shared by every current invocation. A future interactive picker may select entries from that complete plan, but must not add a separate resolver or mutation path.

Local npm-registry tests must cover compatible and latest selection, aliases, dist-tags, prerelease ranges, unknown tags, scoped configuration, peers, optional dependencies, and changed transitive constraints. Workspace fixtures must cover root inclusion, workspace selectors, duplicate declarations, preserved workspace: and peer values, source formatting, unknown fields, deterministic JSON, stale inputs, and injected failures at every replacement step. Tests must also prove that dry runs and blocked plans write nothing, registry endpoints are never mutated, lifecycle scripts do not run, and node_modules is untouched.

The normal full checks, race suite, Windows cross-compilation, and a real Primate dry-run plan remain release acceptance requirements.