Skip to content

feat(cli): support link: and file: specs in aube add#487

Merged
jdx merged 1 commit intomainfrom
feat/cli-add-local-spec
May 3, 2026
Merged

feat(cli): support link: and file: specs in aube add#487
jdx merged 1 commit intomainfrom
feat/cli-add-local-spec

Conversation

@jdx
Copy link
Copy Markdown
Contributor

@jdx jdx commented May 2, 2026

Summary

  • Adds a non-packument branch in parse_pkg_spec for file: / link: local-path specs (file:./pkg, link:../sibling, file:./bundle.tgz). Sits alongside the workspace-spec fork; mirrors the structural shape of feat(cli): support git specs in aube add #483 (git specs).
  • Manifest-key derivation: alias when given (my-alias@file:./pkg), otherwise the basename of the path (with .tgz / .tar.gz stripped). Verbatim spec is written into package.json and the resolver's existing local branch (is_non_registry_specifier) dispatches the install on next pass.
  • Skips packument fetch and catalog logic for local specs (catalogs are for registry deps). aube add file:./pkg no longer 404s on the registry.

Sibling to PR #483 (git specs). Closes the local-path-spec followup that #483's review surfaced. The new branch is parallel to the git-spec branch in parse_pkg_spec — when #483 lands the two branches sit side-by-side without nesting.

Test plan

  • cargo build --workspace
  • cargo test --workspace — 9 new unit tests in commands::add::tests cover file: / link: relative + absolute, tarball-basename derivation, alias forms, scoped/git non-collisions, and the bare file: + alias edge case
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --check
  • mise run test:bats test/add.bats — 28 tests pass (3 new offline tests for file: / link: / aliased file:)
  • mise run test:bats test/local_deps.bats — regression check, 10 tests pass
  • mise run test:bats test/pnpm_install_misc.bats — regression check, 30 tests pass

Note

Medium Risk
Extends aube add parsing/routing to treat file:/link: (and scoped/aliased variants) as non-registry deps, which could impact how ambiguous specs are classified and when registry packument fetches are skipped.

Overview
aube add now supports file: and link: local-path dependencies by routing them through the same non-registry flow as git specs: skip packument fetch/catalog logic and write the verbatim spec into package.json for the resolver to handle.

This introduces local-spec parsing with basename-based manifest key derivation (including stripping .tgz/.tar.gz/.tar), supports alias and @scope/alias@<spec> forms for both git and local specs, updates the packument-fetch skip guards accordingly, and adds unit + bats coverage for the new behaviors.

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 2, 2026

Greptile Summary

This PR extends aube add to handle file: and link: local-path specs (including the alias@file:./pkg and scoped @scope/alias@file:./pkg forms) by routing them through a new non-packument branch that writes the verbatim spec directly to package.json. The implementation also addresses the scoped-alias gap flagged during the git-spec PR review by adding split_scoped_alias to handle @scope/name@<git-or-local-spec> forms.

Confidence Score: 5/5

Safe to merge — only P2 style findings, core routing logic is correct and well-tested.

No P0 or P1 issues found. The two findings are a dead unreachable guard in split_scoped_alias and a thin-wrapper function that silently delegates to its git counterpart. The routing precedence across all spec forms (git, scoped-alias, local, registry) was traced and found correct. Unit and bats coverage is thorough.

No files require special attention.

Important Files Changed

Filename Overview
crates/aube/src/commands/add.rs Adds file:/link: local-path spec parsing and routing; new helpers is_local_path_spec, split_local_alias, split_scoped_alias, basename_from_local_path, parse_local_pkg_spec, and apply_local_spec_to_manifest; minor dead-code guard and thin-wrapper style issues.
test/add.bats Adds three offline bats integration tests for file:, link:, and aliased file: specs; tests look correct and cover the happy paths described in the PR.
test/PNPM_TEST_IMPORT.md Documentation-only change: marks the local-path-spec and updateRewritesSpecifier items as landed and updates their prose.

Fix All in Claude Code

Reviews (3): Last reviewed commit: "feat(cli): support link: and file: specs..." | Re-trigger Greptile

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

