Skip to content

feat(cli): add --lockfile-dir / lockfileDir setting#431

Merged
jdx merged 5 commits intomainfrom
claude/lockfile-dir
Apr 30, 2026
Merged

feat(cli): add --lockfile-dir / lockfileDir setting#431
jdx merged 5 commits intomainfrom
claude/lockfile-dir

Conversation

@jdx
Copy link
Copy Markdown
Contributor

@jdx jdx commented Apr 30, 2026

Summary

Mirrors pnpm's --lockfile-dir: relocates aube-lock.yaml to a different directory than the project root. The project becomes an importer keyed by its relative path from the lockfile directory.

Resolves the --lockfile-dir divergence noted in the misc.ts port TODO (pnpm/test/install/misc.ts:112).

Implementation

A thin importer-key shim around the existing aube-lockfile read/write API. The install pipeline keeps treating the project as the . root importer in-memory; the relative key only appears in the on-disk lockfile, transparent to every downstream consumer (resolver, linker, scripts runner).

Three local helpers in install/mod.rs:

  • parse_lockfile_dir_remapped — reads from lockfile_dir, remaps the project's relative-path importer back to . for the in-memory graph.
  • parse_lockfile_dir_remapped_with_kind — same, plus preserves the detected kind.
  • write_lockfile_dir_remapped — writes to lockfile_dir, remaps . to the relative-path key on the way out. Hard-errors if the in-memory graph is missing the . importer (defensive guard against silent data loss in future code paths).

When lockfile_dir == project_root, both shims short-circuit (no clone, no map mutation), so the default install path is unchanged.

Path-resolution (pnpm parity):

  • create_dir_all before canonicalize, so --lockfile-dir <missing> materializes the directory instead of aborting with ENOENT.
  • Both cwd and lockfile_dir canonicalized before equality / pathdiff, so symlinks and ./project/..-style inputs don't break key derivation.
  • Importer key normalized to forward slashes, so a Windows-written lockfile is portable to Unix CI.

Scope

Single-project lockfile relocation. Multi-project shared lockfiles outside a pnpm-workspace.yaml workspace are explicitly out of scope: pointing two unrelated projects at the same --lockfile-dir would have the second install orphan-strip the first project's package entries when re-resolving. An upfront read-side guard (guard_against_foreign_importers) detects this configuration and fails loudly with a message pointing the user at workspaces or per-project lockfile dirs. Workspace installs (legitimate multi-importer cases) are unaffected because the guard is gated on a non-. importer key, which only happens when --lockfile-dir points outside the project root.

Wired through the install path's main read/write sites (8 call sites). Two boundaries deferred to follow-up PRs:

  • sharedWorkspaceLockfile=false per-project lockfile writes (line ~4118) still target project_root — combining --lockfile-dir with workspace-shared lockfiles needs a separate design decision.
  • merge_git_branch_lockfiles flow likewise targets project_root.

Settings

Layer Key
CLI --lockfile-dir <PATH>
Env AUBE_LOCKFILE_DIR, npm_config_lockfile_dir, NPM_CONFIG_LOCKFILE_DIR
.npmrc lockfile-dir, lockfileDir
pnpm-workspace.yaml lockfileDir

Relative paths resolve against the project root (pnpm convention).

Test plan

  • New: test/lockfile_dir.bats (5 tests — write, importer key, auto-mkdir on missing dir, foreign-importer guard, warm read).
  • New: misc.ts:112 port in test/pnpm_install_misc.bats (bumps the misc.ts port to 13/37, drops --lockfile-dir from the documented-divergences list).
  • mise run test:bats test/install.bats (61/61) — no regressions.
  • mise run test:bats test/lockfile_settings.bats test/workspace.bats — no regressions.
  • cargo test --workspace — all green (after sorting --lockfile-dir alphabetically before --lockfile-only to satisfy cli_ordering_tests::test_cli_ordering).
  • cargo clippy -p aube --all-targets -- -D warnings clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes where aube install reads/writes lockfiles and how importer keys are mapped, which could affect install reproducibility and overwrite behavior if path handling is wrong. Guardrails and tests reduce risk, but this touches core install/lockfile I/O paths.

Overview
Adds --lockfile-dir <PATH> (and lockfileDir setting) to relocate aube-lock.yaml to an arbitrary directory, mirroring pnpm.

aube install now resolves/canonicalizes the target directory, creates it if missing, and remaps lockfile importers keys between . in-memory and the project’s relative-path key on disk; lockfile read/write/detect call sites are updated to use the relocated directory.

Includes a fail-fast guard that errors if the relocated lockfile already contains importers from other projects (multi-project shared lockfiles outside a workspace remain unsupported), plus new Bats coverage and CLI/settings documentation updates.

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

