Skip to content

feat(cli): aube update parses <pkg>@<spec> args + accepts indirect deps#446

Merged
jdx merged 3 commits intomainfrom
claude/update-pkg-spec-and-indirect
May 1, 2026
Merged

feat(cli): aube update parses <pkg>@<spec> args + accepts indirect deps#446
jdx merged 3 commits intomainfrom
claude/update-pkg-spec-and-indirect

Conversation

@jdx
Copy link
Copy Markdown
Contributor

@jdx jdx commented May 1, 2026

Summary

Two small changes in crates/aube/src/commands/update.rs that finish the pnpm/test/update.ts cluster everywhere except the documented prerelease-preservation and shared-lockfile divergences. Brings the file to 15/22 ports (was 13/22).

Changes

1. <pkg>@<spec> arg syntax

pnpm phrases the manifest-rewrite-past-range case as pnpm update foo@latest; aube was rejecting that with package foo@latest is not a dependency because it looked up the literal arg in the deps map. Now <name>@<spec> is split (scope-aware via a split_pkg_arg helper), the bare name drives the dep lookup, and @latest (the only spec form pnpm tests rely on) acts as a per-key --latest — the manifest rewrite triggers only for that one entry instead of every direct dep.

This drops the translation note from the misc.ts:14 port — the new test in this PR uses the original update <pkg>@latest syntax verbatim instead of rewriting to update --latest <pkg>.

2. aube update <indirect-pkg> no longer errors

Previously the args loop hard-failed on any name that wasn't in package.json's deps map. Indirect deps now flow through:

  • Validated against the lockfile graph (rejected if not in any direct/transitive snapshot entry).
  • Filtered out of the locked snapshot so the resolver re-resolves.
  • Their parents' locked dep edges get rewritten to latest when the user passed <indirect>@latest.
  • Manifest is left alone (the indirect has no entry to rewrite).
  • The direct dep that pulls in the indirect stays at its locked version — only the named arg bumps.