github-actions Bot commented May 2, 2026

Benchmark changes

Versions:

  • pnpm: 11.0.3 -> 11.0.4

Public ratios: warm installs vs Bun 7x -> 9x; warm installs vs pnpm 11x -> 13x.

Benchmark aube bun pnpm
Fresh install (warm cache) 332ms -> 240ms (-28%) 2242ms -> 2100ms (-6%) 3500ms -> 3061ms (-13%)
CI install (warm cache, GVS disabled) 930ms -> 892ms (-4%) 1447ms -> 2178ms (+51%) 2475ms -> 2301ms (-7%)
CI install (cold cache, GVS disabled) 4364ms -> 4050ms (-7%) 4083ms -> 4155ms (+2%) 5380ms -> 5371ms (0%)

aba32ba vs 5f05906 | aube/bun/pnpm | 3 scenarios | 3 runs | 500mbit/50ms | generated by Codex.

jdx added a commit that referenced this pull request May 2, 2026
The "scrub from all four dep sections, insert into the right one"
shape was triplicated across the registry path,
`apply_workspace_spec_to_manifest`, and
`apply_local_spec_to_manifest`. Extract `upsert_manifest_dep` next
to the apply_* helpers so the asymmetric peer-section logic lives in
one place.

No behavior change.

Addresses Cursor low-severity review on PR #487.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx
Copy link
Copy Markdown
Contributor Author

jdx commented May 2, 2026

Extracted upsert_manifest_dep next to apply_* helpers and replaced all three call sites (registry path, workspace, local). -85/+46 LOC, no behavior change. Written with Claude.

Adds a non-packument branch in `parse_pkg_spec` for `file:` and
`link:` local-path specs (mirroring the workspace-spec fork). The
verbatim spec is written to `package.json` and the resolver's
existing local branch dispatches the install — `aube add file:./pkg`
no longer 404s on the registry packument fetch.

Manifest key is derived from the path basename when no alias is
given (`file:./packages/foo` -> `foo`, `file:./bundle.tgz` ->
`bundle`). Pass an alias when the upstream `package.json` `name`
differs: `aube add my-pkg@file:./packages/foo`.

Sibling to PR #483 (git specs). Closes the local-path-spec followup
that #483's review surfaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the feat/cli-add-local-spec branch from af6a6d0 to aba32ba Compare May 3, 2026 00:37
@jdx
Copy link
Copy Markdown
Contributor Author

jdx commented May 3, 2026

Rebased onto current main (post-#475 / #483) and addressed all three Greptile P2s in the squashed-and-replayed commit:

  • Redundant starts_with('@') guard — the local-alias splitter and a new shared split_protocol_alias rely on at == 0 for the leading-@ rejection. The guard is gone; behavior unchanged.
  • .tar archives not strippedbasename_from_local_path now strips .tar.gz, .tgz, and .tar (in that order). New unit test test_parse_pkg_spec_file_uncompressed_tarball_strips_extension locks the .tar case in.
  • Scoped alias form silently misroutes — added split_scoped_alias that handles @scope/alias@<git-or-local-spec>, routed before the jsr/npm/registry branches in parse_pkg_spec. Two new unit tests cover @my-scope/alias@file:./pkg and @my-scope/alias@kevva/is-negative.

The Cursor "triplicated scrub-and-insert" low-severity is mostly addressed: apply_local_spec_to_manifest now wraps apply_git_spec_to_manifest (one line of forwarding instead of a copy). Down to two scrub-then-insert sites — this one and apply_workspace_spec_to_manifest.

Validation: cargo build/clippy/fmt clean, cargo test -p aube --bin aube 379/379 (+3 new), mise run test:bats test/add.bats 28/28 including the three local-spec ports.

Written with Claude.

@jdx jdx merged commit 579eb0e into main May 3, 2026
18 checks passed
@jdx jdx deleted the feat/cli-add-local-spec branch May 3, 2026 00:44
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 2 potential issues.

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

return None;
}
Some((&spec[..alias_end], &spec[alias_end + 1..]))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing colon check in split_scoped_alias misroutes protocol specs