Adds pnpm-compatible --lockfile-dir / lockfileDir to relocate aube-lock.yaml outside the project root, recording the project under a relative-path importer key; includes canonicalization, auto-mkdir, and a foreign-importer guard that prevents multi-project shared lockfiles outside workspaces.

  • guard_against_foreign_importers silently passes a foreign "." importer: the filter excludes "." from the foreign-key check, but when importer_key != "." any existing "." entry represents the lockfile directory's own project root. The guard passes, the remap shim finds no importer_key match, and the subsequent write removes the lockfile-dir project's "." entry — exactly the orphan-strip scenario the guard is designed to prevent.
  • Docs actively contradict the guard: both settings.toml and docs/settings/index.md state "several unrelated projects can share a single committed lockfile without declaring a pnpm-workspace.yaml" — the configuration guard_against_foreign_importers hard-errors on.

Confidence Score: 3/5

Not safe to merge as-is: the foreign-importer guard has a logic hole that allows silent lockfile corruption, and the docs describe a multi-project sharing use-case the implementation actively rejects.

Two P1 findings: a guard bypass that can corrupt a legitimate project's lockfile entry, and documentation that directly contradicts runtime behavior. Both affect the correctness and safety of the core feature being added.

crates/aube/src/commands/install/mod.rs (guard filter) and crates/aube-settings/settings.toml + docs/settings/index.md (doc contradiction)

Important Files Changed

Filename Overview
crates/aube/src/commands/install/mod.rs Core implementation: adds lockfile-dir resolution, importer-key remapping shims, and foreign-importer guard; the guard incorrectly excludes "." from foreign-importer detection, allowing silent data corruption when the lockfile directory is itself a project root.
crates/aube-settings/settings.toml Adds lockfileDir setting definition; documentation contradicts the guard's behavior by claiming multi-project shared lockfiles are supported when they are actively rejected at runtime.
docs/settings/index.md Mirrors the incorrect multi-project sharing claim from settings.toml; users who follow this docs example will hit a hard error on the second project's install.
crates/aube-manifest/src/workspace.rs Adds lockfile_dir: Option field to WorkspaceConfig with correct serde default; straightforward.
test/lockfile_dir.bats New Bats test file covering write, importer-key format, auto-mkdir, foreign-importer guard, and warm read round-trip; well-structured and covers the primary scenarios.
aube.usage.kdl Adds --lockfile-dir flag definition in alphabetical order between --ignore-scripts and --lockfile-only; correct.
test/pnpm_install_misc.bats Ports pnpm/test/install/misc.ts:112 as a Bats test; increments ported-count from 21→22 and removes the documented-divergences entry.
docs/cli/install.md Adds --lockfile-dir CLI docs entry in correct alphabetical position; accurate description.
docs/cli/commands.json Adds lockfile-dir to the install command's JSON spec with correct arg definition; auto-generated, no issues.
test/PNPM_TEST_IMPORT.md Updates port tracking: 21→22 ported, removes --lockfile-dir from documented divergences, adds #431 reference; accurate bookkeeping.

Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(cli): address cursor review on --loc..." | Re-trigger Greptile

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

github-actions Bot commented Apr 30, 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 -> 3x; warm installs vs pnpm 5x -> 7x.

Benchmark aube bun pnpm
Fresh install (warm cache) 1021ms -> 770ms (-25%) 4134ms -> 2500ms (-40%) 4717ms -> 5162ms (+9%)
CI install (warm cache, GVS disabled) 2920ms -> 2528ms (-13%) 3396ms -> 2834ms (-17%) 4864ms -> 5334ms (+10%)
CI install (cold cache, GVS disabled) 10801ms -> 7814ms (-28%) 10012ms -> 9787ms (-2%) 9722ms -> 9391ms (-3%)

bd65f92 vs 63fcb9a | aube/bun/pnpm | 3 scenarios | 3 runs | 500mbit/50ms | generated by Codex.

jdx added a commit that referenced this pull request Apr 30, 2026
Adds the misc.ts:112 port to test/pnpm_install_misc.bats now that #431
implements `aube install --lockfile-dir`. is-positive substituted with
is-odd; pnpm's `install <pkg> --lockfile-dir ../` becomes aube's
`install --lockfile-dir ..` with the dep already declared in
package.json (aube's flag is install-only and `aube install` doesn't
take a package argument).

Brings the misc.ts port to 11/37; moves `--lockfile-dir` out of the
documented-divergences list in the TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the claude/lockfile-dir branch from 0bc1d4d to a3addcc Compare April 30, 2026 22:54
jdx added a commit that referenced this pull request Apr 30, 2026
Adds the misc.ts:112 port to test/pnpm_install_misc.bats now that #431
implements `aube install --lockfile-dir`. is-positive substituted with
is-odd; pnpm's `install <pkg> --lockfile-dir ../` becomes aube's
`install --lockfile-dir ..` with the dep already declared in
package.json (aube's flag is install-only and `aube install` doesn't
take a package argument).