The parent-edge rewrite is the non-obvious bit. The resolver's lockfile-reuse path (aube_resolver::resolve.rs:1164) iterates each parent's locked dependencies map and enqueues transitive tasks using the locked version as the range. Just dropping the indirect from the packages map isn't enough — the parent's pinned edge (pkg-with-1-dep@100.0.0's dependencies: { dep-of: 100.0.0 }) still re-resolves to the same version. Forwarding latest on the edge makes the transitive task a dist-tag spec, so the resolver fetches the packument and picks the new latest.

Ports

pnpm test (line) aube @test
update <dep> (14) aube update <pkg>@latest: parses arg syntax, rewrites manifest past range
update indirect dependency should not update package.json (690) aube update <indirect-pkg>@latest: refreshes a transitive dep, leaves manifest alone

The earlier update --latest <pkg> translation of misc.ts:14 stays as its own test — keeps coverage of both invocation forms.

Remaining gaps in update.ts

After this PR, 7 of 22 tests stay unported, all because of distinct divergences (each tracked in PNPM_TEST_IMPORT.md):

  • 51, 95: aube update (no --latest) doesn't rewrite the manifest spec when the existing range allows a newer version (pnpm does).
  • 599: aube update --depth N is parsed-but-no-op.
  • 615, 728, 807: aube's update --latest downgrades a manifest pin past a registry's older latest dist-tag (pnpm preserves the pin when it's numerically newer).
  • 249, 302: subsumed by 369/543 — aube's update -r always writes per-project lockfiles regardless of sharedWorkspaceLockfile.

Test plan

  • AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/pnpm_update.bats — 16/16 pass.
  • AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/update.bats test/add.bats test/install.bats — 96/96 pass (no regression to the existing aube-native test surface).
  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --workspace
  • mise run render — no docs/usage drift (the --save-exact flag landed in #438).

🤖 Generated with Claude Code


Note

Medium Risk
Changes aube update dependency selection and lockfile filtering/rewriting behavior, including transitive edge rewrites, which could affect resolution results across workspaces. Scope is contained to the update command and covered by new bats regression tests.

Overview
aube update now parses positional args of the form <pkg>@<spec> (scope-aware) and treats @latest as a per-package equivalent of --latest, while hard-erroring on any other spec to avoid silently ignoring user intent.

The update flow now accepts indirect (transitive) deps named on the CLI: it validates them against the lockfile graph (with a workspace-root lockfile fallback), drops their snapshots from the reused lockfile, and for <indirect>@latest rewrites parent locked dependency edges to latest so the resolver actually re-resolves the transitive version; package.json is left unchanged for indirect updates.

Recursive update -r was adjusted to forward indirect args into per-project updates by consulting each project’s lockfile (with shared-lockfile fallback) and to avoid misclassifying flag-excluded direct deps (e.g. --prod <devDep>) as indirect. New bats tests cover the new arg syntax, indirect updates, spec rejection, and the --prod regression.

Reviewed by Cursor Bugbot for commit 7f95613. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR adds two capabilities to aube update: parsing pnpm-style <pkg>@<spec> arguments (with non-latest specs hard-rejected) and allowing indirect (transitive) dependencies to be named directly on the command line without touching package.json. The indirect-dep path validates against the lockfile graph, drops the package from the reused-lockfile snapshot, and — critically — rewrites the parent's locked dep edge to "latest" so the resolver actually fetches a fresh version rather than re-pinning the old one.

Two aliased-indirect-dep gaps worth tracking: the parent edge rewrite loop matches indirect_name directly against parent_pkg.dependencies keys, which misses parents that reference the transitive dep under an alias key (the packages.retain block already handles alias_of correctly). Similarly, project_lockfile_names in run_filtered collects only p.name values, so -r <aliased-indirect>@latest silently skips all projects while the non-recursive path succeeds, because in_graph in run checks both p.name and p.alias_of.

Confidence Score: 4/5

Safe to merge; the two flagged gaps are limited to aliased transitive deps, which are uncommon, and all primary code paths are well-tested.

No P0/P1 findings. Two P2 logic gaps exist for aliased transitive deps — one causes a silent no-op for <aliased-indirect>@latest (parent edge not rewritten), the other causes -r mode to silently skip projects for the same arg class. Both are narrow edge cases that require aliased indirect deps and don't affect the documented, tested code paths.

crates/aube/src/commands/update.rs — the filtered_existing parent-edge rewrite block (line ~440) and project_lockfile_names construction in run_filtered (line ~708) if aliased transitive dep support is needed in the future.

Important Files Changed

Filename Overview
crates/aube/src/commands/update.rs Core update command extended with <pkg>@<spec> parsing, indirect dep support, and workspace lockfile fallback; two aliased-indirect-dep gaps noted (P2)
test/pnpm_update.bats Four new bats tests added covering <pkg>@latest syntax, indirect dep refresh, flag-excluded dep rejection, and non-latest spec rejection — all well-structured and complementary
test/PNPM_TEST_IMPORT.md Tracking doc updated to reflect 17/22 ports and the two new fixes; divergence notes cleaned up accurately

Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(cli): aube update flag-excluded dire..." | Re-trigger Greptile

Comment thread crates/aube/src/commands/update.rs Outdated
Comment thread crates/aube/src/commands/update.rs
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Benchmark changes

Versions:

  • aube: 1.5.1 -> 1.5.2
  • pnpm: 11.0.2 -> 11.0.3

Public ratios: warm installs vs Bun 4x -> 4x; warm installs vs pnpm 5x -> 9x.

Benchmark aube bun pnpm
Fresh install (warm cache) 1021ms -> 272ms (-73%) 4134ms -> 1103ms (-73%) 4717ms -> 2387ms (-49%)
CI install (warm cache, GVS disabled) 2920ms -> 973ms (-67%) 3396ms -> 1166ms (-66%) 4864ms -> 2250ms (-54%)
CI install (cold cache, GVS disabled) 10801ms -> 4211ms (-61%) 10012ms -> 4469ms (-55%) 9722ms -> 4529ms (-53%)

7f95613 vs 4326926 | aube/bun/pnpm | 3 scenarios | 3 runs | 500mbit/50ms | generated by Codex.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 226c343. Configure here.

Comment thread crates/aube/src/commands/update.rs
jdx and others added 3 commits April 30, 2026 21:25
Two small changes in update.rs that unblock two more pnpm/test/update.ts
ports (15/22, was 13/22):

1. `<pkg>@<spec>` arg syntax. pnpm phrases the manifest-rewrite-past-
   range case as `pnpm update foo@latest`; aube was rejecting that with
   'package foo@latest is not a dependency' because it looked up the
   literal arg. Now `<name>@<spec>` is split (scope-aware), the bare
   `name` drives the dep lookup, and `@latest` (the only spec form
   pnpm tests rely on) acts as a per-key `--latest` — manifest rewrite
   triggers only for that one entry instead of every direct dep. Drops
   the translation note from the misc.ts:14 port.

2. `aube update <indirect-pkg>` no longer errors. Previously the args
   loop hard-failed on any name that wasn't in package.json's deps map.
   Indirect deps now flow through:
   - Validated against the lockfile graph (rejected if not in any
     declared/transitive snapshot entry).
   - Filtered out of the locked snapshot so the resolver re-resolves.
   - Their parents' locked dep edges get rewritten to `latest` when the
     user passed `<indirect>@latest`. This part is non-obvious: the
     resolver's lockfile-reuse path
     (aube_resolver::resolve.rs:1164) iterates each parent's locked
     `dependencies` map and enqueues transitive tasks using the locked
     *version* as the range. Just dropping the indirect from the
     `packages` map isn't enough — the parent's pinned edge still
     re-resolves to the same version. Forwarding `latest` on the edge
     makes the transitive task a dist-tag spec, so the resolver fetches
     the packument and picks the new latest.
   - Manifest is left alone (the indirect has no entry to rewrite).
   The direct dep that pulls in the indirect stays at its locked
   version — only the named arg bumps.

Ports landed:
- pnpm/test/update.ts:14 ('update <dep>') as `<pkg>@latest` parity
  (the existing `update --latest <pkg>` translation stays as a
  separate test).
- pnpm/test/update.ts:690 ('update indirect dependency should not
  update package.json').

Test plan
- AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats \
    test/pnpm_update.bats — 16/16 pass.
- 96/96 pass across update.bats / add.bats / install.bats — no
  regression to the existing aube-native test surface.
- cargo fmt --check / cargo clippy --all-targets -- -D warnings clean.
- cargo test --workspace clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…@<spec>

Addresses two P2 findings greptile flagged on the PR.

1. `aube update -r <indirect-pkg>@latest` was a silent no-op.
   `run_filtered`'s per-project filter only admitted args present in
   each project's `declared` (direct manifest deps). Indirect deps
   never appear there, so `per_pkg.packages` always emptied and the
   inner `run` was never called. Fix: also consult each project's
   lockfile (preferring per-project, falling back to the workspace-
   root shared one) so transitive deps flow through. Also added the
   same fallback to `run`'s `existing` lookup, since project-local
   lockfiles don't exist in the common
   `aube install` → `aube update -r` flow — without it, the inner
   indirect-arg validation in `run` rejects the dep that lives only
   in the shared lockfile.

2. `aube update foo@^2.0.0` silently swallowed the spec. `split_pkg_arg`
   accepts any `<name>@<spec>` form but only `@latest` was honored —
   anything else (`@^2.0.0`, `@1.2.3`, `@beta`) was treated as a bare
   name with the spec dropped. Fix: new `reject_unsupported_pkg_specs`
   helper called from both `run` and `run_filtered`. Errors with a
   message pointing at the supported alternatives (`--latest`,
   `<pkg>@latest`, or omit the spec for in-range refresh).

Both fixes have regression tests:
- `aube update <pkg>@<spec>: rejects non-latest specs with a helpful error`
- `aube update -r <indirect-pkg>@latest: refreshes the indirect across all projects`

Verified each test fails when its corresponding fix is reverted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt re-resolve

Addresses the cursor-bugbot finding on PR #446. A direct dep excluded
by `--prod`/`--dev`/`--no-optional` (e.g. a devDep under `--prod`)
fell through the new indirect-dep branch added for indirect-arg
support: it missed `all_specifiers` (filtered by flags), the lockfile
graph carries every package regardless of bucket so `in_graph`
passed, and the dep silently re-resolved instead of erroring with the
old "not a dependency" message.

Fix in `run`: build `all_direct_keys` (every direct manifest key,
ignoring flag filters) and short-circuit with the same error before
the indirect-dep branch. The pre-indirect-support behavior is
preserved verbatim — flag mismatches stay visible.

Fix in `run_filtered`: build `all_declared` (same idea, per-project)
and drop flag-excluded direct args from the per-project filter
*before* the lockfile-name fallback rescues them. Without this guard
the indirect-via-lockfile fallback would push devDeps under `--prod`
into the inner `run` as if they were transitive, and `run`'s new
guard would error — turning the existing "silent skip when only a
devDep" recursive behavior into a hard failure. Filtering first
keeps that test green.

Regression test: `aube update --prod <devdep>` errors and leaves the
lockfile pin at the previous version, so a future regression of
either guard is caught.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the claude/update-pkg-spec-and-indirect branch from 226c343 to 7f95613 Compare May 1, 2026 02:27
@jdx
Copy link
Copy Markdown
Contributor Author

jdx commented May 1, 2026

Force-pushed rebased on main (now at 4326926), with three reviewer findings addressed.

greptile P2 — recursive indirect deps silently dropped: run_filtered's per-project filter only admitted args that appeared in each project's direct deps. Now also consults each project's lockfile (or the workspace-root shared one when there isn't a per-project copy yet) so transitive args flow through. run's indirect-arg validation also picks up the workspace-root fallback for the same reason — without it the inner call rejects deps that only live in the shared lockfile after a fresh aube install.

greptile P2 — non-latest @<spec> silently swallowed: aube update foo@^2.0.0 etc. previously parsed past split_pkg_arg but only @latest was honored, so the spec was dropped without feedback. New reject_unsupported_pkg_specs helper called from both run and run_filtered errors with a message naming the supported alternatives (--latest, <pkg>@latest, or omit the spec).

cursor-bugbot — flag-excluded direct deps misclassified as indirect: a devDep named under --prod (or a regular dep under --dev, an optional dep under --no-optional) missed all_specifiers (filtered by flags) and slipped into the new indirect path — in_graph passed because the lockfile carries every dep regardless of bucket, and the dep silently re-resolved instead of erroring with the old "not a dependency" message. Fix in run: build all_direct_keys (every direct manifest key, ignoring flag filters) and short-circuit with the same error before the indirect branch. Fix in run_filtered: build all_declared (per-project, same idea) and drop flag-excluded args from the per-project filter before the lockfile fallback rescues them — keeps the existing aube update -r --prod <devdep> silent-skip behavior intact while still erroring on the non-recursive form.

Each fix has a regression test in test/pnpm_update.bats. Verified locally that cargo test --workspace (1235 tests), cargo clippy --all-targets -- -D warnings, and cargo fmt --check are clean.

Written with Claude.

@jdx jdx merged commit 51e6a9c into main May 1, 2026
18 checks passed
@jdx jdx deleted the claude/update-pkg-spec-and-indirect branch May 1, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant