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:
- Discover the root and workspaces through the canonical
package.jsonworkspace model and select importers and direct declarations. - Read the matching lockfile and record hashes of every input file.
- Fetch each required npm packument once through the normal registry configuration, including scoped registries and read credentials.
- Select candidates and patch dependency string values in memory.
- Resolve the complete proposed graph in memory, including platform and peer checks, without writing a lockfile.
- Compare every manifest and lockfile field, then sort the plan by importer path, dependency-field order, and declared package name.
- Display the entire plan. A blocker anywhere prevents all writes.
- 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.