Brings the misc.ts port to 11/37; moves `--lockfile-dir` out of the
documented-divergences list in the TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jdx added a commit that referenced this pull request Apr 30, 2026
Adds the misc.ts:112 port to test/pnpm_install_misc.bats now that #431
implements `aube install --lockfile-dir`. is-positive substituted with
is-odd; pnpm's `install <pkg> --lockfile-dir ../` becomes aube's
`install --lockfile-dir ..` with the dep already declared in
package.json (aube's flag is install-only and `aube install` doesn't
take a package argument).

Brings the misc.ts port to 11/37; moves `--lockfile-dir` out of the
documented-divergences list in the TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the claude/lockfile-dir branch from a336678 to 7fb0e20 Compare April 30, 2026 22:57
jdx added a commit that referenced this pull request Apr 30, 2026
Adds the misc.ts:112 port to test/pnpm_install_misc.bats now that #431
implements `aube install --lockfile-dir`. is-positive substituted with
is-odd; pnpm's `install <pkg> --lockfile-dir ../` becomes aube's
`install --lockfile-dir ..` with the dep already declared in
package.json (aube's flag is install-only and `aube install` doesn't
take a package argument).

Brings the misc.ts port to 11/37; moves `--lockfile-dir` out of the
documented-divergences list in the TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the claude/lockfile-dir branch from 7fb0e20 to 3dc7727 Compare April 30, 2026 22:59
Comment thread crates/aube/src/commands/install/mod.rs
Comment thread crates/aube/src/commands/install/mod.rs
@jdx jdx enabled auto-merge (squash) April 30, 2026 23:10
jdx and others added 5 commits April 30, 2026 18:25
Mirrors pnpm's --lockfile-dir: relocates aube-lock.yaml to a
different directory than the project root. The project becomes an
importer keyed by its relative path from the lockfile directory, so
several unrelated projects can share one committed lockfile without
declaring a pnpm-workspace.yaml.

Implementation: a thin importer-key shim around the existing
aube-lockfile read/write API. The install pipeline keeps treating
the project as the `.` root importer in-memory; the relative key only
appears in the on-disk lockfile, transparent to every downstream
consumer (resolver, linker, scripts runner).

Scope: install path's main read/write sites. Workspace per-project
lockfile writes (sharedWorkspaceLockfile=false) and merge-git-branch
flows continue to use the project root, since combining --lockfile-dir
with workspace-shared lockfiles requires a separate design decision.

Resolves the --lockfile-dir divergence noted in the misc.ts port TODO
([pnpm/test/install/misc.ts:112]).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Auto-create the lockfile dir when missing (pnpm parity), instead of
  aborting in canonicalize with ENOENT.
- Normalize the importer key to forward slashes so Windows-written
  lockfiles stay portable to Unix CI.
- Hard-error in `write_lockfile_dir_remapped` when the in-memory graph
  is missing the `.` importer, instead of silently writing a lockfile
  without the project's entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the misc.ts:112 port to test/pnpm_install_misc.bats now that #431
implements `aube install --lockfile-dir`. is-positive substituted with
is-odd; pnpm's `install <pkg> --lockfile-dir ../` becomes aube's
`install --lockfile-dir ..` with the dep already declared in
package.json (aube's flag is install-only and `aube install` doesn't
take a package argument).

Brings the misc.ts port to 11/37; moves `--lockfile-dir` out of the
documented-divergences list in the TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Guard against multi-project shared lockfiles outside a workspace.
  Reading another project's lockfile from the same `--lockfile-dir`
  and re-resolving against it would orphan-strip the other project's
  package entries (the resolver only sees the current importer's
  deps). Fail loudly upfront, before any FrozenMode branch, so the
  guard fires regardless of `--no-frozen-lockfile` / `--lockfile-only`
  shortcuts. Workspace installs (legitimate multi-importer case) are
  unaffected because the guard is gated on a non-`.` importer key,
  which only happens when `--lockfile-dir` actually points outside
  the project root.
- Canonicalize `cwd` before comparing to the canonicalized lockfile
  dir, so symlinks or `./project/..`-style inputs no longer make the
  equality check fail or produce a wrong `pathdiff` importer key.
- Drop the misleading "several unrelated projects can share one
  committed lockfile" claim from the inline comment; that case
  requires a workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the claude/lockfile-dir branch from 42141c2 to bd65f92 Compare April 30, 2026 23:27
