Skip to content

fix(cli): version-pin scoped monorepo tags like @tanstack/react-query@5.101.2#122

Merged
amondnet merged 2 commits into
mainfrom
amondnet/121
Jul 5, 2026
Merged

fix(cli): version-pin scoped monorepo tags like @tanstack/react-query@5.101.2#122
amondnet merged 2 commits into
mainfrom
amondnet/121

Conversation

@amondnet

@amondnet amondnet commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #121ask docs npm:@tanstack/react-query (and any scoped monorepo package) could not resolve a version-pinned checkout. Two compounding defects:

  1. npm resolver missed the real tag. Monorepo fallback candidates only used the unscoped name (react-query@5.101.2), which 404s on scoped monorepos. The resolver now prepends the full scoped name (@tanstack/react-query@5.101.2, plus the @v variant) ahead of the unscoped candidates when repository.directory is present.
  2. Unsafe-path guard rejected slash-containing tags. githubStorePath treated the / in scoped tags as path traversal and threw, killing the git-clone path even when the real tag was discovered. Path separators in the tag segment are now encoded as __ (same convention as the skills spec-key encoding) — .../TanStack/query/@tanstack__react-query@5.101.2/ — while .. traversal inputs are still rejected after encoding. This also unblocks slash-containing branch refs like release/v1.2.3.

Because the store dir basename is now an encoded tag, callers can no longer recover the real ref from the path: the github source store-hit path reports the winning candidate in meta.ref, and ensureCheckout prefers meta.ref over the checkout basename for its ref contract (ask src --json).

Applied to both the TS CLI (packages/cli) and the Rust port (crates/ask).

Verification

  • TS: 632 tests pass (new: resolver scoped-candidate ordering, store-path encoding + traversal rejection, end-to-end scoped-tag clone → encoded store dir + meta.ref + cache-hit)
  • Rust: 289 tests pass, clippy/fmt clean (mirrored tests incl. local-bare-repo scoped-tag clone)
  • Parity harness: ALL PARITY CASES IDENTICAL (9 cases)
  • Live repro (both binaries):
    $ ask docs "npm:@tanstack/react-query@5.101.2"
    <ASK_HOME>/github/github.com/TanStack/query/@tanstack__react-query@5.101.2/docs
    $ ask src --json "npm:@tanstack/react-query@5.101.2" | jq .ref
    "@tanstack/react-query@5.101.2"

Closes #121


Summary by cubic

Fixes version-pinned checkouts for scoped monorepo packages like @tanstack/react-query@5.101.2 in both the TS CLI and Rust port. Adds injective encoding for slash-containing refs to prevent cache collisions and reports the actual winning ref.

  • Bug Fixes
    • NPM resolver (monorepos): Try full scoped tags first (<name>@<version>, <name>@v<version>), then unscoped; keeps unscoped for repos like vercel/ai.
    • GitHub store paths: Encode / and \ in the ref segment as __ and append an 8-char sha256 when separators are present; still reject ..; avoids collisions like release/v1.0.0 vs release__v1.0.0; supports tags like @tanstack/react-query@5.101.2 and branches like release/v1.2.3.
    • GitHub source: Return the winning candidate in meta.ref for clones and store hits.
    • ensureCheckout: Prefer meta.ref over the checkout dir basename when reporting ref.

Written for commit 716d716. Summary will update on new commits.

…@5.101.2

Scoped monorepos (@tanstack/*, @trpc/*, ...) tag releases with the FULL
scoped package name, which contains a slash. Two compounding defects
blocked version pinning for them:

- The npm resolver only generated unscoped monorepo tag candidates
  (react-query@5.101.2), which do not exist on scoped monorepos. Add the
  full scoped name (@tanstack/react-query@5.101.2) ahead of the unscoped
  candidates when repository.directory is present.
- githubStorePath rejected slash-containing tags outright (unsafe-path
  guard), killing the git-clone path even when the real tag was known.
  Encode path separators as __ in the tag store segment (same convention
  as the skills spec-key encoding) instead of rejecting; traversal inputs
  are still rejected after encoding.

Since the store dir basename is now an ENCODED tag, callers can no longer
recover the real ref from the path: the github source store-hit path now
reports the winning candidate in meta.ref, and ensureCheckout prefers
meta.ref over the checkout basename for its ref contract.

Applied to both the TS CLI (packages/cli) and the Rust port (crates/ask);
parity harness passes on all 9 cases.

Closes #121
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. type:bug Something isn't working labels Jul 5, 2026
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@amondnet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e271b5a-a907-4ba1-9228-dc8a4e00074b

📥 Commits

Reviewing files that changed from the base of the PR and between 2f40a0c and 716d716.

📒 Files selected for processing (11)
  • crates/ask/src/commands/ensure_checkout.rs
  • crates/ask/src/resolvers/npm.rs
  • crates/ask/src/sources/github.rs
  • crates/ask/src/store/paths.rs
  • packages/cli/src/commands/ensure-checkout.ts
  • packages/cli/src/resolvers/npm.ts
  • packages/cli/src/sources/github.ts
  • packages/cli/src/store/index.ts
  • packages/cli/test/resolvers/npm.test.ts
  • packages/cli/test/sources/github-monorepo.test.ts
  • packages/cli/test/store/index.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch amondnet/121

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codacy-production

codacy-production Bot commented Jul 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity · 4 duplication

Metric Results
Complexity 5
Duplication 4

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploying ask with  Cloudflare Pages  Cloudflare Pages

Latest commit: 716d716
Status: ✅  Deploy successful!
Preview URL: https://58a8b462.ask-6im.pages.dev
Branch Preview URL: https://amondnet-121.ask-6im.pages.dev

View logs

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes two compounding bugs that caused scoped monorepo packages like @tanstack/react-query@5.101.2 to fail version-pinned checkouts: the npm resolver now tries the full scoped tag name before the unscoped fallback, and githubStorePath encodes / and \ in ref segments as __ with a short SHA-256 suffix (for injectivity) rather than rejecting them outright.

  • Resolver fix (npm.ts, npm.rs): Scoped monorepo packages now prepend @scope/pkg@version and @scope/pkg@vversion ahead of the unscoped candidates, while unscoped packages remain unchanged; meta.ref is propagated as the winning candidate from all code paths (store-hit, shallow clone, tar.gz).
  • Store path fix (store/index.ts, store/paths.rs): encodeRefSegment replaces path separators with __ and appends an 8-char SHA-256 suffix of the original ref so slash-twin refs can never share a cache directory; traversal via .. is still caught by assertSafeSegment after encoding.
  • ensure_checkout both ports: actualRef / actual_ref now prefers meta.ref over the checkout dir basename, ensuring the real unencoded tag is reported in ask src --json even though the directory uses the encoded form.

Confidence Score: 4/5

The resolver and store-path encoding changes are correct and well-tested, but the tar.gz fallback path in both ports still interpolates slash-containing candidates directly into HTTP archive URLs without percent-encoding, meaning scoped monorepo tag resolution will fail whenever git is unavailable and the clone path falls through.

The core logic — scoped-first candidate ordering, injective encodeRefSegment, and meta.ref propagation — is sound and validated by an extensive test suite including cross-language parity checks. The outstanding gap is the tar.gz fallback: a candidate like @tanstack/react-query@5.101.2 embedded verbatim in the archive URL produces a malformed path that will 404, leaving the scoped-tag fix incomplete on git-less environments. This was flagged in the prior review round and remains unaddressed.

packages/cli/src/sources/github.ts (fetchFromTarGz) and crates/ask/src/sources/github.rs (fetch_from_tar_gz) — both embed candidate unencoded into archive URL paths.

Important Files Changed

Filename Overview
packages/cli/src/resolvers/npm.ts Adds scoped-first candidates for monorepo packages; unscoped fallback logic unchanged. Logic is correct and well-tested.
packages/cli/src/store/index.ts Adds encodeRefSegment with injective hash suffix; githubStorePath encodes before the traversal guard. Implementation is sound — the hash disambiguates slash-twin refs.
packages/cli/src/sources/github.ts Store-cache hit now reports the winning candidate in meta.ref and shallow clone path sets meta.ref: winningCandidate. Tar.gz fallback URL still interpolates candidate unencoded (previously flagged P1 — not addressed by this PR).
crates/ask/src/sources/github.rs Store-cache hit and shallow clone path both set meta.ref_ to the winning candidate correctly. Tar.gz fallback URL interpolates candidate unencoded into the HTTP path (previously flagged P1 — not addressed by this PR).
crates/ask/src/store/paths.rs Rust port of encodeRefSegment using SHA-256 from sha2 crate; hash output matches the TS side (validated by parity harness and consistent test assertions). Traversal guard is applied after encoding.
crates/ask/src/resolvers/npm.rs Rust port mirrors the TS resolver change: scoped candidates prepended, unscoped fallback preserved. Uses split_once('/') safely.
packages/cli/src/commands/ensure-checkout.ts Prefers fetchResult?.meta?.ref over basename for actualRef; falls back gracefully. Logic is correct.
crates/ask/src/commands/ensure_checkout.rs Rust mirror of the TS actualRef change; fetch_result.meta.ref_ preferred over basename with safe fallback chain.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["npm resolve('@tanstack/react-query', '5.101.2')"] --> B{monorepo package?}
    B -->|No| C["fallbackRefs: ['5.101.2']"]
    B -->|Yes + scoped| D["push '@tanstack/react-query@5.101.2'\npush '@tanstack/react-query@v5.101.2'"]
    D --> E["push 'react-query@5.101.2'\npush 'react-query@v5.101.2'"]
    E --> F["push '5.101.2'"]
    F --> G["GithubSource.fetch(fallbackRefs)"]
    G --> H{store-cache hit?}
    H -->|Yes| I["meta.ref = candidate\n(winning unencoded tag)"]
    H -->|No| J{has git?}
    J -->|Yes| K["shallow clone\nwinningCandidate = '@tanstack/react-query@5.101.2'"]
    K --> L["github_store_path\n→ encodeRefSegment\n→ '@tanstack__react-query@5.101.2-8cd04c22'"]
    L --> M["meta.ref = winningCandidate\n(unencoded)"]
    J -->|No| N["fetchFromTarGz\nURL: /archive/refs/tags/{candidate}.tar.gz\n⚠ slash unencoded — known P1"]
    I --> O["ensureCheckout\nactualRef = meta.ref\n= '@tanstack/react-query@5.101.2'"]
    M --> O
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["npm resolve('@tanstack/react-query', '5.101.2')"] --> B{monorepo package?}
    B -->|No| C["fallbackRefs: ['5.101.2']"]
    B -->|Yes + scoped| D["push '@tanstack/react-query@5.101.2'\npush '@tanstack/react-query@v5.101.2'"]
    D --> E["push 'react-query@5.101.2'\npush 'react-query@v5.101.2'"]
    E --> F["push '5.101.2'"]
    F --> G["GithubSource.fetch(fallbackRefs)"]
    G --> H{store-cache hit?}
    H -->|Yes| I["meta.ref = candidate\n(winning unencoded tag)"]
    H -->|No| J{has git?}
    J -->|Yes| K["shallow clone\nwinningCandidate = '@tanstack/react-query@5.101.2'"]
    K --> L["github_store_path\n→ encodeRefSegment\n→ '@tanstack__react-query@5.101.2-8cd04c22'"]
    L --> M["meta.ref = winningCandidate\n(unencoded)"]
    J -->|No| N["fetchFromTarGz\nURL: /archive/refs/tags/{candidate}.tar.gz\n⚠ slash unencoded — known P1"]
    I --> O["ensureCheckout\nactualRef = meta.ref\n= '@tanstack/react-query@5.101.2'"]
    M --> O
Loading

Reviews (2): Last reviewed commit: "fix(cli): make encodeRefSegment injectiv..." | Re-trigger Greptile

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploying ask-registry with  Cloudflare Pages  Cloudflare Pages

Latest commit: 716d716
Status: ✅  Deploy successful!
Preview URL: https://88099202.ask-registry.pages.dev
Branch Preview URL: https://amondnet-121.ask-registry.pages.dev

View logs

Comment thread packages/cli/src/store/index.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 11 files

Architecture diagram
sequenceDiagram
    participant CLI as ask CLI
    participant Npm as NpmResolver
    participant Github as GithubSource
    participant Store as Store Paths
    participant Git as Git
    participant Filesystem as Filesystem Cache

    Note over CLI,Filesystem: Version-pinned scoped monorepo resolution flow

    CLI->>Npm: resolve("npm:@tanstack/react-query", "latest")
    Npm->>Npm: Fetch npm registry metadata
    Npm->>Npm: Detect repository.directory (monorepo)
    Npm->>Npm: Build fallbackRefs: [@tanstack/react-query@5.101.2, @tanstack/react-query@v5.101.2, react-query@5.101.2, react-query@v5.101.2, 5.101.2]
    Npm-->>CLI: ResolveResult { repo: TanStack/query, ref: v5.101.2, fallbackRefs }

    CLI->>Github: fetch({ repo, tag: v5.101.2, fallbackRefs })
    
    alt Store cache hit (encoded ref dir exists)
        Github->>Store: githubStorePath(askHome, github.com, TanStack, query, @tanstack/react-query@5.101.2)
        Store->>Store: encodeRefSegment(@tanstack/react-query@5.101.2) -> @tanstack__react-query@5.101.2
        Store->>Store: assertSafeSegment(tag, @tanstack__react-query@5.101.2)
        Store-->>Github: Path: .../TanStack/query/@tanstack__react-query@5.101.2
        Github->>Filesystem: Check path exists
        Filesystem-->>Github: Exists
        Github-->>CLI: FetchResult { storePath, meta.ref: @tanstack/react-query@5.101.2, fromStoreCache: true }
    else No store cache hit
        Github->>Git: Try clone with tag: v5.101.2
        alt Tag not found
            Github->>Git: Try clone with fallbackRefs (in order)
            Git->>Git: Clone @tanstack/react-query@5.101.2 tag
            Git-->>Github: Clone successful
        end
        Github->>Store: githubStorePath(askHome, github.com, TanStack, query, @tanstack/react-query@5.101.2)
        Store->>Store: encodeRefSegment(@tanstack/react-query@5.101.2) -> @tanstack__react-query@5.101.2
        Store-->>Github: Encoded store path
        Github->>Github: Move clone to encoded store dir
        Github-->>CLI: FetchResult { storePath, meta.ref: @tanstack/react-query@5.101.2 }
    end

    CLI->>CLI: ensureCheckout(fetchResult)
    CLI->>CLI: resolvedCheckoutDir = fetchResult.storePath
    CLI->>CLI: actualRef = fetchResult.meta.ref (prefer over basename)
    CLI-->>CLI: Return { ref: @tanstack/react-query@5.101.2, path: .../@tanstack__react-query@5.101.2/docs }

    Note over CLI,Filesystem: Key invariants:
    Note over CLI,Filesystem: - Store dir: @tanstack__react-query@5.101.2 (encoded)
    Note over CLI,Filesystem: - meta.ref: @tanstack/react-query@5.101.2 (real tag)
    Note over CLI,Filesystem: - assertSafeSegment still rejects .. traversal after encoding
Loading

Auto-approved: Fixes scoped monorepo tag resolution and path encoding; well-tested, low risk, no logic changes outside the bug scope.

Re-trigger cubic

The plain `__` substitution collides a `release/v1.0.0` branch with a
literal `release__v1.0.0` tag — whichever is fetched first would shadow
the other with a silent stale-cache hit. Append an 8-char sha256 of the
ORIGINAL ref when it contains a separator; refs without separators (the
common case) still map to themselves unchanged.

Addresses greptile review on #122.
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Auto-approved: Fixes scoped monorepo tag resolution and store path encoding, both in Rust and TypeScript. All changes are isolated to the resolver and store-path logic, with thorough tests in both ports.

Re-trigger cubic

@amondnet amondnet merged commit a15f210 into main Jul 5, 2026
13 checks passed
@amondnet amondnet deleted the amondnet/121 branch July 5, 2026 10:08
amondnet added a commit to pleaseai/claude-code-plugins that referenced this pull request Jul 7, 2026
* feat(tanstack): add TanStack intent-based skills plugin

Single use-tanstack skill that drives the official @tanstack/intent CLI
to discover and load Agent Skills shipped inside the installed
@tanstack/* npm packages, so guidance always matches the exact library
versions in the project. Falls back to tanstack.com markdown docs for
packages that do not ship intent skills yet.

* feat(tanstack): prefer ask CLI for docs fallback

When a package ships no intent skills, prefer a locally cached ask
docs/source tree over curling tanstack.com pages. Pinned to @main
because ask cannot resolve TanStack monorepo release tags
(@tanstack/<pkg>@<version>) yet.

* fix(tanstack): use version-pinned ask npm: specifiers in docs fallback

ask >= 0.4.9 (pleaseai/ask#122) resolves scoped monorepo tags like
@tanstack/react-query@5.101.2, so the fallback no longer needs to pin
github:TanStack/query@main. Keep the @main path as a caveat for older
ask versions.

* chore: update agent memory

* chore(tanstack): apply AI code review suggestions

- fix skill description: react-router -> @tanstack/react-router (P1: wrong-library activation)
- harden ask docs path extraction against format/notice shifts (P2)
- README: use bun run plugins:multi-format script instead of direct cli.ts call

* chore(tanstack): apply gemini review suggestions

- handle missing trailing newline in while-read loops (|| [ -n "$p" ])
- apply the same existing-dir filter to ASK_SRC as ASK_DOCS
- curl -fsSL for tanstack.com doc fallbacks (follow redirects, fail on error)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

npm resolver cannot version-pin scoped monorepo tags (@tanstack/react-query@5.101.2): unsafe-path guard + tarball candidates miss the real tag

1 participant