Low Severity

split_scoped_alias does not reject aliases containing :, unlike split_protocol_alias which guards against protocol prefixes with alias.contains(':'). This means a spec like @jsr:@std/file@link:./pkg gets split into alias @jsr:@std/file and rest link:./pkg, causing is_local_path_spec to match and route it as a local spec — instead of falling through to the @jsr: parsing path where it belongs. Adding a contains(':') check on &spec[..alias_end] would align with the sibling function's logic.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aba32ba. Configure here.

let alias_end = slash + 1 + at_in_after;
if alias_end == 0 {
return None;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead alias_end == 0 guard is unreachable code

Low Severity

The alias_end == 0 check in split_scoped_alias can never be true. Since the spec starts with @, slash (from spec.find('/')) is always ≥ 1, and at_in_after is always ≥ 0, so alias_end = slash + 1 + at_in_after is always ≥ 2. This dead branch provides false reassurance that an edge case is handled.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aba32ba. Configure here.

jdx added a commit that referenced this pull request May 3, 2026
`aube add /path/to/lib` (and `./lib`, `../lib`, `~/lib`,
`C:/projects/lib`) used to fall through to the registry path and
surface a confusing HTTP 405 from npmjs. The recently-merged `file:`
/ `link:` support (#487) only handled the prefixed forms.

Match pnpm's `parseBareSpecifier`: detect path-shaped specs at parse
time and route them through the existing local-path machinery. Bare
paths default to `link:` for directories and `file:` for tarballs
(pnpm parity); `~/` is expanded eagerly because the resolver has no
tilde handling. The drive-letter check requires `/` or `\` after the
colon so a single-character npm alias like `a:1.0.0` isn't
reclassified as a Windows path.

Closes discussions/497.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jdx added a commit that referenced this pull request May 3, 2026
`aube add /path/to/lib` (and `./lib`, `../lib`, `~/lib`,
`C:/projects/lib`) used to fall through to the registry path and
surface a confusing HTTP 405 from npmjs. The recently-merged `file:`
/ `link:` support (#487) only handled the prefixed forms.

Match pnpm's `parseBareSpecifier`: detect path-shaped specs at parse
time and route them through the existing local-path machinery. Bare
paths default to `link:` for directories and `file:` for tarballs
(pnpm parity); `~/` is expanded eagerly because the resolver has no
tilde handling. The drive-letter check requires `/` or `\` after the
colon so a single-character npm alias like `a:1.0.0` isn't
reclassified as a Windows path.

Closes discussions/497.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jdx added a commit that referenced this pull request May 3, 2026
`aube add /path/to/lib` (and `./lib`, `../lib`, `~/lib`,
`C:/projects/lib`) used to fall through to the registry path and
surface a confusing HTTP 405 from npmjs. The recently-merged `file:`
/ `link:` support (#487) only handled the prefixed forms.

Match pnpm's `parseBareSpecifier`: detect path-shaped specs at parse
time and route them through the existing local-path machinery. Bare
paths default to `link:` for directories and `file:` for tarballs
(pnpm parity); `~/` is expanded eagerly because the resolver has no
tilde handling. The drive-letter check requires `/` or `\` after the
colon so a single-character npm alias like `a:1.0.0` isn't
reclassified as a Windows path.

Closes discussions/497.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jdx added a commit that referenced this pull request May 3, 2026
`aube add /path/to/lib` (and `./lib`, `../lib`, `~/lib`,
`C:/projects/lib`) used to fall through to the registry path and
surface a confusing HTTP 405 from npmjs. The recently-merged `file:`
/ `link:` support (#487) only handled the prefixed forms.

Match pnpm's `parseBareSpecifier`: detect path-shaped specs at parse
time and route them through the existing local-path machinery. Bare
paths default to `link:` for directories and `file:` for tarballs
(pnpm parity); `~/` is expanded eagerly because the resolver has no
tilde handling. The drive-letter check requires `/` or `\` after the
colon so a single-character npm alias like `a:1.0.0` isn't
reclassified as a Windows path.

Closes discussions/497.

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