.importers
.keys()
.map(String::as_str)
.filter(|k| *k != "." && *k != importer_key)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 "." is incorrectly excluded from foreign-importer detection

When importer_key != ".", a "." entry already in the lockfile means the lockfile directory is itself a project root (someone ran aube install there without --lockfile-dir). That entry is foreign and should be rejected, but the current filter silently exempts it. The guard then passes, parse_lockfile_dir_remapped finds no importer_key match (since the disk has "."), and write_lockfile_dir_remapped removes the old "." entry and replaces it with the subdirectory project — silently orphan-stripping exactly the project the guard is designed to protect.

Suggested change
.filter(|k| *k != "." && *k != importer_key)
.filter(|k| *k != importer_key)

Fix in Claude Code

Comment on lines +1003 to +1005
(e.g. `project` if the lockfile is one directory above), so several
unrelated projects can share a single committed lockfile without
declaring a `pnpm-workspace.yaml`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Docs claim multi-project sharing is supported; implementation rejects it

The sentence "so several unrelated projects can share a single committed lockfile without declaring a pnpm-workspace.yaml" describes exactly the configuration that guard_against_foreign_importers hard-errors on. A user who sets lockfileDir based on this description will hit the "lockfile already records importers from other projects" error the first time a second project tries to install. The same incorrect text is mirrored verbatim into docs/settings/index.md.

This sentence should be replaced with a note that lockfileDir is for single-project relocation only, and multi-project shared lockfiles require a pnpm-workspace.yaml workspace.

Fix in Claude Code

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.

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 bd65f92. Configure here.

.keys()
.map(String::as_str)
.filter(|k| *k != "." && *k != importer_key)
.collect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Guard allows "." importer through, missing foreign project

Medium Severity

The guard_against_foreign_importers filter excludes "." from the foreign-importer check (*k != "."). When lockfile_dir already contains a lockfile from a standalone project (which uses "." as its importer key), the guard silently passes. Both projects then alternate overwriting each other's lockfile data — the exact data-loss scenario the guard exists to prevent. Filtering "." serves no purpose here: workspace installs skip the guard entirely (lockfile_importer_key == "."), and --lockfile-dir installs always write non-"." keys.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bd65f92. Configure here.

@jdx jdx merged commit 400c56a into main Apr 30, 2026
18 checks passed
@jdx jdx deleted the claude/lockfile-dir branch April 30, 2026 23:34
jdx added a commit that referenced this pull request Apr 30, 2026
…cs (#442)

Follow-up to [#431](#431) addressing
two greptile P1s left over after merge.

## Summary

- **Foreign-importer guard previously exempted `"."`.** When the current
project's importer key is non-`.`, a `"."` entry on disk is itself an
unrelated project that ran `aube install` in the lockfile dir directly.
Dropping it on write is the same data-loss class the guard exists to
prevent. Drop the `"."` exemption from the filter; the caller already
gates on `importer_key != "."` so the default install path stays
untouched.
- **Replace stale `lockfileDir` docs.**
[crates/aube-settings/settings.toml](crates/aube-settings/settings.toml)
and the generated [docs/settings/index.md](docs/settings/index.md) still
claimed "several unrelated projects can share a single committed
lockfile", which is the exact configuration the guard rejects. Replaced
with the correct scope: single-project relocation only, multi-project
sharing requires a workspace.

## Test plan

- [x] New: `refuses to clobber a parent dir that is itself a project` in
[test/lockfile_dir.bats](test/lockfile_dir.bats) — covers the `.`
foreign-importer case the previous filter missed.
- [x] `mise run test:bats test/lockfile_dir.bats` (6/6).
- [x] `mise run test:bats test/install.bats test/workspace.bats` (79/79)
— no regressions; workspace installs with multiple importers stay
healthy because the guard is gated on a non-`.` importer key.
- [x] `cargo clippy -p aube --all-targets -- -D warnings` clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes `aube install --lockfile-dir` to fail in an additional
scenario (when the target lockfile already contains a `.` importer),
which could break previously-working but unsafe setups. Scope is small
and covered by a new bats test, but it affects install-time behavior and
lockfile safety checks.
> 
> **Overview**
> Tightens the `--lockfile-dir` foreign-importer guard so a lockfile
containing a `.` importer is treated as *foreign* when installing from a
subproject, preventing silent loss of the parent project’s lockfile
entries.
> 
> Updates `lockfileDir` documentation (settings registry + generated
docs) to remove the claim that unrelated projects can share a lockfile,
clarifying that multi-project sharing requires a `pnpm-workspace.yaml`
workspace. Adds a bats regression test to ensure installs refuse to
target a parent directory that is itself a project.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ffa91ac. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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